Files
cthulhu/internal/app/window_speech_test.go

101 lines
4.1 KiB
Go

// 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)
})
}
}