MUSHclient consumes queued Up/Down keys in PreTranslateMessage before the Edit control receives WM_KEYDOWN. It recalls commands with SetWindowText followed by SetSel at the end of the replacement. The editor subclass only refreshed its snapshot for WM_SETTEXT, so the recalled command was silent or followed by a stray character or blank from the caret update. Recalling equal-length commands also leaves the final caret position unchanged. Observe Up/Down for the focused standard Edit control in WH_GETMESSAGE, excluding Ctrl-modified navigation. Associate the next successful WM_SETTEXT with that retrieved key and announce the bounded recalled text as an existing ordered text-read action. Report empty input as blank. Consume the pending recall after the replacement and suppress the ensuing EM_SETSEL feedback so each recalled command is announced once. Reset recall tracking on subsequent retrieved messages, actual focus and ordinary dispatched navigation. Preserve snapshot refreshes and normal Left/Right reading after recalls. Keep password and foreground/focus exclusion in place, leave unrelated programmatic text updates silent, and check the control class before accessing the shared attachment property so Scintilla's different state structure is untouched. Both native hook architectures receive the fix through the existing build. Extend the Edit fixture with application-side pretranslation of queued history arrows. Assert the exact ordered feedback for Up/Down, equal-length entries, rapid recalls, empty input and horizontal navigation afterward. Require silence for unchanged history boundaries, unrelated programmatic updates, protected fields and independently unprotected background fields. Document history behavior and live MUSHclient acceptance steps. Validation used a private Xorg dummy display, disposable Wine prefixes and recording D-Bus services, with GUI checks run sequentially: - cmake --build build --parallel - ctest --test-dir build --output-on-failure: all four checks passed - python3 tests/menu_integration.py --build-dir build --fixture edit - The Edit check with --32-bit, --32-bit --late-helper, and --32-bit --restart-helper - python3 tests/menu_integration.py --build-dir build --fixture winforms --32-bit - python3 tests/menu_integration.py --build-dir build --fixture notepad - python3 tests/menu_integration.py --build-dir build --fixture menu --32-bit - git diff --check The new 32-bit history regression failed against the original hook, recording only blank actions, and passed with the fix. Final 32-bit, 64-bit and Windows Forms editor checks passed, including existing typing, selection, deletion, Unicode, password and read-only assertions. Built and installed the helper into the existing user-local installation, backed up the previous five files and verified the installed artifacts. The user subsequently confirmed that the installed fix works in MUSHclient. Physical braille acceptance was not separately established.
353 lines
25 KiB
Python
353 lines
25 KiB
Python
"""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.
|
|
"""
|
|
import argparse
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
|
|
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("--reader", choices=("cthulhu", "orca"))
|
|
parser.add_argument("--fixture", choices=("menu", "edit", "notepad", "list", "listbox", "delphi", "winforms", "controls", "scintilla", "goldwave"), default="menu")
|
|
parser.add_argument("--notepadpp-exe", type=Path)
|
|
parser.add_argument("--goldwave-exe", type=Path)
|
|
parser.add_argument("--32-bit", dest="is32Bit", action="store_true")
|
|
parser.add_argument("--late-helper", action="store_true")
|
|
parser.add_argument("--restart-helper", action="store_true")
|
|
args = parser.parse_args()
|
|
if args.is32Bit and args.fixture not in ("menu", "edit", "winforms", "list", "listbox", "delphi", "controls"):
|
|
parser.error("32-bit checks currently require --fixture menu, edit, winforms, list, listbox, delphi or controls")
|
|
if (args.late_helper or args.restart_helper) and args.fixture not in ("edit", "winforms", "scintilla"):
|
|
parser.error("helper lifecycle checks require --fixture edit, winforms or scintilla")
|
|
if args.fixture == "scintilla" and (not args.notepadpp_exe or not args.notepadpp_exe.is_file()):
|
|
parser.error("--fixture scintilla requires --notepadpp-exe pointing to a 64-bit Notepad++ executable")
|
|
if args.fixture == "goldwave" and (not args.goldwave_exe or not args.goldwave_exe.is_file()):
|
|
parser.error("--fixture goldwave requires --goldwave-exe pointing to GoldWave 5.70")
|
|
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,
|
|
*(["--reader", args.reader] if args.reader else []),
|
|
*(["--notepadpp-exe", str(args.notepadpp_exe.resolve())] if args.notepadpp_exe else []),
|
|
*(["--goldwave-exe", str(args.goldwave_exe.resolve())] if args.goldwave_exe else []),
|
|
*(["--32-bit"] if args.is32Bit else []),
|
|
*(["--late-helper"] if args.late_helper else []),
|
|
*(["--restart-helper"] if args.restart_helper else [])]))
|
|
|
|
interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
|
object_path = "/" + interface.replace(".", "/")
|
|
readerArguments = ["--reader", args.reader] if args.reader else []
|
|
xml = '<node><interface name="' + interface + '"><method name="PresentAccessibleEvent">'
|
|
for index, kind in enumerate(["s"] * 7 + ["i", "i", "s"]):
|
|
xml += f'<arg name="arg{index}" type="{kind}" direction="in"/>'
|
|
xml += '<arg type="b" direction="out"/></method></interface></node>'
|
|
events = []
|
|
|
|
def record(connection, sender, path, iface, method, parameters, invocation):
|
|
events.append(parameters.unpack())
|
|
invocation.return_value(GLib.Variant("(b)", (True,)))
|
|
|
|
connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
|
connection.register_object(object_path, Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], record, None, None)
|
|
connection.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus",
|
|
"org.freedesktop.DBus", "RequestName",
|
|
GLib.Variant("(su)", (interface, 0)), None,
|
|
Gio.DBusCallFlags.NONE, 1000, None)
|
|
|
|
with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
|
env = os.environ.copy()
|
|
env.update(WINEPREFIX=str(Path(temporary) / "prefix"), WINEDEBUG=os.environ.get("WINE_ACCESS_TEST_DEBUG", "-all"),
|
|
WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1")
|
|
ready = Path(temporary) / "ready"
|
|
env["WINE_ACCESS_FIXTURE_READY"] = "Z:" + str(ready).replace("/", "\\")
|
|
if args.fixture == "goldwave":
|
|
appDir = Path(temporary) / "goldwave"
|
|
shutil.copytree(args.goldwave_exe.parent, appDir)
|
|
env["WINE_ACCESS_GOLDWAVE_EXE"] = "Z:" + str(appDir / args.goldwave_exe.name).replace("/", "\\")
|
|
if args.fixture == "scintilla":
|
|
# Copy the application to isolate its configuration as well as its
|
|
# document. Never open or edit files in the user's running instance.
|
|
appDir = Path(temporary) / "notepadpp"
|
|
appDir.mkdir()
|
|
for name in (args.notepadpp_exe.name, "langs.model.xml", "stylers.model.xml"):
|
|
source = args.notepadpp_exe.parent / name
|
|
if source.is_file():
|
|
shutil.copy2(source, appDir / name)
|
|
(appDir / "doLocalConf.xml").touch()
|
|
document = Path(temporary) / "editor.txt"
|
|
document.write_bytes(("hello beta\r\nScintilla second line\r\ncafé 😀 tail\r\n" +
|
|
"x" * 1100000 + "\r\n" + "x" * 1022 + "😀tail").encode("utf-8"))
|
|
env["WINE_ACCESS_NOTEPADPP_EXE"] = "Z:" + str(appDir / args.notepadpp_exe.name).replace("/", "\\")
|
|
env["WINE_ACCESS_SCINTILLA_DOCUMENT"] = "Z:" + str(document).replace("/", "\\")
|
|
fixture = Path(temporary) / "menu-fixture.exe"
|
|
compiler = "i686-w64-mingw32-gcc" if args.is32Bit or args.fixture == "goldwave" else "x86_64-w64-mingw32-gcc"
|
|
sourceName = "edit" if args.fixture == "winforms" else args.fixture
|
|
if args.fixture == "delphi":
|
|
sourceName = "list"
|
|
subprocess.run([compiler, "-Wall", "-Wextra", "-mwindows",
|
|
*(["-DWINE_ACCESS_WINFORMS_FIXTURE"] if args.fixture == "winforms" else []),
|
|
*(["-DWINE_ACCESS_DELPHI_FIXTURE"] if args.fixture == "delphi" else []),
|
|
"-o", str(fixture), str(Path(__file__).with_name(sourceName + "_fixture.c")),
|
|
"-luser32", "-lcomctl32", "-loleacc", "-lole32", "-ladvapi32"], check=True)
|
|
helper = None
|
|
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() + 10
|
|
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", *readerArguments], env=env)
|
|
# Allow the helper to register its bridge before opening the fixture.
|
|
deadline = time.monotonic() + 1
|
|
while time.monotonic() < deadline:
|
|
while GLib.MainContext.default().iteration(False):
|
|
pass
|
|
time.sleep(0.01)
|
|
assert helper.poll() is None, "helper exited before fixture startup"
|
|
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", *readerArguments], env=env)
|
|
restarted = True
|
|
time.sleep(0.01)
|
|
assert app.poll() == 0, "fixture failed or timed out"
|
|
assert helper.poll() is None, f"helper exited during the fixture: {helper.returncode}"
|
|
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}"
|
|
elif event[1] == "value" and event[3] == "combo box":
|
|
assert event[0] == focused, f"combo value sent for an unfocused source: {event!r}; focus={focused}; events={events!r}"
|
|
if args.fixture == "goldwave":
|
|
for event in events:
|
|
print(event[1:9])
|
|
menus = [e for e in events if ":menu:" in e[0]]
|
|
assert any(e[2] == "File" and e[3] == "submenu" for e in menus), "GoldWave File menu missing"
|
|
assert any(e[2].startswith("Open") and e[3] == "menu item" for e in menus), "GoldWave Open command missing"
|
|
values = [e[4] for e in events if e[1] == "value" and e[3] == "combo box"]
|
|
assert len(values) >= 3 and len(set(values)) >= 2, f"GoldWave combo arrow feedback missing: {values!r}"
|
|
print("PASS: actual 32-bit GoldWave menus and file-type combo arrows without tabbing away")
|
|
sys.exit(0)
|
|
if args.fixture == "controls":
|
|
for event in events:
|
|
print(event[1:9])
|
|
for caption in ("Albums & artists β", "Details", "Default Playlist", "Second Playlist"):
|
|
assert any(e[2] == caption and e[3] == "page tab" and e[7] in (1, 2)
|
|
and e[8] == 2 for e in events), f"native/ATL tab missing: {caption}"
|
|
assert any(e[2] == "All music" and e[3] == "tree item" and "collapsed" in e[5]
|
|
for e in events), "collapsed tree selection missing"
|
|
assert any(e[2] == "All music" and "expanded" in e[5] for e in events), "tree expansion missing"
|
|
assert any(e[2] == "Artist & album β" and e[3] == "tree item" for e in events), "tree navigation missing"
|
|
assert any(e[1] == "focus" and e[2] == "Named & native button" and e[3] == "button"
|
|
for e in events), "named generic MSAA client hid button role"
|
|
for value in ("artist & album", "year && date"):
|
|
assert any(e[2] == "View & sort" and e[3] == "combo box" and e[4] == value
|
|
for e in events), f"combo label/role/value missing: {value}"
|
|
assert any(e[4] == "search & find" for e in events), "edit value changed"
|
|
if not args.is32Bit:
|
|
assert any(e[2] == "Filter & query" and e[3] == "edit" and e[4] == "search & find"
|
|
for e in events), "late native edit label/value missing"
|
|
assert any(e[3] == "list" and e[4] == "Rock & roll && blues"
|
|
for e in events), "list box lost literal ampersands"
|
|
controlEvents = [e for e in events if e[3] in ("page tab", "tree item", "button", "combo box", "edit")]
|
|
assert controlEvents and all(e[9] == "Wine & controls fixture" for e in controlEvents), "window title changed"
|
|
assert any(e[2] == "" and e[3] == "page tab" and e[7:9] == (0, 0)
|
|
for e in events), "empty tabs retained stale caption"
|
|
assert any(e[2] == "" and e[3] == "tree" for e in events), "empty tree retained stale item"
|
|
assert not any(e[2] == "Tree1" for e in events), "generic tree name leaked"
|
|
print("PASS: native/ATL tabs, tree selection/states/return, named button, combo and edit labels, empty controls")
|
|
sys.exit(0)
|
|
if args.fixture == "delphi":
|
|
for event in events:
|
|
print(event[1:9])
|
|
assert any(e[1] == "focus" and e[2] == "Required components" and
|
|
e[3] == "list" and e[4] == "Pending, Runtime & tools β" and
|
|
e[5] == "not checked" and e[7:9] == (1, 2) for e in events), "Delphi initial row/label/state missing"
|
|
rows = [e for e in events if e[3] == "list" and e[4] == "Installed, Second component"]
|
|
assert any(e[1] == "selection" and e[5] == "selected, not checked" for e in rows), "Delphi Down missing"
|
|
assert any(e[1] == "selection" and e[5] == "selected, checked" for e in rows), "Space checkbox change missing"
|
|
assert any(e[1] == "focus" and e[5] == "selected, checked" for e in rows), "Delphi return lost checkbox"
|
|
assert sum(e[1] == "selection" and e[5] == "selected, not checked" for e in rows) >= 2, "Space uncheck missing"
|
|
for caption in ("Return to control", "OK"):
|
|
assert any(e[1] == "focus" and e[2] == caption and e[3] == "button" for e in events), f"Delphi button missing: {caption}"
|
|
assert any(e[3] == "list" and e[4] == "" and e[5] == "" and e[7:9] == (0, 0) for e in events), "empty Delphi list retained state"
|
|
assert not any(e[2] == "List1" for e in events), "Delphi generic client leaked"
|
|
print("PASS: Delphi list labels, rows, keyboard navigation, checkbox toggles, return, buttons and empty state")
|
|
sys.exit(0)
|
|
if args.fixture == "listbox":
|
|
listEvents = [e for e in events if e[3] == "list"]
|
|
for event in listEvents:
|
|
print(event[1:9])
|
|
assert any(e[1] == "focus" and e[2] == "Tutorial maps" and
|
|
e[4] == "First map" and e[7:9] == (1, 3)
|
|
for e in listEvents), "initial map or label missing"
|
|
selections = [(e[4], e[7], e[8]) for e in listEvents if e[1] == "selection"]
|
|
assert selections == [("Second map β", 2, 3), ("Third map", 3, 3),
|
|
("Second map β", 2, 3), ("", 0, 0)], \
|
|
f"map navigation/empty state missing or duplicated: {selections}"
|
|
assert sum(e[1] == "focus" and e[4] == "First map"
|
|
for e in listEvents) == 2, "return to map list missing or duplicated"
|
|
assert any(e[1] == "focus" and e[2] == "OK" for e in events), "button focus missing"
|
|
assert not any("Background-only" in str(e) for e in events), "background map leaked"
|
|
assert len({e[0] for e in listEvents}) == 1, "map events changed source identity"
|
|
print("PASS: list-box arrows, Unicode, positions, focus return, background exclusion and empty state")
|
|
sys.exit(0)
|
|
if args.fixture == "list":
|
|
listEvents = [e for e in events if e[3] == "list"]
|
|
for event in listEvents:
|
|
print(event[1:9])
|
|
assert any(e[1] == "focus" and e[2] == "Required components" and
|
|
e[4] == "Pending, Runtime & tools β" and e[7:9] == (1, 2)
|
|
and e[5] == "" for e in listEvents), "unselected initial row or label missing"
|
|
assert any(e[1] == "selection" and e[4] == "Installed, Second component" and
|
|
e[7:9] == (2, 2) for e in listEvents), "Down lost second row/columns"
|
|
assert any(e[1] == "selection" and e[4] == "Pending, Runtime & tools β" and
|
|
e[7:9] == (1, 2) for e in listEvents), "Up lost first row"
|
|
assert any(e[1] == "focus" and e[4] == "Pending, Runtime & tools β" and
|
|
e[5] == "selected" for e in listEvents), "return to list lost selection"
|
|
assert any(e[4] == "Ready, Runtime & tools β" for e in listEvents), "value event lost changed row text"
|
|
assert any(e[4] == "" and e[7:9] == (0, 0) for e in listEvents), "empty list retained stale row"
|
|
assert any(e[2] == "Return to control" for e in events), "button focus missing"
|
|
assert not any(e[2] == "List1" for e in events), "generic MSAA client leaked"
|
|
print("PASS: native list focus, columns, Unicode, navigation, return, and empty list")
|
|
sys.exit(0)
|
|
if args.fixture == "notepad":
|
|
dialogEvents = [e for e in events if e[2] in ("Yes", "No", "Cancel")]
|
|
for event in dialogEvents:
|
|
print("Save dialog:", event[:7])
|
|
assert [e[2] for e in dialogEvents if e[1] == "focus"] == ["Yes", "No", "Cancel", "No"], "save dialog focus order changed"
|
|
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"
|
|
typed = [e[2] for e in text_events if e[1] == "text" and "kind=8" in e[5]]
|
|
assert typed == ["z", "z", "space", "new line"], f"Notepad typing echo missing or incorrect: {typed!r}"
|
|
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 typing echo, keyboard characters, words, lines, selection, deletion, and menu return")
|
|
sys.exit(0)
|
|
if args.fixture == "scintilla":
|
|
textEvents = [e for e in events if e[0].endswith(":text")]
|
|
for event in textEvents:
|
|
print(event[1:7])
|
|
spoken = [e[2] for e in textEvents if e[1] == "text"]
|
|
for expected in ("e", "l", "beta", "hello ", "Scintilla second line", "hello beta", "S selected",
|
|
"c selected", "c unselected", "S unselected", "S deleted", "c",
|
|
"Scintilla selected", "Scintilla deleted", "café 😀 tail", "é", "😀",
|
|
"😀 selected", "😀 unselected", "😀 deleted"):
|
|
assert expected in spoken, f"missing Scintilla feedback: {expected!r}; {textEvents!r}"
|
|
assert spoken.count("l") == 2, "Scintilla lost repeated characters"
|
|
assert any(e[1] == "focus" and e[4] == "intilla second line" for e in textEvents), "menu return lost Scintilla caret line"
|
|
assert any(e[3] == "read only edit" for e in textEvents), "Scintilla read-only role missing"
|
|
assert any(e[2] == "c" and "kind=7" in e[5] for e in textEvents), "Scintilla Backspace suffix wrong"
|
|
assert any(e[2] == "😀" and "kind=7" in e[5] for e in textEvents), "multibyte Backspace missing"
|
|
assert any(e[2] == "x" * 1022 for e in textEvents), "Scintilla truncation split a surrogate pair"
|
|
assert any(e[2] == "x" * 1023 for e in textEvents), "large Scintilla document line missing"
|
|
focusEvents = [e for e in textEvents if e[1] == "focus"]
|
|
assert [e[4] for e in focusEvents[-2:]] == ["hello beta", "hello beta"], "single-byte Scintilla charset reading failed"
|
|
assert all("offset_unit=byte" in e[5] for e in textEvents if e[1] == "text"), "Scintilla offsets mislabeled"
|
|
print("PASS: actual Notepad++ arrows, words, Unicode, selection, deletion, undo, menu return and read-only document")
|
|
sys.exit(0)
|
|
if args.fixture in ("edit", "winforms"):
|
|
text_events = [e for e in events if e[0].endswith(":text")]
|
|
for event in text_events:
|
|
print(event[2:7])
|
|
spoken = [e[2] for e in text_events]
|
|
for expected in ("l", "beta", "second line", "s selected", "e selected",
|
|
"e unselected", "s unselected", "s deleted", "e",
|
|
"alpha selected", "alpha deleted", "😀"):
|
|
assert expected in spoken, f"missing editor feedback: {expected!r}"
|
|
assert any(e[3] == "edit" and e[4] == "alpha beta" for e in text_events), "initial line missing"
|
|
assert any(e[3] == "read only edit" for e in text_events), "read-only focus missing"
|
|
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 any(e[1] == "text" and e[2] == "e" and "kind=7" in e[5] for e in text_events), "Backspace did not report only the removed character"
|
|
assert "e deleted" not in spoken, "Backspace retained the deletion suffix"
|
|
assert sum(e[2] == "new line" and "kind=7" in e[5] for e in text_events) == 1, "CRLF Backspace feedback missing"
|
|
assert sum(e[2] == "new line deleted" and "kind=4" in e[5] for e in text_events) == 1, "CRLF Delete 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"
|
|
for line in ("readonly first", "readonly second"):
|
|
assert any(e[1] == "text" and e[2] == line for e in text_events), f"read-only arrow feedback missing: {line}"
|
|
assert "r deleted" not in spoken, "read-only document reported an unsuccessful deletion"
|
|
typed = [e[2] for e in text_events if e[1] == "text" and "kind=8" in e[5]]
|
|
assert typed == ["z", "z", "space", "\u00e9", "\U0001f600", "new line", "tab", "q", "L", "\U0001f600"], f"typing echo missing or incorrect: {typed!r}"
|
|
assert not any(e[1] == "text" and e[2] in ("!", "@", "~", "%") for e in events), "rejected, password or background typing leaked"
|
|
history_focus = next(e[0] for e in text_events
|
|
if e[1] == "focus" and e[4] == "blank")
|
|
history_actions = [e[2] for e in text_events
|
|
if e[0] == history_focus and e[1] == "text"]
|
|
assert history_actions == ["history north", "history south", "history north", "blank",
|
|
"history north", "history south", "history north", "h", "blank", "blank"], \
|
|
f"command history missing, reordered, duplicated or leaked: {history_actions!r}"
|
|
if args.fixture == "winforms":
|
|
assert any(e[2] == "Email" and e[3] == "edit" and e[4] == "alpha beta"
|
|
for e in text_events), "managed email label/focus missing"
|
|
assert any(e[2] == "Password" and e[3] == "edit" and e[6] == "password"
|
|
and e[4] == "" for e in text_events), "managed password label/protection missing"
|
|
assert not any(e[2] == "Unrelated" for e in text_events), "unrelated label associated with field"
|
|
print("PASS: editor focus, typing echo, 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]]
|
|
for event in menus:
|
|
print(event[2:7])
|
|
assert any(e[2] == "File" and e[3] == "submenu" for e in menus), "File menu missing"
|
|
assert any(e[2] == "Open" and e[4] == "Ctrl+O" for e in menus), "Open or shortcut missing"
|
|
assert any(e[2] == "Word wrap" and e[6] == "checked" for e in menus), "checked item missing"
|
|
assert any(e[2] == "Edit" and e[3] == "submenu" for e in menus), "Edit menu missing"
|
|
assert any(e[2] == "Cut" and e[4] == "Ctrl+X" for e in menus), "Edit command missing"
|
|
assert any(e[2] == "Context action" for e in menus), "context menu missing"
|
|
assert any(e[2] == "Favorites" and e[3] == "submenu" for e in menus), "nested submenu missing"
|
|
assert any(e[2] == "Nested action" for e in menus), "nested command missing"
|
|
saved = [e for e in menus if e[2] == "Pinned & offline"]
|
|
assert len(saved) == 2, f"duplicate suppression/menu reset failed: {len(saved)}"
|
|
assert all(e[4] == "Ctrl+P" and e[6] == "checked, unavailable" for e in saved)
|
|
assert any(e[2] == "Return to control" and e[3] == "button" for e in events), "control focus missing"
|
|
assert any(e[2] == "Application-owned accessibility" and e[3] == "client"
|
|
for e in events), "marshaled application MSAA object unavailable"
|
|
subprocess.run([str(build / "cthulhu-wine-access.exe"),
|
|
"--no-dialog-reader", "--no-controller"],
|
|
env=env, check=True, timeout=5)
|
|
print("PASS: native and nested menus, context menus, shortcuts, states, "
|
|
"duplicate suppression, reopening, marshaled MSAA, control focus, and disable options")
|
|
finally:
|
|
# Only this test's newly created Wine prefix is affected.
|
|
subprocess.run(["wineserver", "-k"], env=env, timeout=10, check=False)
|
|
for child in (app, helper):
|
|
if child is not None:
|
|
child.wait(timeout=10)
|