119 lines
3.8 KiB
Go
119 lines
3.8 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package atspi
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNativeBrowserAtomicLiveRegion(t *testing.T) {
|
|
if os.Getenv("CTHULHU_NATIVE_BROWSER") != "1" {
|
|
t.Skip("requires private browser session")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
|
|
defer cancel()
|
|
adapter := New()
|
|
if err := adapter.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
stop, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
if err := adapter.Stop(stop); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
window, err := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", "Cthulhu Browser Probe").Output()
|
|
if err != nil || len(strings.Fields(string(window))) == 0 {
|
|
t.Fatalf("browser window: %s: %v", window, err)
|
|
}
|
|
if out, err := exec.CommandContext(ctx, "xdotool", "windowfocus", strings.Fields(string(window))[0]).CombinedOutput(); err != nil {
|
|
t.Fatalf("focus: %s: %v", out, err)
|
|
}
|
|
for _, test := range []struct {
|
|
key, marker, want string
|
|
atomic bool
|
|
}{
|
|
{"F9", "Completed", "Download status: Completed", true},
|
|
{"F10", "Verified", "Account status: Verified", true},
|
|
{"F11", "Inner update", "Inner update", false},
|
|
} {
|
|
t.Run(test.key, func(t *testing.T) {
|
|
if out, err := exec.CommandContext(ctx, "xdotool", "key", test.key).CombinedOutput(); err != nil {
|
|
t.Fatalf("trigger: %s: %v", out, err)
|
|
}
|
|
deadline := time.NewTimer(5 * time.Second)
|
|
defer deadline.Stop()
|
|
for {
|
|
select {
|
|
case event := <-adapter.Events():
|
|
if !event.Source.LiveRegion || (!strings.Contains(event.Source.Text, test.marker) && !strings.Contains(event.Payload, test.marker)) {
|
|
continue
|
|
}
|
|
if event.Source.LiveAtomic != test.atomic || event.Source.LiveAtomic && event.Source.Text != test.want {
|
|
t.Fatalf("atomic=%v text=%q payload=%q; want atomic=%v text=%q", event.Source.LiveAtomic, event.Source.Text, event.Payload, test.atomic, test.want)
|
|
}
|
|
if !test.atomic && strings.Contains(event.Source.Text, "Outer context:") {
|
|
t.Fatalf("crossed atomic=false boundary: %q", event.Source.Text)
|
|
}
|
|
return
|
|
case <-deadline.C:
|
|
t.Fatal("live update missing")
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
// A busy region must remain silent while its intermediate contents change.
|
|
if out, err := exec.CommandContext(ctx, "xdotool", "key", "F12").CombinedOutput(); err != nil {
|
|
t.Fatalf("busy start: %s %v", out, err)
|
|
}
|
|
quiet := time.NewTimer(200 * time.Millisecond)
|
|
quietLoop:
|
|
for {
|
|
select {
|
|
case event := <-adapter.Events():
|
|
if event.Source.LiveRegion && (strings.Contains(event.Source.Text, "Intermediate update") || strings.Contains(event.Payload, "Intermediate update")) {
|
|
t.Fatalf("busy region announced incomplete text: %+v", event)
|
|
}
|
|
case <-quiet.C:
|
|
break quietLoop
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
}
|
|
quiet.Stop()
|
|
if out, err := exec.CommandContext(ctx, "xdotool", "key", "F7").CombinedOutput(); err != nil {
|
|
t.Fatalf("busy complete: %s %v", out, err)
|
|
}
|
|
completion := time.NewTimer(5 * time.Second)
|
|
defer completion.Stop()
|
|
for {
|
|
select {
|
|
case event := <-adapter.Events():
|
|
if event.Source.LiveRegion && strings.Contains(event.Source.Text, "Intermediate update") {
|
|
if event.Type != TextInsertedEvent && event.Type != ChildrenAddedEvent {
|
|
t.Fatalf("live completion changed focus/lifecycle: %+v", event)
|
|
}
|
|
if event.Source.Text != "Busy status: Intermediate update" {
|
|
t.Fatalf("completion text: %q", event.Source.Text)
|
|
}
|
|
return
|
|
}
|
|
case <-completion.C:
|
|
t.Fatal("busy completion missing")
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
}
|
|
|
|
}
|