366 lines
26 KiB
Python
366 lines
26 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 time
|
|
|
|
from gi.repository import Gio, GLib
|
|
|
|
from wine_test_environment import add_test_options, run_wine_test, wine_test_directory
|
|
|
|
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("--extension-endpoint", action="store_true")
|
|
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")
|
|
add_test_options(parser)
|
|
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(run_wine_test(["dbus-run-session", "--", sys.executable,
|
|
__file__, "--session", "--build-dir", str(build),
|
|
"--fixture", args.fixture,
|
|
*(["--extension-endpoint"] if args.extension_endpoint else []),
|
|
*(["--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 [])], build, "menu_integration",
|
|
keep=args.keep, keepOnFailure=args.keepOnFailure))
|
|
|
|
busName = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux.Cthulhu1.Service"
|
|
interface = busName.rsplit(".", 1)[0] + ".WineAccess" if args.extension_endpoint else busName
|
|
object_path = "/" + busName.replace(".", "/") + ("/WineAccess" if args.extension_endpoint else "")
|
|
readerArguments = ["--reader", args.reader] if args.reader else []
|
|
if args.extension_endpoint:
|
|
readerArguments += ["--reader-path", object_path, "--reader-interface", interface]
|
|
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>'
|
|
for method, inputs in (("SpeakText", "sb"), ("CancelSpeech", "")):
|
|
xml += '<method name="' + method + '">'
|
|
xml += ''.join('<arg type="' + kind + '" direction="in"/>' for kind in inputs)
|
|
xml += '<arg type="b" direction="out"/></method>'
|
|
xml += '</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)", (busName, 0)), None,
|
|
Gio.DBusCallFlags.NONE, 1000, None)
|
|
|
|
with wine_test_directory(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 / "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 / "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", "n"], \
|
|
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 / "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)
|