// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package review import ( "context" "os" "os/exec" "strings" "testing" "time" "git.stormux.org/storm/cthulhu/internal/atspi" ) func TestNativeBrowserChromeFlatReview(t *testing.T) { if os.Getenv("CTHULHU_NATIVE_BROWSER") != "1" { t.Skip("requires private browser session") } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() adapter := atspi.New() if err := adapter.Start(ctx); err != nil { t.Fatal(err) } defer adapter.Stop(context.Background()) keys := func(chord string) { t.Helper() if out, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", chord).CombinedOutput(); err != nil { t.Fatalf("keys: %v %s", err, out) } } wait := func(match func(atspi.Event) bool) atspi.Event { t.Helper() for { select { case e := <-adapter.Events(): if match(e) { return e } case <-ctx.Done(): t.Fatal("missing browser event: ", ctx.Err()) return atspi.Event{} } } } capture := func(focus atspi.ObjectID) ([]flatLine, atspi.ReviewSnapshot) { t.Helper() tree, err := adapter.CaptureReviewWindow(ctx, focus) if err != nil { t.Fatal(err) } snapshot, err := tree.ReviewWindow(ctx, focus) if err != nil { t.Fatal(err) } lines, status, err := build_flat_layout(ctx, snapshot.Nodes) if err != nil { t.Fatal(err) } t.Logf("review lines=%d completeness=%+v", len(lines), status) return lines, snapshot } contains := func(lines []flatLine, want string) bool { for _, line := range lines { for _, zone := range line.zones { if strings.Contains(zone.text, want) { return true } } } return false } keys("ctrl+l") address := wait(func(e atspi.Event) bool { return (e.Type == atspi.FocusEvent || e.Type == atspi.FocusedStateEvent && e.Detail1 != 0) && e.Source.Role == "entry" }) lines, snapshot := capture(address.Source.ID()) for _, name := range []string{"Back", "Forward", "Reload", "New Tab"} { if !contains(lines, name) { t.Errorf("browser control missing from review: %q", name) } } tabLabel := false for _, line := range lines { for _, zone := range line.zones { tabLabel = tabLabel || zone.role == "page tab" && strings.Contains(zone.text, "Cthulhu Browser Probe") } } if !tabLabel { t.Error("tab's label missing beside its Close button") } for _, n := range snapshot.Nodes { if n.Text.Issue != "" { t.Logf("unresolved text object: role=%s issue=%s children=%d", n.Snapshot.Role, n.Text.Issue, len(n.Children)) } } // Chrome currently returns zero character/range geometry for the address // value. Do not substitute its accessible label for the missing URL. for _, n := range snapshot.Nodes { if n.Snapshot.ID() != address.Source.ID() { continue } if !n.Text.Known && n.Text.Issue == "" { t.Fatal("unavailable address text lost its diagnostic") } if n.Text.Issue != "" { for _, line := range lines { for _, zone := range line.zones { if zone.object == address.Source.ID() { t.Fatalf("address label substituted for unavailable URL: %+v", zone) } } } t.Logf("address value remains unavailable: %s", n.Text.Issue) } else { t.Logf("address value captured in %d spans", len(n.Text.Ranges)) } } keys("F6") wait(func(e atspi.Event) bool { return (e.Type == atspi.FocusEvent || e.Type == atspi.FocusedStateEvent && e.Detail1 != 0) && e.Source.Role != "entry" }) unfocusedLines, unfocusedSnapshot := capture(address.Source.ID()) for _, n := range unfocusedSnapshot.Nodes { if n.Snapshot.ID() == address.Source.ID() && !n.Text.Known && n.Text.Issue == "" { t.Fatal("unfocused address text lost its diagnostic") } } if !contains(unfocusedLines, "Reload") { t.Fatal("toolbar lost after leaving address bar") } keys("Escape") keys("F10") wait(func(e atspi.Event) bool { return (e.Type == atspi.FocusEvent || e.Type == atspi.FocusedStateEvent && e.Detail1 != 0) && e.Source.Role == "button" }) keys("Down") menu := wait(func(e atspi.Event) bool { return e.Type == atspi.WindowActivatedEvent && e.Source.Role == "menu" }) lines, _ = capture(menu.Source.ID()) if !contains(lines, "New tab") && !contains(lines, "New Tab") { t.Fatal("popup menu entries missing from flat review") } keys("Escape") }