Files
cthulhu/internal/app/window_title_command_test.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")
}