148 lines
4.3 KiB
Go
148 lines
4.3 KiB
Go
// 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")
|
|
}
|