Files
cthulhu/internal/atspi/browser_live_native_test.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())
}
}
}