Files
cthulhu/internal/app/webscripts_test.go

115 lines
4.4 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"
"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")
}
})
}
}