Found more weird breakage on Slint. Fixed now.
This commit is contained in:
1 parent
6df27c88de
commit
7918fc02c4
14 files changed
+502
-6
No files matched your search
@@ -0,0 +1,149 @@
|
||||
// Cthulhu screen reader.
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package atspi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNativeTobyComboPopupLifetime(t *testing.T) {
|
||||
if os.Getenv("CTHULHU_NATIVE_TOBY_COMBO") != "1" {
|
||||
t.Skip("requires isolated Toby combo fixture")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
adapter := New()
|
||||
if err := adapter.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
stopCtx, stopCancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer stopCancel()
|
||||
if err := adapter.Stop(stopCtx); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
if _, err := adapter.CaptureDesktop(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key := func(keys ...string) {
|
||||
t.Helper()
|
||||
arguments := append([]string{"key", "--delay", "120"}, keys...)
|
||||
if err := exec.CommandContext(ctx, "xdotool", arguments...).Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
var combo ObjectID
|
||||
var comboCarets int
|
||||
var slowest time.Duration
|
||||
wait := func(description string, match func(Event) bool) Event {
|
||||
t.Helper()
|
||||
deadline := time.NewTimer(4 * time.Second)
|
||||
defer deadline.Stop()
|
||||
var recent []string
|
||||
for {
|
||||
select {
|
||||
case event := <-adapter.Events():
|
||||
if event.Source.ApplicationName != "toby_combo_fixture.py" {
|
||||
continue
|
||||
}
|
||||
recent = append(recent, event.Type+" "+event.Source.Role+" "+event.Source.Name)
|
||||
if len(recent) > 12 {
|
||||
recent = recent[1:]
|
||||
}
|
||||
slowest = max(slowest, event.SnapshotDuration)
|
||||
if event.Type == CaretEvent && event.Source.ReadOnlyCombo.Valid() {
|
||||
comboCarets++
|
||||
if event.Source.ReadOnlyCombo != combo || event.Source.CaretWord != "" {
|
||||
t.Fatalf("read-only combo caret: owner=%v word=%q", event.Source.ReadOnlyCombo, event.Source.CaretWord)
|
||||
}
|
||||
}
|
||||
if match(event) {
|
||||
return event
|
||||
}
|
||||
case <-deadline.C:
|
||||
t.Fatalf("missing %s; recent events: %q; background filtered=%d", description, recent, adapter.backgroundEventsFiltered.Load())
|
||||
case <-ctx.Done():
|
||||
t.Fatal(ctx.Err())
|
||||
}
|
||||
}
|
||||
}
|
||||
focus := func(role, name string) Event {
|
||||
t.Helper()
|
||||
return wait("focus on "+role+" "+name, func(event Event) bool {
|
||||
return event.Type == FocusedStateEvent && event.Detail1 == 1 &&
|
||||
event.Source.Role == role && event.Source.Name == name
|
||||
})
|
||||
}
|
||||
name := func(value string) Event {
|
||||
t.Helper()
|
||||
return wait("combo value "+value, func(event Event) bool {
|
||||
return event.Type == AccessibleNameEvent && event.Source.Role == "combo box" && event.Source.Name == value
|
||||
})
|
||||
}
|
||||
// Closed arrows produce consecutive caret events without another combo
|
||||
// focus event between them. This was missing from the popup-only probe.
|
||||
key("Tab", "shift+Tab")
|
||||
combo = focus("combo box", "Classic Doom").Source.ID()
|
||||
for _, step := range []struct{ key, value string }{
|
||||
{"Down", "Bring Back the 90s"}, {"Down", "Funny Pack - Toby Doom"},
|
||||
{"Up", "Bring Back the 90s"}, {"Up", "Classic Doom"},
|
||||
{"End", "Funny Pack - Toby Doom"}, {"Home", "Classic Doom"},
|
||||
} {
|
||||
key(step.key)
|
||||
name(step.value)
|
||||
}
|
||||
if comboCarets < 4 {
|
||||
t.Fatalf("only %d read-only combo caret events observed", comboCarets)
|
||||
}
|
||||
for _, closeKey := range []string{"Return", "Escape"} {
|
||||
key("alt+Down")
|
||||
focus("list", "")
|
||||
key("Down")
|
||||
wait("popup selection", func(event Event) bool {
|
||||
return event.Type == SelectionChangedEvent && event.Source.Role == "list item" && event.Source.Selected
|
||||
})
|
||||
key(closeKey)
|
||||
wait("combo focus after "+closeKey, func(event Event) bool {
|
||||
return event.Type == FocusedStateEvent && event.Detail1 == 1 && event.Source.ID() == combo
|
||||
})
|
||||
key("Tab")
|
||||
focus("button", "Launch")
|
||||
key("shift+Tab")
|
||||
wait("return to combo", func(event Event) bool {
|
||||
return event.Type == FocusedStateEvent && event.Detail1 == 1 && event.Source.ID() == combo
|
||||
})
|
||||
}
|
||||
key("Tab")
|
||||
focus("button", "Launch")
|
||||
// Other text controls still need native word capture, including read-only
|
||||
// fields outside combos and writable combo editors in this same provider.
|
||||
for _, control := range []struct{ role, name string }{
|
||||
{"text", "Plain editor"}, {"text", "Read-only text"}, {"combo box", "alpha beta"},
|
||||
} {
|
||||
key("Tab")
|
||||
// Qt can expose entry into a line edit only through a text-selection
|
||||
// event, without refreshing libatspi's focused state or emitting focus.
|
||||
wait("entry into "+control.name, func(event Event) bool {
|
||||
return event.Source.Role == control.role && event.Source.Name == control.name
|
||||
})
|
||||
key("Home", "ctrl+Right")
|
||||
caret := wait("word caret in "+control.name, func(event Event) bool {
|
||||
return event.Type == CaretEvent && event.Source.CaretWord == "beta"
|
||||
})
|
||||
if caret.Source.ReadOnlyCombo.Valid() {
|
||||
t.Fatalf("ordinary text caret acquired combo selection owner: %+v", caret.Source)
|
||||
}
|
||||
}
|
||||
if dropped := adapter.Overflow(); dropped != 0 {
|
||||
t.Fatalf("native event queue dropped %d events", dropped)
|
||||
}
|
||||
t.Logf("closed arrows, Home/End, popup Return/Escape and text navigation completed; slowest snapshot %s", slowest)
|
||||
}
|
||||
Reference in new issue
Block a user