package app import ( "context" "errors" "log/slog" "reflect" "testing" "time" "git.stormux.org/storm/cthulhu/internal/atspi" "git.stormux.org/storm/cthulhu/internal/input" "git.stormux.org/storm/cthulhu/internal/review" ) type currentLineAdapter struct{ *fakeAdapter } func (*currentLineAdapter) ReadFocusedLine(_ context.Context, focus atspi.ObjectID) (atspi.DocumentContent, error) { return atspi.DocumentContent{Text: "Complete caret line", Snapshot: atspi.Snapshot{BusName: focus.BusName, Path: focus.Path, Role: "entry"}}, nil } func TestCurrentLineEventLoopPreservesFocusedItem(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) adapter := ¤tLineAdapter{&fakeAdapter{events: make(chan atspi.Event)}} worker, _ := review.NewWorker(adapter, time.Second) if err := worker.Start(ctx); err != nil { t.Fatal(err) } queue := &fakeSpeechQueue{spoken: make(chan string, 8)} requests := make(chan input.Command) done := make(chan error, 1) go func() { done <- processEventsWithSettings(ctx, slog.New(slog.DiscardHandler), queue, adapter, requests, worker, 0, nil) }() t.Cleanup(func() { cancel() if err := worker.Stop(context.Background()); err != nil { t.Error(err) } <-done }) waitSpeech := func(want string) { t.Helper() select { case got := <-queue.spoken: if got != want { t.Fatalf("speech %q, want %q", got, want) } case <-time.After(time.Second): t.Fatalf("missing speech %q", want) } } focus := atspi.NewSnapshot("/entry", "Original item", "entry", "", "") focus.BusName = ":1.42" adapter.events <- atspi.NewEvent(atspi.FocusEvent, focus) waitSpeech("Original item, entry") requests <- input.PresentCurrentLine waitSpeech("Complete caret line") requests <- input.WhereAmI waitSpeech("Original item, entry") } func TestCurrentLinePresentationAndStaleResults(t *testing.T) { focus := atspi.ObjectID{BusName: ":1.42", Path: "/entry"} worker, _ := review.NewWorker(&fakeAdapter{}, time.Second) worker.SyncFocus(focus) queue := &fakeSpeechQueue{requests: make(chan speechRequest, 8), spoken: make(chan string, 8)} handler := eventReviewHandler{objectReview: worker, speech: queue, focusGate: &focusGate{}} for _, probe := range []struct { text string style review.FlatSpeech err error want string spelling []string }{ {"A book", review.FlatSpeak, nil, "A book", nil}, {"A", review.FlatSpell, nil, "A", []string{"A"}}, {"A", review.FlatPhonetic, nil, "A", []string{"alpha"}}, {"\n", review.FlatSpeak, nil, "blank", nil}, {"", review.FlatSpeak, nil, "blank", nil}, {"", review.FlatSpeak, errors.New("unavailable"), "Could not read current line.", nil}, } { result := review.Result{CurrentLine: true, DocumentFocus: focus, DocumentCaretText: probe.text, FlatSpeech: probe.style, Err: probe.err} handler.present_current_line(result) got := <-queue.requests if got.Text != probe.want || !reflect.DeepEqual(got.Spelling, probe.spelling) { t.Fatalf("current line: %+v, want %q %v", got, probe.want, probe.spelling) } } worker.ResetDocument(focus) handler.present_current_line(review.Result{CurrentLine: true, DocumentFocus: focus}) if len(queue.requests) != 0 { t.Fatal("stale report was spoken") } worker.SyncFocus(atspi.ObjectID{BusName: ":1.42", Path: "/other"}) handler.present_current_line(review.Result{CurrentLine: true, DocumentFocus: focus, DocumentEpoch: worker.DocumentEpoch()}) if len(queue.requests) != 0 { t.Fatal("report from previous focus was spoken") } } func TestCurrentLineSecureFocus(t *testing.T) { for _, mode := range []string{"lock", "handoff", "foreground-lock", "foreground-handoff"} { t.Run(mode, func(t *testing.T) { focus := atspi.NewObjectID(":1.42", "/focus") worker, _ := review.NewWorker(&fakeAdapter{}, time.Second) worker.SyncFocus(focus) queue := &fakeSpeechQueue{requests: make(chan speechRequest, 1), spoken: make(chan string, 1)} handler := eventReviewHandler{state: &eventLoopState{}, objectReview: worker, speech: queue, logger: slog.New(slog.DiscardHandler), focusGate: &focusGate{}} handler.state.current = atspi.Snapshot{BusName: focus.BusName, Path: focus.Path, Role: "text"} secure := func() { if mode == "lock" || mode == "foreground-lock" { handler.focusGate.SetLocked(true, nil, nil, nil, nil, &brailleWorker{}) } else { handler.focusGate.SetSuspended(true) } } if mode == "lock" || mode == "handoff" { secure() handler.syncForeground = func() { t.Fatal("secure command synchronized foreground") } } else { handler.syncForeground = secure } handler.current_line(input.PresentCurrentLine) // An already completed read must also be rejected on secure input. result := native_current_line_result() result.DocumentFocus = focus handler.present_current_line(result) if len(queue.requests) != 0 { t.Fatal("current line spoke during lock or handoff") } }) } }