Files
cthulhu/internal/app/gui_table_test.go

96 lines
3.9 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"
)
func TestGUITableNormalizedFocusRetainsOwnerAndTitle(t *testing.T) {
speech := &fakeSpeechQueue{spoken: make(chan string, 16), requests: make(chan speechRequest, 16)}
adapter := &fakeAdapter{events: make(chan atspi.Event, 16)}
inputs := &sequencedInputActivity{}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
completed := make(chan struct{})
go func() {
processEventsWithInput(ctx, slog.New(slog.DiscardHandler), speech, adapter,
nil, nil, 0, nil, inputs, nil, nil)
close(completed)
}()
window := atspi.NewObjectID(":1.22", "/preferences")
cell_event := func(path, name, rowText string, row, column int, eventType string) atspi.Event {
cell := atspi.NewSnapshot(path, name, "table cell", "", "")
cell.BusName = window.BusName
cell.TablePath, cell.TableName = "/preferences/reading", "Reading settings"
cell.TableRowIndex, cell.TableColumnIndex = row, column
cell.TablePositionKnown, cell.GUITable = true, true
cell.TableRowText = rowText
event := atspi.NewManagedDescendantEvent(eventType, cell.TablePath, cell)
event.Window = window
if eventType == atspi.FocusedStateEvent {
event.Detail1 = 1
}
return event
}
expect_speech := func(event atspi.Event, sequence uint64, want string) {
t.Helper()
inputs.set(sequence)
adapter.events <- event
select {
case request := <-speech.requests:
if request.Text != want {
t.Fatalf("%s: got %q, want %q", event.Type, request.Text, want)
}
case <-time.After(time.Second):
t.Fatalf("%s produced no speech", event.Type)
}
}
// The adapter resolves the table's initial focus to a selected cell. GTK
// need not send another focused-state event as Up/Down changes rows.
first := cell_event("/reading/0/state", "unchecked", "unchecked, Only speak displayed text", 0, 0, atspi.FocusedStateEvent)
expect_speech(first, 1, "Reading settings, unchecked, Only speak displayed text")
second := cell_event("/reading/1/state", "checked", "checked, Break speech into chunks between pauses", 1, 0, atspi.SelectionChangedEvent)
expect_speech(second, 2, "checked, Break speech into chunks between pauses")
duplicate := second
duplicate.Type = atspi.ActiveDescendantEvent
adapter.events <- duplicate
right := cell_event("/reading/1/label", "Break speech into chunks between pauses", second.Source.TableRowText, 1, 1, atspi.ActiveDescendantEvent)
expect_speech(right, 3, "Break speech into chunks between pauses")
left := second
left.Type = atspi.ActiveDescendantEvent
expect_speech(left, 4, "checked")
up := first
up.Type = atspi.SelectionChangedEvent
expect_speech(up, 5, "unchecked, Only speak displayed text")
// Background tables and another bus cannot inherit the focused table.
other := second
other.OriginPath, other.Source.TablePath = "/unrelated", "/unrelated"
other.Source.Path = "/unrelated/cell"
adapter.events <- other
other.OriginPath, other.Source.TablePath = first.OriginPath, first.Source.TablePath
other.Source.BusName = ":1.99"
adapter.events <- other
expect_speech(second, 6, "checked, Break speech into chunks between pauses")
button := atspi.NewSnapshot("/preferences/apply", "Apply", "button", "", "")
button.BusName = window.BusName
focus := atspi.NewEvent(atspi.FocusEvent, button)
focus.Window = window
expect_speech(focus, 7, "Apply, button")
adapter.events <- up // A table selection after leaving the table is noise.
// Returning through a cell focus event establishes ownership and speaks
// the table name again, without changing the user's row-reading option.
first.OriginPath = first.Source.Path
expect_speech(first, 8, "Reading settings, unchecked, Only speak displayed text")
expect_speech(second, 9, "checked, Break speech into chunks between pauses")
cancel()
<-completed
}