// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "log/slog" "testing" "git.stormux.org/storm/cthulhu/internal/atspi" "git.stormux.org/storm/cthulhu/internal/input" "git.stormux.org/storm/cthulhu/internal/presentation" ) func TestWindowTitleQueuesFollowingPresentation(t *testing.T) { for _, title := range []string{"Cthulhu Preferences", "Cthulhu Preferences for Example Application"} { t.Run(title, func(t *testing.T) { speaker := &controlledSpeaker{calls: make(chan speechCall, 10)} worker := newSpeechWorker(slog.New(slog.DiscardHandler), speaker) if err := worker.Start(context.Background()); err != nil { t.Fatal(err) } t.Cleanup(func() { worker.Stop(context.Background()) }) presenter := eventPresenter{state: &eventLoopState{}, logger: slog.New(slog.DiscardHandler), speech: worker} presenter.state.presentationOptions = presentation.DefaultOptions() activation := ownerEvent(atspi.WindowActivatedEvent, ":1.70", "/window", "/window") activation.Source.Name, activation.Source.Role = title, "dialog" activation.WindowTitle = title presenter.speak(activation) close(expectSpeechCall(t, speaker, "cancel").done) titleCall := expectSpeechCall(t, speaker, title) worker.ProtectCommand(100) if worker.InterruptForInput(input.Event{Pressed: true, Timestamp: 100}) { t.Fatal("delayed initiating shortcut interrupted the title") } focus := ownerEvent(atspi.FocusedStateEvent, ":1.70", "/window", "/general") focus.Source.Name, focus.Source.Role = "General", "page tab" focus.Detail1 = 1 presenter.speak(focus) close(titleCall.done) close(expectSpeechCall(t, speaker, "dialog").done) // A CANCEL here would discard the title still queued in the // synthesizer, even though its SPEAK call has returned. close(expectSpeechCall(t, speaker, "General").done) close(expectSpeechCall(t, speaker, "page tab").done) focus.Source.Path, focus.Source.Name = "/speech", "Speech" presenter.speak(focus) close(expectSpeechCall(t, speaker, "cancel").done) close(expectSpeechCall(t, speaker, "Speech").done) close(expectSpeechCall(t, speaker, "page tab").done) }) } } func TestWindowTitlePairingIsScoped(t *testing.T) { for _, test := range []struct { name string window atspi.ObjectID untitled bool interrupt func(*speechWorker) }{ {name: "another window", window: atspi.NewObjectID(":1.70", "/other")}, {name: "another application", window: atspi.NewObjectID(":1.71", "/window")}, {name: "unowned output"}, {name: "no title announcement", window: atspi.NewObjectID(":1.70", "/window"), untitled: true}, {name: "explicit interruption", window: atspi.NewObjectID(":1.70", "/window"), interrupt: (*speechWorker).Interrupt}, {name: "navigation key", window: atspi.NewObjectID(":1.70", "/window"), interrupt: func(worker *speechWorker) { worker.InterruptForInput(input.Event{Pressed: true, Timestamp: 101}) }}, } { t.Run(test.name, func(t *testing.T) { speaker := &controlledSpeaker{calls: make(chan speechCall, 10)} worker := newSpeechWorker(slog.New(slog.DiscardHandler), speaker) if err := worker.Start(context.Background()); err != nil { t.Fatal(err) } t.Cleanup(func() { worker.Stop(context.Background()) }) worker.Enqueue(speechRequest{Text: "Window title", Window: atspi.NewObjectID(":1.70", "/window"), IntroducesWindow: !test.untitled}) close(expectSpeechCall(t, speaker, "cancel").done) titleCall := expectSpeechCall(t, speaker, "Window title") if test.interrupt != nil { test.interrupt(worker) close(expectSpeechCall(t, speaker, "cancel").done) } worker.Enqueue(speechRequest{Text: "Next control", Window: test.window}) close(titleCall.done) close(expectSpeechCall(t, speaker, "cancel").done) if test.interrupt != nil { // The cancellation barrier settles the in-flight title // before the new focus request performs its own CANCEL. close(expectSpeechCall(t, speaker, "cancel").done) } close(expectSpeechCall(t, speaker, "Next control").done) }) } }