101 lines
4.1 KiB
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)
|
|
})
|
|
}
|
|
}
|