// 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/config" "git.stormux.org/storm/cthulhu/internal/platform/xorg" ) func TestTooltipPreferenceGatesTransientSpeechWithoutMovingFocus(t *testing.T) { settings := config.DefaultSettings() live := &liveSettings{} live.Store(settings) queue := &fakeSpeechQueue{spoken: make(chan string, 2), requests: make(chan speechRequest, 2), interrupt: make(chan struct{}, 2)} state := &eventLoopState{} state.focusedID = atspi.ObjectID{BusName: ":1.42", Path: "/button"} handler := accessibilityEventHandler{state: state, live: live, speech: queue} event := atspi.NewEvent(atspi.ShowingStateEvent, atspi.Snapshot{Role: "tool tip", Name: "Save document"}) event.Detail1 = 1 if !handler.handleTooltip(event) || len(queue.requests) != 0 { t.Fatal("disabled tooltip was presented") } settings.Reading.PresentTooltips = true live.Store(settings) if !handler.handleTooltip(event) { t.Fatal("showing tooltip was not handled") } request := <-queue.requests if request.Text != "Save document, tool tip" || request.Cause != "tooltip" || state.focusedID.Path != "/button" { t.Fatalf("tooltip presentation = %+v; focus = %+v", request, state.focusedID) } event.Detail1 = 0 if !handler.handleTooltip(event) || len(queue.requests) != 0 { t.Fatal("hidden tooltip was presented") } } func TestOwnedTooltipPopupReachesSpeechAndUnownedPopupDoesNot(t *testing.T) { adapter := &foregroundAdapter{fakeAdapter: fakeAdapter{events: make(chan atspi.Event, 8)}, changes: make(chan struct{}, 1)} foreground := atspi.ForegroundState{Client: xorg.Foreground{Known: true, Window: 101}, Epoch: 1} adapter.state.Store(&foreground) settings := config.DefaultSettings() settings.Reading.PresentTooltips = true live := &liveSettings{} live.Store(settings) queue := &fakeSpeechQueue{spoken: make(chan string, 8), requests: make(chan speechRequest, 8), interrupt: make(chan struct{}, 8)} ctx, cancel := context.WithCancel(context.Background()) done := make(chan struct{}) go func() { defer close(done) _ = processEventsWithSettings(ctx, slog.New(slog.DiscardHandler), queue, adapter, nil, nil, 0, live) }() defer func() { cancel(); <-done }() focus := ownerEvent(atspi.FocusedStateEvent, ":1.42", "/window", "/button") focus.Source.Role, focus.Source.Name, focus.Source.Focused = "push button", "Save", true focus.Detail1, focus.Foreground = 1, foreground adapter.events <- focus expectActivationSpeech(t, queue.spoken, "Save, push button") tooltip := ownerEvent(atspi.ShowingStateEvent, ":1.42", "/popup", "/tooltip") tooltip.Source.Role, tooltip.Source.Name = "tool tip", "Save document" tooltip.Detail1, tooltip.Foreground, tooltip.ForegroundMismatch = 1, foreground, true adapter.events <- tooltip expectActivationSpeech(t, queue.spoken, "Save document, tool tip") settings.Reading.PresentTooltips = false live.Store(settings) adapter.events <- tooltip select { case got := <-queue.spoken: t.Fatalf("disabled tooltip spoke %q", got) case <-time.After(100 * time.Millisecond): } settings.Reading.PresentTooltips = true live.Store(settings) tooltip.Source.BusName = ":1.43" adapter.events <- tooltip select { case got := <-queue.spoken: t.Fatalf("unowned tooltip spoke %q", got) case <-time.After(100 * time.Millisecond): } }