Files
cthulhu/internal/app/browser_disabled_fields_native_test.go

118 lines
3.4 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"os"
"os/exec"
"strings"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/review"
)
func TestNativeBrowserDisabledFieldsRemainReadable(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 := atspi.New()
if err := adapter.Start(ctx); err != nil {
t.Fatal(err)
}
defer adapter.Stop(context.Background())
command := func(args ...string) {
t.Helper()
if output, err := exec.CommandContext(ctx, "xdotool", args...).CombinedOutput(); err != nil {
t.Fatalf("xdotool: %v: %s", err, output)
}
}
windows, err := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", "Cthulhu Browser Probe").Output()
if err != nil || len(strings.Fields(string(windows))) == 0 {
t.Fatalf("fixture window: %s %v", windows, err)
}
command("windowfocus", strings.Fields(string(windows))[0])
var focus atspi.ObjectID
for attempt := 0; attempt < 12 && !focus.Valid(); attempt++ {
command("windowfocus", strings.Fields(string(windows))[0])
command("key", "--clearmodifiers", "Tab")
timer := time.NewTimer(300 * time.Millisecond)
waitFocus:
for {
select {
case event := <-adapter.Events():
if (event.Type == atspi.FocusEvent || event.Type == atspi.FocusedStateEvent && event.Detail1 == 1) && event.Source.Name == "Read only answer" {
focus = event.Source.ID()
break waitFocus
}
case <-timer.C:
break waitFocus
case <-ctx.Done():
t.Fatal(ctx.Err())
}
}
timer.Stop()
}
if !focus.Valid() {
t.Fatal("fixture focus missing")
}
// Capture before the transition to exercise invalidation of a retained tree.
if _, err := adapter.CaptureDocument(ctx, focus); err != nil {
t.Fatal(err)
}
command("windowfocus", strings.Fields(string(windows))[0])
command("key", "F9")
for {
select {
case event := <-adapter.Events():
if event.Type != "object:state-changed:sensitive" || event.Source.Name != "Answer" || event.Detail1 != 0 {
continue
}
case <-ctx.Done():
t.Fatal("disabled state update missing: ", ctx.Err())
}
break
}
worker, err := review.NewWorker(adapter, 2*time.Second)
if err != nil {
t.Fatal(err)
}
if err := worker.Start(ctx); err != nil {
t.Fatal(err)
}
defer worker.Stop(context.Background())
worker.SyncFocus(focus)
for _, step := range []struct {
command review.DocumentCaretCommand
want string
}{
{review.CaretDocumentStart, "Before fields"},
{review.CaretNextLine, ""}, // Keep the named row, then enter its disabled fields.
{review.CaretNextLine, "Solved"},
{review.CaretNextCharacter, "o"},
{review.CaretNextLine, "Confirmed"},
{review.CaretNextLine, "After fields"},
{review.CaretPreviousLine, "Confirmed"},
{review.CaretPreviousLine, "Solved"},
} {
if err := worker.MoveDocumentCaret(step.command); err != nil {
t.Fatal(err)
}
select {
case result := <-worker.Results():
if result.Err != nil || !result.Moved || result.DocumentCaretText != step.want {
t.Fatalf("%s: text=%q moved=%t err=%v; want %q", step.command,
result.DocumentCaretText, result.Moved, result.Err, step.want)
}
case <-ctx.Done():
t.Fatal(ctx.Err())
}
}
}