Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,100 @@
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user