// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "fmt" "testing" "time" "git.stormux.org/storm/cthulhu/internal/atspi" "git.stormux.org/storm/cthulhu/internal/input" "git.stormux.org/storm/cthulhu/internal/review" ) type listTestSource struct { keys *uiTestKeys selected chan int count int } func (source *listTestSource) ListDocument(context.Context, review.DocumentTarget, int) (review.DocumentListSnapshot, error) { list := review.DocumentListSnapshot{Selected: source.count - 1} for index := 0; index < source.count; index++ { list.Entries = append(list.Entries, review.DocumentListEntry{DocumentPosition: review.DocumentPosition{Snapshot: atspi.Snapshot{Name: fmt.Sprintf("Example %03d", index), Role: "link"}}}) } return list, nil } func (source *listTestSource) SelectDocumentList(_ review.DocumentListSnapshot, index int) error { if source.keys.InternalUISession() != 0 { index = -1 } source.selected <- index return nil } func TestBrowserListNavigationSelectionAndCancellation(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) keys := &uiTestKeys{} source := &listTestSource{keys: keys, selected: make(chan int, 1), count: 300} output := &ocrTestOutput{speech: make(chan speechRequest, 16)} ui := &internalUI{keys: keys, documents: source, speech: output, gate: &focusGate{}, events: make(chan input.CommandEvent, 16), done: make(chan struct{})} go ui.run(ctx) defer func() { cancel(); <-ui.done }() wait := func(want string) { t.Helper() select { case message := <-output.speech: if message.Text != want { t.Fatalf("got %q want %q", message.Text, want) } case <-ctx.Done(): t.Fatal(ctx.Err()) } } send := func(symbol uint32) { ui.Handle(input.CommandEvent{Command: input.InternalUIKey, UISession: 1, Chord: input.CommandChord{Symbol: symbol}}) } ui.Handle(input.CommandEvent{Command: input.DocumentListLinks}) wait("Links: 300 items found. Example 299") send(0xff50) wait("Example 000") send(0xff54) wait("Example 001") send(0xff0d) select { case index := <-source.selected: if index != 1 { t.Fatalf("selected %d or keyboard capture retained", index) } case <-ctx.Done(): t.Fatal("selection did not finish") } ui.Handle(input.CommandEvent{Command: input.DocumentListLinks}) wait("Links: 300 items found. Example 299") send(0xff1b) // A second request is processed after Escape, establishing cancellation // without a timing assumption or a selection callback. ui.Handle(input.CommandEvent{Command: input.DocumentListLinks}) wait("Links: 300 items found. Example 299") select { case <-source.selected: t.Fatal("Escape selected an item") default: } send(0xff1b) } func TestBrowserListCatalogMapsAllOrcaListTypes(t *testing.T) { for _, binding := range input.DocumentListBindings() { if documentListTarget(binding) == 0 { t.Fatalf("unhandled %s", binding.Command) } } }