// 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/input" "git.stormux.org/storm/cthulhu/internal/platform/xorg" "git.stormux.org/storm/cthulhu/internal/review" ) func TestNativeBrowserTables(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) } } var focus atspi.Snapshot for attempt := 0; attempt < 25 && focus.Name != "Browser foundation"; attempt++ { keys("Tab") timer := time.NewTimer(200 * time.Millisecond) drain: for { select { case event := <-adapter.Events(): if isFocusEvent(event.Type) { focus = event.Source } if focus.Name == "Browser foundation" { break drain } case <-timer.C: break drain case <-ctx.Done(): t.Fatal(ctx.Err()) } } timer.Stop() } if focus.Name != "Browser foundation" { t.Fatal("fixture focus missing") } 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.ID()) commands := xorg.NewCommands([]string{"Insert", "KP_Insert"}, input.DesktopKeyboardLayout) if err = commands.Start(ctx); err != nil { t.Fatal(err) } defer commands.Stop(context.Background()) applied := func(enabled bool) { t.Helper() commands.SetBrowseNavigation(enabled) deadline := time.Now().Add(time.Second) for commands.BrowseNavigationEnabled() != enabled && time.Now().Before(deadline) { time.Sleep(10 * time.Millisecond) } if commands.BrowseNavigationEnabled() != enabled { t.Fatal("browse grabs did not change") } } applied(true) command := func(chord string) input.Command { t.Helper() keys(chord) select { case got := <-commands.Commands(): return got.Command case err := <-commands.Failures(): t.Fatal(err) case <-time.After(2 * time.Second): t.Fatalf("no command for %s", chord) } return "" } // Mode changes replace native browse grabs between successive A presses. if got := command("Insert+a"); got != input.ToggleDocumentMode { t.Fatal(got) } applied(false) if got := command("Insert+a"); got != input.StickyFocusMode { t.Fatal(got) } if got := command("Insert+a"); got != input.StickyBrowseMode { t.Fatal(got) } applied(true) result := func() review.Result { t.Helper() select { case got := <-worker.Results(): if got.Err != nil || got.CaretErr != nil { t.Fatalf("navigation errors %v %v", got.Err, got.CaretErr) } return got case <-ctx.Done(): t.Fatal(ctx.Err()) } return review.Result{} } if got := command("t"); got != input.DocumentNextTable { t.Fatal(got) } if err = worker.MoveDocument(review.DocumentTable, 0, false); err != nil { t.Fatal(err) } if got := result(); got.DocumentPosition.Snapshot.Name != "Navigation probe" { t.Fatalf("table %+v", got) } for _, step := range []struct { key, text string moved bool }{ {"alt+shift+Home", "Item", true}, {"alt+shift+Right", "Count", true}, {"alt+shift+Down", "Five", true}, {"alt+shift+Down", "Spans 2 columns.", true}, {"alt+shift+Down", "blank Row 4, column 2.", true}, {"alt+shift+Down", "Bottom of column.", false}, {"alt+shift+Up", "Merged total", true}, {"alt+shift+Up", "Five", true}, {"alt+shift+End", "blank", true}, {"alt+shift+Left", "Last item", true}, } { cmd := command(step.key) if err = worker.MoveDocumentCaret(documentCaretCommand(cmd)); err != nil { t.Fatal(err) } got := result() if got.Moved != step.moved || !strings.Contains(got.DocumentCaretText, step.text) { t.Fatalf("%s got %+v want %s", step.key, got, step.text) } if step.text == "Five" && !strings.Contains(got.DocumentCaretText, "Apples") { t.Fatalf("row header missing: %q", got.DocumentCaretText) } if step.text == "Five" && (got.DocumentTableRowHeader != "Apples" || got.DocumentTableColumnHeader != "Count") { t.Fatalf("table header metadata missing for verbosity: %+v", got) } } }