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