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,114 @@
|
||||
// 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"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/notification"
|
||||
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
|
||||
"git.stormux.org/storm/cthulhu/internal/webscripts"
|
||||
)
|
||||
|
||||
func steamToastEvent(text string) atspi.Event {
|
||||
source := atspi.NewSnapshot("/fragment", "", "static", "", "")
|
||||
source.BusName = ":1.42"
|
||||
source.ApplicationName = "Chromium"
|
||||
source.ApplicationProcess = "steamwebhelper"
|
||||
event := atspi.NewEventWithPayload(atspi.TextInsertedEvent, source, text)
|
||||
event.WebNotification = &webscripts.Notification{BusName: source.BusName, RootPath: "/toast", WindowPath: "/steam", Text: text}
|
||||
event.ForegroundMismatch = true
|
||||
return event
|
||||
}
|
||||
|
||||
func TestSteamBackgroundNotificationReachesSpeechBrailleAndHistory(t *testing.T) {
|
||||
adapter := &foregroundAdapter{fakeAdapter: fakeAdapter{events: make(chan atspi.Event, 8)}, changes: make(chan struct{}, 1)}
|
||||
state := atspi.ForegroundState{Client: xorg.Foreground{Known: true, Window: 101}, Epoch: 1}
|
||||
adapter.state.Store(&state)
|
||||
queue := &fakeSpeechQueue{spoken: make(chan string, 8)}
|
||||
gate := &focusGate{}
|
||||
gate.SetSuspended(true)
|
||||
display := &flashRecorder{}
|
||||
history := notification.NewPresenter()
|
||||
output := outputQueue{speech: gatedSpeech{speech: queue, gate: gate}, braille: display}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
processEventsWithInput(ctx, slog.New(slog.DiscardHandler), output, adapter, nil, nil, 0, nil, nil, gate, history)
|
||||
}()
|
||||
defer func() { cancel(); <-done }()
|
||||
adapter.events <- steamToastEvent("Username")
|
||||
adapter.events <- steamToastEvent("Playing: Example Game")
|
||||
select {
|
||||
case message := <-queue.spoken:
|
||||
if message != "Steam: Username Playing: Example Game" {
|
||||
t.Fatalf("fragmented message: %q", message)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("background Steam notification was filtered or timer did not fire")
|
||||
}
|
||||
if !gate.Suspended() {
|
||||
t.Fatal("background toast took foreground ownership")
|
||||
}
|
||||
if entries := history.Entries(); len(entries) != 1 || entries[0].Message != "Steam: Username Playing: Example Game" {
|
||||
t.Fatalf("notification history: %+v", entries)
|
||||
}
|
||||
if len(display.messages) != 1 || display.messages[0] != "Steam: Username Playing: Example Game" {
|
||||
t.Fatalf("braille messages: %q", display.messages)
|
||||
}
|
||||
// A Chromium page with identical metadata cannot borrow the Steam match.
|
||||
other := steamToastEvent("Unrelated browser content")
|
||||
other.Source.ApplicationProcess = "chromium"
|
||||
other.Source.LiveRegion = true
|
||||
adapter.events <- other
|
||||
// A normal accepted focus event is a barrier for the rejected event above.
|
||||
focus := ownerEvent(atspi.FocusedStateEvent, ":1.43", "/window", "/button")
|
||||
focus.Source.Name, focus.Source.Role = "Current control", "button"
|
||||
focus.Source.Focused, focus.Detail1, focus.Foreground = true, 1, state
|
||||
adapter.events <- focus
|
||||
expectActivationSpeech(t, queue.spoken, "Current control, button")
|
||||
select {
|
||||
case message := <-queue.spoken:
|
||||
t.Fatalf("background Chromium spoke: %q", message)
|
||||
default:
|
||||
}
|
||||
if len(history.Entries()) != 1 {
|
||||
t.Fatal("background Chromium entered notification history")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSteamNotificationTimerCancelledOnLockAndDestruction(t *testing.T) {
|
||||
for _, retire := range []string{"lock", "root", "window"} {
|
||||
t.Run(retire, func(t *testing.T) {
|
||||
queue := &fakeSpeechQueue{spoken: make(chan string, 1)}
|
||||
history := notification.NewPresenter()
|
||||
gate := &focusGate{}
|
||||
controller := &webScriptController{speech: queue, notifications: history, gate: gate}
|
||||
defer controller.reset()
|
||||
if !controller.handle(steamToastEvent("Pending content")) || controller.due == nil {
|
||||
t.Fatal("toast did not arm a timer")
|
||||
}
|
||||
switch retire {
|
||||
case "lock":
|
||||
gate.secure.Store(true)
|
||||
controller.flush()
|
||||
case "root":
|
||||
event := atspi.NewEvent(atspi.DefunctStateEvent, atspi.Snapshot{BusName: ":1.42", Path: "/toast"})
|
||||
event.Detail1 = 1
|
||||
controller.handle(event)
|
||||
case "window":
|
||||
controller.handle(atspi.NewEvent(atspi.WindowDestroyedEvent, atspi.Snapshot{BusName: ":1.42", Path: "/steam"}))
|
||||
}
|
||||
if controller.due != nil || !controller.registry.Deadline().IsZero() || len(history.Entries()) != 0 {
|
||||
t.Fatal("retired toast remained queued")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user