Preserve terminal feedback while coalescing repaints
This commit is contained in:
1 parent
8c26aad128
commit
e222d336a5
13 files changed
+321
-39
No files matched your search
@@ -323,7 +323,7 @@ func TestProcessEventsTerminalRepaintsInDifferentRegionsBothSpeak(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessEventsDropsTrivialTerminalRepaintFragments(t *testing.T) {
|
||||
func TestProcessEventsPreservesShortTerminalRepaints(t *testing.T) {
|
||||
speech := &fakeSpeechQueue{spoken: make(chan string, 4)}
|
||||
adapter := &fakeAdapter{events: make(chan atspi.Event, 8)}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -338,11 +338,16 @@ func TestProcessEventsDropsTrivialTerminalRepaintFragments(t *testing.T) {
|
||||
terminal.TextCaretKnown = true
|
||||
terminal.BrailleLineStart = 200
|
||||
terminal.BrailleCaret = 205
|
||||
repaintSequence(adapter, terminal, 100, "40")
|
||||
select {
|
||||
case text := <-speech.spoken:
|
||||
t.Fatalf("trivial repaint fragment was presented as %q", text)
|
||||
case <-time.After(2 * terminalRepaintDebounce):
|
||||
for _, expected := range []string{"40", "OK", "猫"} {
|
||||
repaintSequence(adapter, terminal, 100, expected)
|
||||
select {
|
||||
case text := <-speech.spoken:
|
||||
if text != expected {
|
||||
t.Fatalf("short repaint = %q, want %q", text, expected)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatalf("short repaint %q was lost", expected)
|
||||
}
|
||||
}
|
||||
repaintSequence(adapter, terminal, 100, "Thinking (4s)")
|
||||
select {
|
||||
@@ -576,3 +581,106 @@ func TestProcessEventsPresentsTerminalFocusWithoutFollowingInsertion(t *testing.
|
||||
t.Fatal("event loop did not stop")
|
||||
}
|
||||
}
|
||||
|
||||
// Run the real event handler synchronously so coalescing assertions do not
|
||||
// depend on a scheduler processing an entire burst before a timer fires.
|
||||
func terminalHandlerFixture(t *testing.T) (*accessibilityEventHandler, *[]string) {
|
||||
t.Helper()
|
||||
state := &eventLoopState{}
|
||||
t.Cleanup(state.pending.clear)
|
||||
spoken := []string{}
|
||||
handler := &accessibilityEventHandler{
|
||||
state: state, logger: slog.New(slog.DiscardHandler), adapter: &fakeAdapter{},
|
||||
syncForeground: func() {}, webScripts: &webScriptController{},
|
||||
speak: func(event atspi.Event, _ ...input.Intent) { spoken = append(spoken, event.Payload) },
|
||||
}
|
||||
handler.flushTerminalDeletion = func() {
|
||||
event, _ := state.pending.terminalDeletion.take()
|
||||
handler.speak(event)
|
||||
}
|
||||
return handler, &spoken
|
||||
}
|
||||
|
||||
func TestTerminalRepaintsKeepTerminalIdentityAndFirstDeadline(t *testing.T) {
|
||||
for _, differentTerminal := range []bool{false, true} {
|
||||
t.Run(map[bool]string{false: "same terminal", true: "different bus"}[differentTerminal], func(t *testing.T) {
|
||||
handler, spoken := terminalHandlerFixture(t)
|
||||
terminal := atspi.Snapshot{BusName: ":1.10", Path: "/terminal", Role: "terminal"}
|
||||
repaint := func(text string) {
|
||||
deletion := atspi.NewEventWithPayload(atspi.TextDeletedEvent, terminal, "Old status")
|
||||
insertion := atspi.NewEventWithPayload(atspi.TextInsertedEvent, terminal, text)
|
||||
handler.handle(deletion)
|
||||
handler.handle(insertion)
|
||||
}
|
||||
repaint("First status")
|
||||
firstTimer := handler.state.pending.terminalRepaint.timer
|
||||
if differentTerminal {
|
||||
terminal.BusName = ":1.11"
|
||||
}
|
||||
repaint("Second status")
|
||||
pending := &handler.state.pending.terminalRepaint
|
||||
if differentTerminal {
|
||||
if len(*spoken) != 1 || (*spoken)[0] != "First status" {
|
||||
t.Fatalf("different terminal discarded earlier repaint: %v", *spoken)
|
||||
}
|
||||
} else if pending.timer != firstTimer {
|
||||
t.Fatal("continuous repaint postponed the first deadline")
|
||||
}
|
||||
event, _ := pending.take()
|
||||
if event.Payload != "Second status" {
|
||||
t.Fatalf("latest repaint = %q", event.Payload)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTerminalSeparateDeletionsPreserveEarlierFeedback(t *testing.T) {
|
||||
handler, spoken := terminalHandlerFixture(t)
|
||||
terminal := atspi.Snapshot{BusName: ":1.10", Path: "/terminal", Role: "terminal"}
|
||||
first := atspi.NewEventWithPayload(atspi.TextDeletedEvent, terminal, "First deletion")
|
||||
first.Detail1 = 100
|
||||
second := atspi.NewEventWithPayload(atspi.TextDeletedEvent, terminal, "Second deletion")
|
||||
second.Detail1 = 300
|
||||
handler.handle(first)
|
||||
handler.handle(second)
|
||||
if len(*spoken) != 1 || (*spoken)[0] != first.Payload {
|
||||
t.Fatalf("unrelated deletion discarded earlier feedback: %v", *spoken)
|
||||
}
|
||||
if handler.state.pending.terminalDeletion.event.Payload != second.Payload {
|
||||
t.Fatal("later deletion was lost")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTerminalHeldRepaintPreservesOrderAndRetiresOnFocusChange(t *testing.T) {
|
||||
for _, next := range []string{"append", "deletion", "focus"} {
|
||||
t.Run(next, func(t *testing.T) {
|
||||
handler, spoken := terminalHandlerFixture(t)
|
||||
terminal := atspi.Snapshot{BusName: ":1.10", Path: "/terminal", Role: "terminal"}
|
||||
deletion := atspi.NewEventWithPayload(atspi.TextDeletedEvent, terminal, "Old status")
|
||||
insertion := atspi.NewEventWithPayload(atspi.TextInsertedEvent, terminal, "Latest status")
|
||||
handler.handle(deletion)
|
||||
handler.handle(insertion)
|
||||
event := atspi.NewEventWithPayload(atspi.TextInsertedEvent, terminal, "Appended output")
|
||||
event.Detail1 = 300
|
||||
switch next {
|
||||
case "deletion":
|
||||
event.Type, event.Payload = atspi.TextDeletedEvent, "Later deletion"
|
||||
case "focus":
|
||||
event = atspi.NewEvent(atspi.FocusEvent, atspi.Snapshot{BusName: ":1.10", Path: "/entry", Role: "text", Focused: true})
|
||||
}
|
||||
handler.handle(event)
|
||||
if handler.state.pending.terminalRepaint.event.Type != "" {
|
||||
t.Fatal("old repaint remains pending")
|
||||
}
|
||||
if next == "focus" {
|
||||
for _, text := range *spoken {
|
||||
if text == "Latest status" {
|
||||
t.Fatal("old terminal spoke after focus moved to another control")
|
||||
}
|
||||
}
|
||||
} else if len(*spoken) == 0 || (*spoken)[0] != "Latest status" {
|
||||
t.Fatalf("later output overtook pending repaint: %v", *spoken)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user