Fixed a bug with some edit field types that would cause every event to read the whole field.
This commit is contained in:
1 parent
736f5a0f68
commit
03e2b31e09
11 files changed
+282
-21
No files matched your search
@@ -4,7 +4,7 @@
|
||||
pkgbase=cthulhu-git
|
||||
pkgname=cthulhu-git
|
||||
_pkgname=cthulhu
|
||||
pkgver=2026.10.22.r70.gd7b1efe
|
||||
pkgver=2026.10.23.r72.g736f5a0
|
||||
pkgrel=1
|
||||
pkgdesc="Desktop-agnostic screen reader with speech, Braille, and Tentacle support"
|
||||
url="https://git.stormux.org/storm/cthulhu"
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Browser editor typing
|
||||
|
||||
Chromium contenteditable textboxes can keep native focus on an `entry` while
|
||||
editable child paragraphs emit insertion, deletion, caret and selection events.
|
||||
The native adapter records the nearest focusable writable editor as
|
||||
`EditableRoot`, just as it records an editable document for document editors.
|
||||
The event loop accepts that relationship only while that exact editor owns focus.
|
||||
Lookup stops at document, window, password and read-only boundaries.
|
||||
|
||||
The reported ChatGPT failure was captured in the debug log on 2026-10-23:
|
||||
paragraph text changes were rejected as outside the focused editor because
|
||||
`editable_root` was empty. Following caret events then spoke the growing line.
|
||||
Recognizing the parent editor restores configured typing echo and the existing
|
||||
suppression of caret speech following a text change. This uses accessibility
|
||||
identity and states; it does not depend on a website, label or DOM class.
|
||||
|
||||
## Automated checks
|
||||
|
||||
```sh
|
||||
env CTHULHU_NATIVE_BROWSER_TESTS=editor python3 tests/native_browser.py
|
||||
env CTHULHU_NATIVE_BROWSER_TESTS=editing,selection,live python3 tests/native_browser.py
|
||||
python3 tests/native_test_session.py go test ./internal/app ./internal/atspi ./internal/presentation -count=1
|
||||
```
|
||||
|
||||
The editor proof uses real Chromium paragraph events and the reader's event
|
||||
pipeline on a private Xvfb display and D-Bus session. It checks quiet typing with
|
||||
echo disabled, parent identity, character and line reading, deletion feedback,
|
||||
and movement into another editor. Unit coverage checks disabled, character,
|
||||
word and sentence echo, delayed events after focus leaves, existing editable
|
||||
document handling, and Braille ownership.
|
||||
|
||||
## Live acceptance
|
||||
|
||||
After rebuilding and restarting the logging reader through its usual launcher:
|
||||
|
||||
1. In ChatGPT, compose several words, pause, and continue typing. With typing
|
||||
echo disabled, each key must not reread the draft. With character or word
|
||||
echo enabled, hear only the configured echo.
|
||||
2. Use Left/Right, Control+Left/Right and Up/Down in a multiline draft. Confirm
|
||||
character, word and line reading, Backspace/Delete feedback, and Shift-arrow
|
||||
selection. Check Braille text and cursor placement if available.
|
||||
3. Tab and Shift+Tab out of and back into the composer. Confirm focus labels
|
||||
and that typing in another field cannot announce the old draft.
|
||||
4. Check browser address-bar typing and suggestions, ordinary form fields,
|
||||
password fields, and live status/response announcements. Check an editable
|
||||
document body as well as its separate header fields when available.
|
||||
|
||||
Private test results do not establish audible speech or physical Braille
|
||||
acceptance on the original ChatGPT page. That acceptance remains pending.
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
|
||||
"git.stormux.org/storm/cthulhu/internal/presentation"
|
||||
)
|
||||
|
||||
func TestNativeBrowserEditorTyping(t *testing.T) {
|
||||
if os.Getenv("CTHULHU_NATIVE_BROWSER") != "1" {
|
||||
t.Skip("requires private browser session")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
|
||||
defer cancel()
|
||||
adapter := atspi.New()
|
||||
if err := adapter.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer adapter.Stop(context.Background())
|
||||
listener := xorg.New()
|
||||
if err := listener.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Stop(context.Background())
|
||||
var focus atomic.Pointer[atspi.Snapshot]
|
||||
var insertion atomic.Pointer[atspi.Event]
|
||||
observed := &browserEventProbe{Adapter: adapter, events: make(chan atspi.Event, 64)}
|
||||
relayDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(relayDone)
|
||||
for {
|
||||
select {
|
||||
case event := <-adapter.Events():
|
||||
if isFocusGainEvent(event) {
|
||||
focus.Store(&event.Source)
|
||||
}
|
||||
if event.Type == atspi.TextInsertedEvent && event.Source.Role == "paragraph" {
|
||||
insertion.Store(&event)
|
||||
}
|
||||
select {
|
||||
case observed.events <- event:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
defer func() { cancel(); <-relayDone }()
|
||||
queue := &fakeSpeechQueue{spoken: make(chan string, 128)}
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
options := presentation.DefaultOptions()
|
||||
options.EnableKeyEcho = false
|
||||
processEventsWithInput(ctx, slog.New(slog.DiscardHandler), queue, observed, nil, nil,
|
||||
20*time.Millisecond, nil, listener, nil, nil, options)
|
||||
}()
|
||||
defer func() { cancel(); <-done }()
|
||||
keys := func(chord string) {
|
||||
t.Helper()
|
||||
if out, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", chord).CombinedOutput(); err != nil {
|
||||
t.Fatalf("keys: %v %s", err, out)
|
||||
}
|
||||
}
|
||||
collect := func() []string {
|
||||
t.Helper()
|
||||
timer := time.NewTimer(180 * time.Millisecond)
|
||||
defer timer.Stop()
|
||||
var texts []string
|
||||
for {
|
||||
select {
|
||||
case text := <-queue.spoken:
|
||||
texts = append(texts, text)
|
||||
case <-timer.C:
|
||||
return texts
|
||||
case <-ctx.Done():
|
||||
t.Fatal(ctx.Err())
|
||||
}
|
||||
}
|
||||
}
|
||||
for attempt := 0; attempt < 20; attempt++ {
|
||||
keys("Tab")
|
||||
collect()
|
||||
if current := focus.Load(); current != nil && current.Name == "Message composer" {
|
||||
break
|
||||
}
|
||||
}
|
||||
editor := focus.Load()
|
||||
if editor == nil || editor.Name != "Message composer" || !editor.Editable || editor.Role != "entry" {
|
||||
t.Fatalf("composer focus = %+v", editor)
|
||||
}
|
||||
keys("End")
|
||||
collect()
|
||||
for _, letter := range []string{"h", "e", "l", "l", "o"} {
|
||||
keys(letter)
|
||||
if got := collect(); len(got) != 0 {
|
||||
t.Fatalf("typing %q with echo disabled spoke %q", letter, got)
|
||||
}
|
||||
}
|
||||
if event := insertion.Load(); event == nil || event.Source.EditableRoot != editor.ID() {
|
||||
t.Fatalf("paragraph insertion lost editor ownership: %+v", event)
|
||||
}
|
||||
keys("Left")
|
||||
if got := collect(); len(got) != 1 || got[0] != "o" {
|
||||
t.Fatalf("character reading = %q", got)
|
||||
}
|
||||
keys("Down")
|
||||
if got := collect(); len(got) != 1 || got[0] != "Following line" {
|
||||
t.Fatalf("next line reading = %q", got)
|
||||
}
|
||||
keys("Up")
|
||||
if got := collect(); len(got) != 1 || got[0] != "Draft:hello" {
|
||||
t.Fatalf("line reading = %q", got)
|
||||
}
|
||||
keys("End")
|
||||
collect()
|
||||
keys("BackSpace")
|
||||
if got := collect(); len(got) != 1 || got[0] != "o" {
|
||||
t.Fatalf("deletion = %q", got)
|
||||
}
|
||||
keys("Tab")
|
||||
if got := collect(); len(got) == 0 || !strings.Contains(strings.Join(got, " "), "Other editor") {
|
||||
t.Fatalf("next editor focus = %q", got)
|
||||
}
|
||||
keys("x")
|
||||
if got := collect(); len(got) != 0 {
|
||||
t.Fatalf("typing in second editor spoke %q", got)
|
||||
}
|
||||
if current, event := focus.Load(), insertion.Load(); current == nil || current.Name != "Other editor" ||
|
||||
event == nil || event.Source.EditableRoot != current.ID() || event.Source.EditableRoot == editor.ID() {
|
||||
t.Fatalf("second editor ownership: focus=%+v insertion=%+v", current, event)
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,65 @@ import (
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/braille"
|
||||
"git.stormux.org/storm/cthulhu/internal/presentation"
|
||||
)
|
||||
|
||||
func TestEditorParagraphUsesConfiguredEcho(t *testing.T) {
|
||||
for _, echo := range []string{"disabled", "character", "word", "sentence"} {
|
||||
t.Run(echo, func(t *testing.T) {
|
||||
options := presentation.DefaultOptions()
|
||||
payload, contextText, want := "o", "Hello", "o"
|
||||
switch echo {
|
||||
case "disabled":
|
||||
options.EnableKeyEcho, want = false, ""
|
||||
case "character":
|
||||
options.EnableEchoByCharacter = true
|
||||
case "word":
|
||||
options.EnableEchoByWord = true
|
||||
payload, contextText, want = " ", "Hello ", "Hello"
|
||||
case "sentence":
|
||||
options.EnableEchoBySentence = true
|
||||
payload, contextText, want = " ", "Hello. ", "Hello."
|
||||
}
|
||||
queue := &fakeSpeechQueue{spoken: make(chan string, 16)}
|
||||
adapter := &fakeAdapter{events: make(chan atspi.Event, 16)}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
processEvents(ctx, slog.New(slog.DiscardHandler), queue, adapter, 0, options)
|
||||
}()
|
||||
defer func() { cancel(); <-done }()
|
||||
editor := atspi.Snapshot{BusName: ":1.7", Path: "/editor", Name: "Composer", Role: "entry", Editable: true}
|
||||
paragraph := atspi.Snapshot{BusName: editor.BusName, Path: "/paragraph", Role: "paragraph", Editable: true,
|
||||
EditableRoot: editor.ID(), Text: contextText, TextBeforeCaret: contextText}
|
||||
adapter.events <- atspi.NewEvent(atspi.FocusEvent, editor)
|
||||
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, paragraph, payload)
|
||||
adapter.events <- atspi.NewEvent(atspi.CaretEvent, paragraph)
|
||||
adapter.events <- atspi.NewEvent(atspi.FocusEvent, atspi.Snapshot{BusName: editor.BusName, Path: "/next", Role: "button", Name: "Next"})
|
||||
// Once focus leaves, delayed events from that editor stay quiet.
|
||||
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, paragraph, "delayed edit")
|
||||
adapter.events <- atspi.NewEvent(atspi.CaretEvent, paragraph)
|
||||
adapter.events <- atspi.NewEvent(atspi.FocusEvent, atspi.Snapshot{BusName: editor.BusName, Path: "/end", Role: "button", Name: "End"})
|
||||
wants := []string{"Composer, entry"}
|
||||
if want != "" {
|
||||
wants = append(wants, want)
|
||||
}
|
||||
wants = append(wants, "Next, button", "End, button")
|
||||
for _, want := range wants {
|
||||
select {
|
||||
case got := <-queue.spoken:
|
||||
if got != want {
|
||||
t.Fatalf("speech = %q, want %q", got, want)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("missing %q", want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditableDocumentParagraphEvents(t *testing.T) {
|
||||
queue := &fakeSpeechQueue{spoken: make(chan string, 16)}
|
||||
adapter := &fakeAdapter{events: make(chan atspi.Event, 16)}
|
||||
|
||||
@@ -116,7 +116,7 @@ func (adapter *Adapter) captureNativeEventSnapshot(event *C.AtspiEvent, target *
|
||||
snapshot := snapshotFromNativeWithCaret(target, captureCaret, eventType == CaretEvent, false, adapter.captureSound.Load(), adapter.captureTextAttributes.Load())
|
||||
if snapshot.Editable && (eventType == CaretEvent || eventType == TextInsertedEvent ||
|
||||
eventType == TextDeletedEvent || eventType == TextSelectionChangedEvent) {
|
||||
if editor := C.cthulhu_editable_document(target); editor != nil {
|
||||
if editor := C.cthulhu_editable_root(target); editor != nil {
|
||||
snapshot.EditableRoot = nativeObjectID(editor)
|
||||
C.g_object_unref(C.gpointer(editor))
|
||||
}
|
||||
|
||||
@@ -581,25 +581,24 @@ AtspiAccessible *cthulhu_window_ancestor(AtspiAccessible *source) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Editable documents can retain native focus while their paragraphs emit
|
||||
// typing/caret/selection events. Stop at the nearest document boundary so an
|
||||
// unrelated outer editor cannot claim events from a nested read-only document.
|
||||
AtspiAccessible *cthulhu_editable_document(AtspiAccessible *source) {
|
||||
// Editors can retain native focus while their paragraphs emit typing/caret/
|
||||
// selection events. Use the nearest focusable writable ancestor or editable
|
||||
// document. Never cross a document, password, or read-only editor boundary.
|
||||
AtspiAccessible *cthulhu_editable_root(AtspiAccessible *source) {
|
||||
AtspiAccessible *current = source != NULL ? g_object_ref(source) : NULL;
|
||||
for (gint depth = 0; current != NULL && depth < 64; depth++) {
|
||||
AtspiRole role = atspi_accessible_get_role(current, NULL);
|
||||
if (role == ATSPI_ROLE_DOCUMENT_WEB || role == ATSPI_ROLE_DOCUMENT_FRAME) {
|
||||
AtspiStateSet *states = atspi_accessible_get_state_set(current);
|
||||
gboolean editable = states != NULL &&
|
||||
atspi_state_set_contains(states, ATSPI_STATE_EDITABLE) &&
|
||||
!atspi_state_set_contains(states, ATSPI_STATE_READ_ONLY) &&
|
||||
!atspi_state_set_contains(states, ATSPI_STATE_DEFUNCT);
|
||||
if (states != NULL) g_object_unref(states);
|
||||
if (editable) return current;
|
||||
g_object_unref(current);
|
||||
return NULL;
|
||||
}
|
||||
if (role == ATSPI_ROLE_FRAME || role == ATSPI_ROLE_WINDOW || role == ATSPI_ROLE_APPLICATION) break;
|
||||
AtspiStateSet *states = atspi_accessible_get_state_set(current);
|
||||
gboolean blocked = states == NULL || role == ATSPI_ROLE_PASSWORD_TEXT ||
|
||||
atspi_state_set_contains(states, ATSPI_STATE_READ_ONLY) ||
|
||||
atspi_state_set_contains(states, ATSPI_STATE_DEFUNCT);
|
||||
gboolean editable = !blocked && atspi_state_set_contains(states, ATSPI_STATE_EDITABLE);
|
||||
gboolean focusable = !blocked && atspi_state_set_contains(states, ATSPI_STATE_FOCUSABLE);
|
||||
if (states != NULL) g_object_unref(states);
|
||||
gboolean document = role == ATSPI_ROLE_DOCUMENT_WEB || role == ATSPI_ROLE_DOCUMENT_FRAME;
|
||||
if (editable && (focusable || document)) return current;
|
||||
if (blocked || document || focusable) break;
|
||||
AtspiAccessible *parent = atspi_accessible_get_parent(current, NULL);
|
||||
g_object_unref(current);
|
||||
current = parent;
|
||||
|
||||
@@ -131,7 +131,7 @@ const gchar *cthulhu_accessible_bus_name(AtspiAccessible *source);
|
||||
AtspiAccessible *cthulhu_window_ancestor(AtspiAccessible *source);
|
||||
gboolean cthulhu_may_be_background_notification(AtspiAccessible *source);
|
||||
AtspiAccessible *cthulhu_document_ancestor(AtspiAccessible *source);
|
||||
AtspiAccessible *cthulhu_editable_document(AtspiAccessible *source);
|
||||
AtspiAccessible *cthulhu_editable_root(AtspiAccessible *source);
|
||||
AtspiEventListenerCB cthulhu_event_callback_pointer(void);
|
||||
gboolean cthulhu_request_event_loop_quit(void);
|
||||
gboolean cthulhu_request_tree_capture(gpointer userData);
|
||||
|
||||
@@ -90,8 +90,8 @@ type Snapshot struct {
|
||||
Checked bool
|
||||
Defunct bool
|
||||
Editable bool
|
||||
// EditableRoot identifies the nearest editable web document, copied while
|
||||
// capturing native events. Paragraph events can belong to its native focus.
|
||||
// EditableRoot identifies the nearest focusable writable editor or editable
|
||||
// web document. Paragraph events can belong to its native focus.
|
||||
EditableRoot ObjectID
|
||||
Expandable bool
|
||||
Expanded bool
|
||||
|
||||
Vendored
+1
-1
@@ -14,7 +14,7 @@ background-event-filtering,Orca src/orca/event_manager.py,focus,Reject routine b
|
||||
xterm-focus-handoff,src/cthulhu/input_event_manager.py,input,Suspend Cthulhu while XTerm owns focus and restore when a known non-XTerm window takes focus,Storm Dragon,internal/app internal/atspi internal/platform/xorg,implemented,NO_COLOR=1 GOCACHE=/tmp/cthulhu-build-cache go test ./internal/app -run '^(TestXTermFocus|TestProcessEvents)' -count=1; NO_COLOR=1 GOCACHE=/tmp/cthulhu-build-cache go test ./internal/platform/xorg -count=1; python3 tests/native_x11.py,Private Xvfb/D-Bus proof passes: XTerm focus suspends speech and command grabs; Insert+Q reaches a real XTerm; an unknown active window remains suspended; a known non-XTerm window restores all existing command and release checks. Maintainer confirms constant live use of XTerm handoff and recovery with good results on 2026-10-22.,,Maintainer confirms XTerm handoff and recovery work very well during constant use on 2026-10-22. See docs/testing/release-acceptance.md.,,Follow-up gates delegated modifier interruption and echo; commands dispatch on press with balanced shutdown draining. Go retains the existing XTerm identity matching focus monitoring grab suspension and event suppression boundary. Notifications bypass suppression. Unknown active windows remain suspended so an incomplete match cannot strand a delegated terminal reader; live XTerm handoff and recovery are accepted by the maintainer on 2026-10-22.
|
||||
focus-speech,src/cthulhu/scripts/default.py,speech,Speak the focused accessible object including common control states and range values,Storm Dragon,internal/atspi internal/presentation internal/app,implemented,go test ./internal/atspi ./internal/presentation ./internal/app -count=1,Maintainer accepted post-change YAD value and LibreOffice focus value and menu behavior on physical Xorg on 2026-09-12. The combined log had no accessibility-event speech or sound queue overflow; 599 debug diagnostics were dropped during a LibreOffice state-event burst.,,Maintainer confirms thorough live testing of native application and terminal workflows; typing echo; selection; clipboard feedback; and object navigation on 2026-10-22. These workflows pass. See docs/testing/release-acceptance.md.,,Focus presentation includes common states and range values. Focused value changes speak only the value; non-focused state and value events are filtered; menu selection and table-cell verbosity are reduced. Relations position context background progress updates complete role-specific formatting and diagnostics saturation acceptance remain pending. Parity review corrections and remaining physical gates: docs/testing/parity-review-follow-up.md. September 25 review: window-owned title tracking suppresses repeated name events and preserves empty-to-restored title changes; event-loop regression passes. Combo selection follow-up: focus/name/value event regressions verify selection-only speech without repeated combo cues and restore the cue on re-entry; focused app/presentation race checks pass. Current general native focus listening is accepted by the maintainer on 2026-10-22.
|
||||
terminal-caret-character,src/cthulhu/scripts/default.py,terminal,Speak the character at the caret,Storm Dragon,internal/app,implemented,go test ./internal/atspi ./internal/presentation ./internal/app -count=1,Maintainer accepted terminal caret behavior in the launcher's LXTerminal test application on Xorg during the Stage 1 physical checklist on 2026-09-11.,,Maintainer confirms thorough live testing of native application and terminal workflows; typing echo; selection; clipboard feedback; and object navigation on 2026-10-22. These workflows pass. See docs/testing/release-acceptance.md.,,Uses AT-SPI character granularity with line fallback debounces caret movement and suppresses unchanged path-and-announcement combinations; the maintainer confirms thorough native and terminal live testing on 2026-10-22.
|
||||
terminal-text-change-echo,src/cthulhu/scripts/terminal/script.py,terminal,Speak sanitized inserted or deleted text from terminals and editable text controls,Storm Dragon,internal/app,implemented,go test ./internal/atspi ./internal/presentation ./internal/app -count=1,"Maintainer accepted terminal insertion in LXTerminal on Xorg on 2026-09-11, accepted the post-change LibreOffice Writer typing behavior on physical Xorg on 2026-09-12, and confirmed YAD close-and-prompt coalescing on physical Xorg on 2026-09-13. Physical acceptance of terminal deletion-redraw coalescing is pending.",,Maintainer confirms thorough live testing of native application and terminal workflows; typing echo; selection; clipboard feedback; and object navigation on 2026-10-22. These workflows pass. See docs/testing/release-acceptance.md.,,Text changes are limited to terminals and editable text or paragraph roles. A change suppresses the next caret event from the same object so typing does not replay the line. Same-terminal insertion within the event-coalescing window suppresses a redundant interrupting terminal-focus presentation. A terminal deletion waits 50 ms and is suppressed only when the same terminal inserts replacement text at the same AT-SPI offset; standalone and different-offset deletions remain presentable. Debug speech delivery records retain the final utterance with cause and policy metadata; normal logging omits speech content. Broader application coverage remains incomplete.
|
||||
terminal-text-change-echo,src/cthulhu/scripts/terminal/script.py,terminal,Speak sanitized inserted or deleted text from terminals and editable text controls,Storm Dragon,internal/app,implemented,go test ./internal/atspi ./internal/presentation ./internal/app -count=1,"Maintainer accepted terminal insertion in LXTerminal on Xorg on 2026-09-11, accepted the post-change LibreOffice Writer typing behavior on physical Xorg on 2026-09-12, and confirmed YAD close-and-prompt coalescing on physical Xorg on 2026-09-13. Physical acceptance of terminal deletion-redraw coalescing is pending.",,Maintainer confirms thorough live testing of native application and terminal workflows; typing echo; selection; clipboard feedback; and object navigation on 2026-10-22. These workflows pass. See docs/testing/release-acceptance.md.,,"Text changes are limited to terminals and editable text or paragraph roles. A change suppresses the next caret event from the same object so typing does not replay the line. Same-terminal insertion within the event-coalescing window suppresses a redundant interrupting terminal-focus presentation. A terminal deletion waits 50 ms and is suppressed only when the same terminal inserts replacement text at the same AT-SPI offset; standalone and different-offset deletions remain presentable. Debug speech delivery records retain the final utterance with cause and policy metadata; normal logging omits speech content. Broader application coverage remains incomplete. Focusable writable browser editors also own their editable paragraph events, preventing rejected insertions from replaying the growing line through caret speech. Private Chromium coverage and live acceptance steps are in docs/testing/browser-editor.md; original-page ChatGPT listening and physical Braille acceptance remain pending."
|
||||
managed-descendant-presentation,src/cthulhu/scripts/default.py,focus,Resolve focused container selection and active-descendant changes to the managed child,Storm Dragon,internal/atspi internal/app,implemented,go test ./internal/atspi ./internal/app -count=1; python3 tests/native_x11.py; python3 tests/native_atspi.py --table-only,The isolated GTK/Xvfb/D-Bus selection fixture and physical I38 run-dialog completion selection speech passed on 2026-09-12 without regressing X11 pass-through command consumption release ordering lock states lifecycle or grab cleanup.,,Maintainer confirms thorough live testing of native application and terminal workflows; typing echo; selection; clipboard feedback; and object navigation on 2026-10-22. These workflows pass. See docs/testing/release-acceptance.md.,,The generic adapter registers both selection and active-descendant events resolves their managed child preserves the originating container path and filters events unless the container or child owns focus. Duplicate target presentations are suppressed. No I38-specific workaround was added. Broader GTK and Qt managed-descendant coverage remains pending. October 17: GTK controls with list role and Table interface retain selected-cell focus and table ownership without normalizing ordinary lists or choosing among multiple selected rows. Private native focus and recorded-speech tests cover first Down and genuine Shift+Tab exit; activation descriptions are not repeated on first row selection. Current physical listening and Braille acceptance remain pending. See docs/testing/gui-list-tables.md.
|
||||
ssip-speak-cancel,src/cthulhu/speechdispatcherfactory.py,speech,Send speech and cancellation through SSIP,Storm Dragon,internal/speech,in-progress,go test ./internal/speech -count=1,Maintainer accepted audible non-consuming Control/Shift interruption in the Stage 1 physical Xorg checklist on 2026-09-11.,,Maintainer heard lifecycle and terminal speech plus Control/Shift interruption during the accepted Stage 1 physical Xorg checklist on 2026-09-11.,,Protocol replies 230 and 225 were confirmed against the local service; broader backend and X11 coverage remains pending.
|
||||
web-structural-navigation,src/cthulhu/scripts/web/script.py,web,Move by headings and other document structure,Storm Dragon,internal/input internal/review internal/app,implemented,go test -race ./... -count=1; python3 tests/native_browser.py,Private Brave/Xvfb structural and mode/chrome sequences pass on 2026-09-20.,,Maintainer confirms extensive live browser testing passes in Firefox and Chromium-based browsers on 2026-10-22; element lists and jumping to selected items are explicitly confirmed. See docs/testing/release-acceptance.md.,,Structural navigation follows native caret movement. Echo suppression ends on subsequent input. Failed same-object context capture retries. Delayed document carets cannot override editable focus and stale results cannot overwrite unresolved unrelated focus. All 27 browse symbols pass through after routing is disabled. Complete Python structural settings and dialogs remain pending. Parity review corrections and remaining physical gates: docs/testing/parity-review-follow-up.md.
|
||||
|
||||
|
@@ -0,0 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="en"><meta charset="utf-8"><title>Cthulhu Browser Probe</title>
|
||||
<body>
|
||||
<div contenteditable="true" role="textbox" aria-label="Message composer" aria-multiline="true"><p>Draft:</p><p>Following line</p></div>
|
||||
<div contenteditable="true" role="textbox" aria-label="Other editor" aria-multiline="true"><p>Other text</p></div>
|
||||
<button>After editors</button>
|
||||
</body></html>
|
||||
@@ -33,6 +33,7 @@ BROWSER_PROOFS = {
|
||||
"live": ("./internal/atspi", "^TestNativeBrowserLiveRegion$", 30, 35),
|
||||
"document": ("./internal/app", "^TestNativeBrowserDocumentCommands$", 45, 50),
|
||||
"editing": ("./internal/app", "^TestNativeBrowserEditing$", 45, 50),
|
||||
"editor": ("./internal/app", "^TestNativeBrowserEditorTyping$", 30, 35, "browser-editor.html"),
|
||||
"selection": ("./internal/app", "^TestNativeBrowserSelection$", 35, 40),
|
||||
"modes": ("./internal/app", "^TestNativeBrowserModes$", 45, 50),
|
||||
"keys": ("./internal/platform/xorg", "^TestNativeBrowseKeyTransitions$", 20, 25),
|
||||
|
||||
Reference in new issue
Block a user