165 lines
5.4 KiB
Go
165 lines
5.4 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// 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/config"
|
|
"git.stormux.org/storm/cthulhu/internal/input"
|
|
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
|
|
)
|
|
|
|
func TestPresentWindowTitleRepeatsDespiteAutomaticPolicy(t *testing.T) {
|
|
titles, queue, changed := titleControllerFixture(t)
|
|
settings := titles.live.Load()
|
|
settings.Reading.AutomaticallyReadTitleChanges = false
|
|
titles.live.Store(settings)
|
|
changed.Source.Name = "Current page - Browser"
|
|
titles.accessible(changed)
|
|
titles.lastTitle = changed.Source.Name
|
|
for range 2 {
|
|
titles.present()
|
|
}
|
|
if len(queue.requests) != 2 || queue.interruptions != 0 || titles.pending != nil {
|
|
t.Fatalf("explicit titles = %+v; interruptions %d; pending %+v", queue.requests, queue.interruptions, titles.pending)
|
|
}
|
|
for _, request := range queue.requests {
|
|
if request.Text != changed.Source.Name || request.Cause != "present-window-title" || request.Role != "system" ||
|
|
request.Append || request.BypassFocusGate || request.Window != titles.state.owner.window {
|
|
t.Fatalf("explicit request = %+v", request)
|
|
}
|
|
}
|
|
titles.flush()
|
|
if len(queue.requests) != 2 {
|
|
t.Fatal("a pending automatic title repeated the explicit request")
|
|
}
|
|
}
|
|
|
|
func TestPresentWindowTitleForegroundSources(t *testing.T) {
|
|
for _, name := range []string{"native", "wine-child", "accessible", "stale-epoch", "wrong-window", "missing"} {
|
|
t.Run(name, func(t *testing.T) {
|
|
titles, queue, _ := titleControllerFixture(t)
|
|
titles.state.owner.title = "Accessible title"
|
|
titles.state.foregroundState = atspi.ForegroundState{Epoch: 2, Client: xorg.Foreground{Known: true, Window: 10}}
|
|
source := &titleProbeSource{title: atspi.ForegroundTitle{Epoch: 2, Title: xorg.WindowTitle{Window: 10, TitleWindow: 10, Title: "Native title"}}}
|
|
titles.source = source
|
|
want := "Native title"
|
|
switch name {
|
|
case "wine-child":
|
|
source.title.Title.TitleWindow = 11
|
|
titles.state.owner.clear()
|
|
case "accessible":
|
|
titles.source = nil
|
|
want = "Accessible title"
|
|
case "stale-epoch":
|
|
source.title.Epoch--
|
|
titles.state.owner.clear()
|
|
want = "Could not find current window title."
|
|
case "wrong-window":
|
|
source.title.Title.Window = 20
|
|
titles.state.owner.clear()
|
|
want = "Could not find current window title."
|
|
case "missing":
|
|
source.title.Title.Title = " "
|
|
titles.state.owner.clear()
|
|
want = "Could not find current window title."
|
|
}
|
|
titles.present()
|
|
if len(queue.requests) != 1 || queue.requests[0].Text != want {
|
|
t.Fatalf("title = %+v, want %q", queue.requests, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPresentWindowTitleFlashesOnBraille(t *testing.T) {
|
|
titles, speech, _ := titleControllerFixture(t)
|
|
display := &flashRecorder{}
|
|
titles.speech = outputQueue{speech: speech, braille: display, live: titles.live}
|
|
titles.present()
|
|
if len(display.messages) != 1 || display.messages[0] != "Original" {
|
|
t.Fatalf("title Braille = %v", display.messages)
|
|
}
|
|
}
|
|
|
|
func TestPresentWindowTitleRespectsLockHandoffAndForegroundChange(t *testing.T) {
|
|
for _, name := range []string{"locked", "suspended", "foreground-changed"} {
|
|
t.Run(name, func(t *testing.T) {
|
|
titles, queue, _ := titleControllerFixture(t)
|
|
titles.gate = &focusGate{}
|
|
switch name {
|
|
case "locked":
|
|
titles.gate.secure.Store(true)
|
|
case "suspended":
|
|
titles.gate.suspended.Store(true)
|
|
case "foreground-changed":
|
|
titles.foreground = &titleProbeSource{title: atspi.ForegroundTitle{Epoch: 1}}
|
|
}
|
|
titles.present()
|
|
if len(queue.requests) != 0 || queue.interruptions != 0 {
|
|
t.Fatalf("unexpected title during %s: %+v", name, queue.requests)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProcessEventsPresentWindowTitlePreservesFocus(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
speech := &fakeSpeechQueue{spoken: make(chan string, 8)}
|
|
adapter := &fakeAdapter{events: make(chan atspi.Event)}
|
|
requests := make(chan input.Command)
|
|
live := &liveSettings{}
|
|
settings := config.DefaultSettings()
|
|
settings.Reading.AutomaticallyReadTitleChanges = false
|
|
live.Store(settings)
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- processEventsWithSettings(ctx, slog.New(slog.DiscardHandler), speech, adapter, requests, nil, 0, live)
|
|
}()
|
|
t.Cleanup(func() {
|
|
cancel()
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Error("event loop did not stop")
|
|
}
|
|
})
|
|
wait := func(want string) {
|
|
t.Helper()
|
|
select {
|
|
case got := <-speech.spoken:
|
|
if got != want {
|
|
t.Fatalf("speech = %q, want %q", got, want)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("missing speech %q", want)
|
|
}
|
|
}
|
|
activation := ownerEvent(atspi.WindowActivatedEvent, ":1.50", "/window", "/window")
|
|
activation.Source.Name, activation.Source.Role = "Page title", "frame"
|
|
adapter.events <- activation
|
|
wait("Page title, frame")
|
|
focus := ownerEvent(atspi.FocusEvent, ":1.50", "/window", "/button")
|
|
focus.Source.Name, focus.Source.Role, focus.Source.Focused = "Next post", "button", true
|
|
adapter.events <- focus
|
|
wait("Next post, button")
|
|
changed := activation
|
|
changed.Type, changed.Source.Name = atspi.AccessibleNameEvent, "Updated page title"
|
|
adapter.events <- changed
|
|
for range 2 {
|
|
requests <- input.PresentWindowTitle
|
|
wait("Updated page title")
|
|
}
|
|
requests <- input.WhereAmI
|
|
wait("Next post, button")
|
|
}
|