Finish up edit field accessibility.

This commit is contained in:
ak47 committed 2026-10-10 00:18:22 -04:00
1 parent 6f6d4040c4
commit c5657ee5d4
9 files changed
+361 -39

No files matched your search

+51 -5
View File
@@ -1,4 +1,4 @@
"""Exercise a real Wine menu hook against a private D-Bus recording service.
"""Exercise Wine menus/editors against a private D-Bus recording service.
Requires a graphical session, PyGObject, Wine, and MinGW. No speech is sent to
the running reader. The fixture window closes automatically.
@@ -16,13 +16,19 @@ from gi.repository import Gio, GLib
parser = argparse.ArgumentParser()
parser.add_argument("--build-dir", default="build")
parser.add_argument("--session", action="store_true")
parser.add_argument("--fixture", choices=("menu", "edit"), default="menu")
parser.add_argument("--fixture", choices=("menu", "edit", "notepad"), default="menu")
parser.add_argument("--late-helper", action="store_true")
parser.add_argument("--restart-helper", action="store_true")
args = parser.parse_args()
if (args.late_helper or args.restart_helper) and args.fixture != "edit":
parser.error("helper lifecycle checks require --fixture edit")
build = Path(args.build_dir).resolve()
if not args.session:
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable,
__file__, "--session", "--build-dir", str(build),
"--fixture", args.fixture]))
"--fixture", args.fixture,
*(["--late-helper"] if args.late_helper else []),
*(["--restart-helper"] if args.restart_helper else [])]))
interface = "org.stormux.Cthulhu1.Service"
object_path = "/org/stormux/Cthulhu1/Service"
@@ -47,6 +53,8 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
env = os.environ.copy()
env.update(WINEPREFIX=str(Path(temporary) / "prefix"), WINEDEBUG="-all",
WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1")
ready = Path(temporary) / "ready"
env["WINE_ACCESS_FIXTURE_READY"] = "Z:" + str(ready).replace("/", "\\")
fixture = Path(temporary) / "menu-fixture.exe"
subprocess.run(["x86_64-w64-mingw32-gcc", "-Wall", "-Wextra", "-mwindows",
"-o", str(fixture), str(Path(__file__).with_name(args.fixture + "_fixture.c")),
@@ -55,6 +63,12 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
app = None
try:
subprocess.run(["wineboot", "-u"], env=env, check=True, timeout=45)
if args.late_helper:
app = subprocess.Popen(["wine", str(fixture)], env=env)
deadline = time.monotonic() + 5
while not ready.exists() and app.poll() is None and time.monotonic() < deadline:
time.sleep(0.01)
assert ready.exists(), "editor did not become ready before helper attachment"
helper = subprocess.Popen([str(build / "cthulhu-wine-access.exe"), "--no-controller"], env=env)
# Allow the helper to register its bridge before opening the fixture.
deadline = time.monotonic() + 1
@@ -63,17 +77,43 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
pass
time.sleep(0.01)
assert helper.poll() is None, "helper exited before fixture startup"
app = subprocess.Popen(["wine", str(fixture)], env=env)
deadline = time.monotonic() + 15
if app is None:
app = subprocess.Popen(["wine", str(fixture)], env=env)
restarted = False
deadline = time.monotonic() + 25
while app.poll() is None and time.monotonic() < deadline:
while GLib.MainContext.default().iteration(False):
pass
if args.restart_helper and not restarted and any(e[0].endswith(":text") for e in events):
helper.terminate()
helper.wait(timeout=5)
helper = subprocess.Popen([str(build / "cthulhu-wine-access.exe"), "--no-controller"], env=env)
restarted = True
time.sleep(0.01)
assert app.poll() == 0, "fixture failed or timed out"
assert helper.poll() is None, "helper exited during the fixture"
if args.restart_helper:
assert restarted, "fixture never triggered a helper restart"
for _ in range(20):
while GLib.MainContext.default().iteration(False):
pass
time.sleep(0.01)
focused = None
for event in events:
if event[1] == "focus":
focused = event[0]
elif event[1] == "text":
assert event[0] == focused, f"text sent without establishing focus: {event!r}"
if args.fixture == "notepad":
text_events = [e for e in events if e[0].endswith(":text")]
spoken = [e[2] for e in text_events]
for expected in ("e", "l", "beta", "Notepad second line", "N selected",
"N unselected", "N deleted"):
assert expected in spoken, f"missing Notepad feedback: {expected!r}; {text_events!r}"
assert spoken.count("l") == 2, "Notepad lost repeated characters"
assert any(e[3] == "edit" and e[4] == "otepad second line" for e in text_events), "Notepad menu return missing"
print("PASS: actual Wine Notepad keyboard characters, words, lines, selection, deletion, and menu return")
sys.exit(0)
if args.fixture == "edit":
text_events = [e for e in events if e[0].endswith(":text")]
for event in text_events:
@@ -88,6 +128,12 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
assert any(e[6] == "password" for e in text_events), "password role missing"
assert not any("private-password-fixture" in str(e) for e in events), "password leaked"
assert "😀 deleted" not in spoken, "read-only control reported deletion"
assert "hello deleted" not in spoken, "replacement misreported as deletion"
assert sum(e[1] == "text" and e[2] == "l" for e in text_events) == 3, "rapid repeated letters lost"
assert any(e[3] == "edit" and e[4] == "menu return line" for e in text_events), "menu return lost caret line"
assert spoken.count("new line deleted") == 2, "CRLF deletion feedback missing"
assert any(e[4] == "x" * 1022 for e in text_events), "truncated surrogate discarded line"
assert "d" in spoken and "undo restored" in spoken, "Undo left stale text snapshot"
print("PASS: editor focus, characters, words, lines, selection, deletion, Unicode, passwords, and read-only controls")
sys.exit(0)
menus = [event for event in events if ":menu:" in event[0]]