Sync with testing.
This commit is contained in:
commit
f2a35a9305
14 files changed
+502
-6
No files matched your search
@@ -100,6 +100,7 @@ func (handler *accessibilityEventHandler) handle(event atspi.Event) {
|
||||
"snapshot_duration", event.SnapshotDuration,
|
||||
"editable", event.Source.Editable,
|
||||
"editable_root", event.Source.EditableRoot.Path,
|
||||
"read_only_combo", event.Source.ReadOnlyCombo.Path,
|
||||
"autocomplete", event.Source.Autocomplete,
|
||||
"autocomplete_popup", event.Source.AutocompletePopup,
|
||||
"event_detail1", event.Detail1,
|
||||
|
||||
@@ -21,6 +21,13 @@ func (handler *accessibilityEventHandler) handleCaret(event atspi.Event, activit
|
||||
objectReview := handler.objectReview
|
||||
debounce := handler.debounce
|
||||
if event.Type == atspi.CaretEvent {
|
||||
if event.Source.ReadOnlyCombo.Valid() && event.Source.ReadOnlyCombo == state.focusedID &&
|
||||
state.current.ID() == state.focusedID && state.current.Role == "combo box" {
|
||||
// The combo's name/value event presents this selection, even when
|
||||
// provider requests delay it past the normal caret coalescing time.
|
||||
logger.Debug("skipped read-only combo editor caret", "combo_path", event.Source.ReadOnlyCombo.Path)
|
||||
return true
|
||||
}
|
||||
if event.Source.ID() == state.selectionCaretID {
|
||||
state.selectionCaretID = atspi.ObjectID{}
|
||||
if activity.Sequence == state.selectionCaretSequence && time.Since(state.selectionCaretAt) < 150*time.Millisecond {
|
||||
|
||||
@@ -20,10 +20,12 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/input"
|
||||
)
|
||||
|
||||
func TestProcessEventsSpeaksChangedCaretLines(t *testing.T) {
|
||||
@@ -101,6 +103,59 @@ func TestProcessEventsSuppressesCaretAfterEditableTextChange(t *testing.T) {
|
||||
<-completed
|
||||
}
|
||||
|
||||
func TestProcessEventsReadOnlyComboUsesNameInsteadOfEditorCaret(t *testing.T) {
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 16)}
|
||||
adapter := &fakeAdapter{events: make(chan atspi.Event, 16)}
|
||||
combo := atspi.Snapshot{BusName: ":1.7", Path: "/combo", Role: "combo box", Name: "Classic Doom", Focused: true}
|
||||
adapter.events <- atspi.NewEvent(atspi.FocusEvent, combo)
|
||||
editor := atspi.Snapshot{BusName: combo.BusName, Path: "/combo/editor", Role: "text", Text: "Funny Pack - Classic Doom",
|
||||
BrailleLine: "Funny Pack - Classic Doom", TextCaretKnown: true, Editable: true, ReadOnly: true,
|
||||
Focused: true, ReadOnlyCombo: combo.ID()}
|
||||
for _, value := range []string{"Funny Pack - Classic Doom", "Toby Doom", "Classic Doom"} {
|
||||
editor.Text, editor.BrailleLine = value, value
|
||||
adapter.events <- atspi.NewEvent(atspi.CaretEvent, editor)
|
||||
combo.Name = value
|
||||
adapter.events <- atspi.NewEvent(atspi.AccessibleNameEvent, combo)
|
||||
}
|
||||
close(adapter.events)
|
||||
processEvents(context.Background(), slog.New(slog.DiscardHandler), speech, adapter, 0)
|
||||
close(speech.spoken)
|
||||
var got []string
|
||||
for text := range speech.spoken {
|
||||
got = append(got, text)
|
||||
}
|
||||
want := []string{"Classic Doom, combo box", "Funny Pack - Classic Doom", "Toby Doom", "Classic Doom"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("combo speech = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadOnlyComboCaretRequiresExactOwnedCombo(t *testing.T) {
|
||||
combo := atspi.Snapshot{BusName: ":1.7", Path: "/combo", Role: "combo box"}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
owner atspi.ObjectID
|
||||
skip bool
|
||||
}{
|
||||
{"owned", combo.ID(), true},
|
||||
{"different combo", atspi.NewObjectID(combo.BusName, "/other-combo"), false},
|
||||
{"different provider", atspi.NewObjectID(":1.8", combo.Path), false},
|
||||
{"ordinary editor", atspi.ObjectID{}, false},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
state := &eventLoopState{}
|
||||
state.current, state.focusedID = combo, combo.ID()
|
||||
handler := accessibilityEventHandler{state: state, logger: slog.New(slog.DiscardHandler), debounce: time.Hour}
|
||||
event := atspi.NewEvent(atspi.CaretEvent, atspi.Snapshot{BusName: combo.BusName, Path: "/editor", Role: "text", ReadOnlyCombo: test.owner})
|
||||
handler.handleCaret(event, input.Activity{}, input.NoIntent)
|
||||
defer state.pending.clear()
|
||||
if skipped := state.pending.caret.event.Type == ""; skipped != test.skip {
|
||||
t.Fatalf("caret skipped = %v, want %v", skipped, test.skip)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessEventsSpeaksCaretCharacters(t *testing.T) {
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 3)}
|
||||
adapter := &fakeAdapter{events: make(chan atspi.Event, 3)}
|
||||
|
||||
@@ -33,11 +33,13 @@ func (handler *accessibilityEventHandler) acceptFocusedEvent(event atspi.Event,
|
||||
// Compare with the owned focus snapshot, not the last speech request:
|
||||
// a state/value update can intervene between two toolkit focus events.
|
||||
// Still refresh review and braille below before suppressing presentation.
|
||||
managedSelectionFocus := isFocusGainEvent(event) && state.managedDescendantID.Valid() &&
|
||||
event.Source.ID() == state.managedDescendantID && sameSelectionItem(event.Source, state.current)
|
||||
duplicateFocus := (event.Type == atspi.FocusEvent || event.Type == atspi.FocusedStateEvent && event.Detail1 != 0) &&
|
||||
event.Source.Role != "terminal" && !presentsReusedRow(event.Source) &&
|
||||
!(state.pendingPromptWindow.Valid() && event.Window == state.pendingPromptWindow) &&
|
||||
state.focusedID.Valid() && event.Source.ID() == state.focusedID &&
|
||||
event.Source.ID() == state.current.ID() &&
|
||||
((state.focusedID.Valid() && event.Source.ID() == state.focusedID &&
|
||||
event.Source.ID() == state.current.ID()) || managedSelectionFocus) &&
|
||||
presentation.FocusTextWithOptions(event.Source, state.presentationOptions) == presentation.FocusTextWithOptions(state.current, state.presentationOptions) &&
|
||||
slices.Equal(presentation.SoundCues(event.Source, state.presentationOptions), presentation.SoundCues(state.current, state.presentationOptions))
|
||||
if isFocusEvent(event.Type) {
|
||||
@@ -93,6 +95,11 @@ func (handler *accessibilityEventHandler) acceptFocusedEvent(event atspi.Event,
|
||||
logger.Debug("skipped selection already presented by focus", "role", event.Source.Role)
|
||||
return true
|
||||
}
|
||||
} else if event.Type == atspi.SelectedStateEvent && !event.Source.Selected &&
|
||||
(event.Source.Role == "tree item" || event.Source.Role == "list item") &&
|
||||
state.managedDescendantID.Valid() && event.Source.ID() != state.managedDescendantID {
|
||||
logger.Debug("skipped deselected previous managed descendant", "role", event.Source.Role)
|
||||
return true
|
||||
} else if (isStateChangeEvent(event.Type) || isValueChangeEvent(event.Type)) &&
|
||||
(state.focusedID.Path == "" || event.Source.ID() != state.focusedID) {
|
||||
logger.Debug("skipped event outside focused object", "type", event.Type, "role", event.Source.Role)
|
||||
|
||||
@@ -67,6 +67,52 @@ func TestProcessEventsSuppressesDuplicateFocusObjects(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessEventsSuppressesQtPopupSelectionRepeats(t *testing.T) {
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 8)}
|
||||
adapter := &fakeAdapter{events: make(chan atspi.Event, 8)}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
processEvents(ctx, slog.New(slog.DiscardHandler), speech, adapter, 0)
|
||||
close(done)
|
||||
}()
|
||||
defer func() { cancel(); <-done }()
|
||||
|
||||
list := atspi.Snapshot{BusName: ":1.7", Path: "/combo/list", Role: "list"}
|
||||
adapter.events <- atspi.NewEvent(atspi.FocusEvent, list)
|
||||
if got := <-speech.spoken; got != "list" {
|
||||
t.Fatalf("list focus speech = %q", got)
|
||||
}
|
||||
item := func(path, name string) atspi.Snapshot {
|
||||
return atspi.Snapshot{BusName: list.BusName, Path: path, Role: "list item", Name: name, Focused: true, Selected: true}
|
||||
}
|
||||
first := item("/combo/list/1", "Classic Doom")
|
||||
adapter.events <- atspi.NewManagedDescendantEvent(atspi.SelectionChangedEvent, list.Path, first)
|
||||
if got := <-speech.spoken; got != "Classic Doom, list item, selected" {
|
||||
t.Fatalf("first selection speech = %q", got)
|
||||
}
|
||||
firstFocus := atspi.NewEvent(atspi.FocusedStateEvent, first)
|
||||
firstFocus.Detail1 = 1
|
||||
adapter.events <- firstFocus
|
||||
|
||||
second := item("/combo/list/2", "Funny Pack - Toby Doom")
|
||||
adapter.events <- atspi.NewManagedDescendantEvent(atspi.SelectionChangedEvent, list.Path, second)
|
||||
if got := <-speech.spoken; got != "Funny Pack - Toby Doom, list item, selected" {
|
||||
t.Fatalf("second selection speech = %q", got)
|
||||
}
|
||||
first.Focused, first.Selected = false, false
|
||||
adapter.events <- atspi.NewEvent(atspi.SelectedStateEvent, first)
|
||||
secondFocus := atspi.NewEvent(atspi.FocusedStateEvent, second)
|
||||
secondFocus.Detail1 = 1
|
||||
adapter.events <- secondFocus
|
||||
|
||||
select {
|
||||
case got := <-speech.spoken:
|
||||
t.Fatalf("Qt popup selection repeated as %q", got)
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessEventsSuppressesUnfocusedNameChangesDuringFileNavigation(t *testing.T) {
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 8)}
|
||||
adapter := &fakeAdapter{events: make(chan atspi.Event, 8)}
|
||||
|
||||
Reference in new issue
Block a user