More fixes for menus and combo boxes.
This commit is contained in:
1 parent
67ebc28d15
commit
872388a59b
11 files changed
+392
-35
No files matched your search
+34
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
Build the project, then run `python3 tests/menu_integration.py --build-dir build`
|
||||
from the repository root in a graphical session. Dependencies are Wine,
|
||||
MinGW's x86_64 C compiler, PyGObject, and `dbus-run-session`.
|
||||
MinGW's x86_64 and i686 C compilers, PyGObject, and `dbus-run-session`.
|
||||
|
||||
The test creates a temporary Wine prefix and a private D-Bus service. Its
|
||||
64-bit PE application exercises the separately running Winelib helper's
|
||||
@@ -108,6 +108,39 @@ reader's `docs/testing/wine-accessibility.md` for current evidence and live
|
||||
acceptance gates. Run Xvfb fixtures sequentially; concurrent automatic display
|
||||
allocation can collide.
|
||||
|
||||
## GoldWave menus and combo boxes
|
||||
|
||||
The helper supplies both 32-bit and 64-bit menu hooks. Check both architectures:
|
||||
|
||||
```sh
|
||||
xvfb-run -a python3 tests/menu_integration.py --fixture menu
|
||||
xvfb-run -a python3 tests/menu_integration.py --fixture menu --32-bit
|
||||
xvfb-run -a python3 tests/menu_integration.py --fixture controls
|
||||
xvfb-run -a python3 tests/menu_integration.py --fixture controls --32-bit
|
||||
xvfb-run -a python3 tests/menu_integration.py --fixture goldwave --goldwave-exe /path/to/GoldWave.exe
|
||||
```
|
||||
|
||||
The controls fixture changes a combo selection without emitting a value
|
||||
WinEvent. It requires the new value while focus stays on the combo, with the
|
||||
same source identity as its focus event. This catches both missing polling and
|
||||
Wine's misleading focus notifications for the combo's internal list.
|
||||
|
||||
The optional GoldWave 5.70 fixture copies the application directory into a
|
||||
temporary directory and uses a fresh Wine prefix with Force screen reader
|
||||
mode enabled. It waits for the actual main form, dismisses the first-run
|
||||
settings prompt in the test copy, and drives Alt+F, menu arrows, Ctrl+O and
|
||||
file-type combo Down/Down/Up using SendInput. On October 11 it received File,
|
||||
New, Open and unavailable Close menu events, followed by Wave/MPEG Audio/Wave
|
||||
value updates without tabbing away. This confirms helper events through the
|
||||
private D-Bus recorder; audible speech and physical braille need live checks.
|
||||
|
||||
Install all five helper artifacts together, restart Cthulhu, and reopen
|
||||
GoldWave. With screen reader mode enabled, check File and other menus with
|
||||
Alt and arrows, then Escape back to the previous control. In Open Sound,
|
||||
check Files of type and Look in with arrows: expect the new value immediately
|
||||
in speech and braille, with keyboard focus remaining on the combo. Repeat
|
||||
Tab/Shift+Tab and Alt+Down/Escape dropdown opening and closing.
|
||||
|
||||
## Notepad++ / Scintilla checks
|
||||
|
||||
Use a locally available 64-bit Notepad++ executable:
|
||||
|
||||
@@ -28,7 +28,7 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
|
||||
case 10: focus_control(combo); break;
|
||||
case 11:
|
||||
SendMessageW(combo, CB_SETCURSEL, 1, 0);
|
||||
NotifyWinEvent(EVENT_OBJECT_VALUECHANGE, combo, OBJID_CLIENT, CHILDID_SELF);
|
||||
// Common file dialogs can change the selection without a WinEvent.
|
||||
break;
|
||||
case 12: focus_control(edit); break;
|
||||
case 13: focus_control(atlTabs); break;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Drive a disposable copy of GoldWave 5.70 using real Wine keyboard input.
|
||||
#include <windows.h>
|
||||
#include <dlgs.h>
|
||||
|
||||
static DWORD appPid;
|
||||
static HWND app, dialog;
|
||||
|
||||
static BOOL CALLBACK dismiss_setup(HWND window, LPARAM unused) {
|
||||
(void)unused;
|
||||
WCHAR text[64] = {0};
|
||||
GetWindowTextW(window, text, 64);
|
||||
if (!lstrcmpW(text, L"No") || !lstrcmpW(text, L"&No"))
|
||||
PostMessageW(window, BM_CLICK, 0, 0);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static BOOL CALLBACK find_window(HWND window, LPARAM unused) {
|
||||
(void)unused;
|
||||
DWORD pid = 0;
|
||||
WCHAR title[128] = {0};
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
GetWindowTextW(window, title, 128);
|
||||
if (pid == appPid && IsWindowVisible(window)) {
|
||||
WCHAR cls[128] = {0};
|
||||
GetClassNameW(window, cls, 128);
|
||||
if (!lstrcmpW(cls, L"TGWMainForm")) app = window;
|
||||
if (GetDlgItem(window, cmb1)) dialog = window;
|
||||
if (!lstrcmpW(cls, L"TMessageForm")) EnumChildWindows(window, dismiss_setup, 0);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void key(WORD vk, BOOL alt, BOOL control) {
|
||||
INPUT input[6] = {0};
|
||||
UINT count = 0;
|
||||
if (alt) { input[count].ki.wVk = VK_MENU; ++count; }
|
||||
if (control) { input[count].ki.wVk = VK_CONTROL; ++count; }
|
||||
input[count++].ki.wVk = vk;
|
||||
input[count].ki.wVk = vk; input[count++].ki.dwFlags = KEYEVENTF_KEYUP;
|
||||
if (control) { input[count].ki.wVk = VK_CONTROL; input[count++].ki.dwFlags = KEYEVENTF_KEYUP; }
|
||||
if (alt) { input[count].ki.wVk = VK_MENU; input[count++].ki.dwFlags = KEYEVENTF_KEYUP; }
|
||||
for (UINT i = 0; i < count; ++i) input[i].type = INPUT_KEYBOARD;
|
||||
if (SendInput(count, input, sizeof(INPUT)) != count) ExitProcess(3);
|
||||
Sleep(400);
|
||||
}
|
||||
|
||||
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) {
|
||||
(void)instance; (void)previous; (void)args; (void)show;
|
||||
// The runner supplies a fresh prefix; this setting never touches the user's
|
||||
// configuration. Match the reported Force screen reader mode setup.
|
||||
HKEY options;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\GoldWave\\GoldWave\\Options", 0,
|
||||
NULL, 0, KEY_SET_VALUE, NULL, &options, NULL) != ERROR_SUCCESS) return 2;
|
||||
DWORD startupOptions = 0x3008;
|
||||
RegSetValueExW(options, L"StartupOptions", 0, REG_DWORD, (BYTE *)&startupOptions, sizeof(startupOptions));
|
||||
RegCloseKey(options);
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\GoldWave\\GoldWave", 0,
|
||||
NULL, 0, KEY_SET_VALUE, NULL, &options, NULL) != ERROR_SUCCESS) return 2;
|
||||
const WCHAR version[] = L"5.70";
|
||||
RegSetValueExW(options, L"Version", 0, REG_SZ, (const BYTE *)version, sizeof(version));
|
||||
RegCloseKey(options);
|
||||
WCHAR executable[4096] = {0};
|
||||
if (!GetEnvironmentVariableW(L"WINE_ACCESS_GOLDWAVE_EXE", executable, 4096)) return 2;
|
||||
STARTUPINFOW startup = {0};
|
||||
startup.cb = sizeof(startup);
|
||||
PROCESS_INFORMATION process = {0};
|
||||
if (!CreateProcessW(executable, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &startup, &process)) return 2;
|
||||
appPid = process.dwProcessId;
|
||||
for (int i = 0; i < 200 && !app; ++i) { EnumWindows(find_window, 0); Sleep(50); }
|
||||
if (!app) return 2;
|
||||
SetForegroundWindow(app);
|
||||
Sleep(500);
|
||||
key('F', TRUE, FALSE);
|
||||
key(VK_DOWN, FALSE, FALSE);
|
||||
key(VK_DOWN, FALSE, FALSE);
|
||||
key(VK_ESCAPE, FALSE, FALSE);
|
||||
key(VK_ESCAPE, FALSE, FALSE);
|
||||
key('O', FALSE, TRUE);
|
||||
for (int i = 0; i < 100 && !dialog; ++i) { EnumWindows(find_window, 0); Sleep(50); }
|
||||
if (!dialog) return 2;
|
||||
HWND combo = GetDlgItem(dialog, cmb1);
|
||||
SendMessageW(dialog, WM_NEXTDLGCTL, (WPARAM)combo, TRUE);
|
||||
Sleep(500);
|
||||
WCHAR before[512] = {0}, after[512] = {0};
|
||||
SendMessageW(combo, WM_GETTEXT, 512, (LPARAM)before);
|
||||
key(VK_DOWN, FALSE, FALSE);
|
||||
SendMessageW(combo, WM_GETTEXT, 512, (LPARAM)after);
|
||||
if (!lstrcmpW(before, after)) return 4;
|
||||
GUITHREADINFO info = {0};
|
||||
info.cbSize = sizeof(info);
|
||||
GetGUIThreadInfo(GetWindowThreadProcessId(dialog, NULL), &info);
|
||||
if (info.hwndFocus != combo) return 4;
|
||||
key(VK_DOWN, FALSE, FALSE);
|
||||
key(VK_UP, FALSE, FALSE);
|
||||
key(VK_ESCAPE, FALSE, FALSE);
|
||||
PostMessageW(app, WM_CLOSE, 0, 0);
|
||||
int result = WaitForSingleObject(process.hProcess, 3000) == WAIT_OBJECT_0 ? 0 : 2;
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
return result;
|
||||
}
|
||||
@@ -17,24 +17,28 @@ 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", "notepad", "list", "delphi", "winforms", "controls", "scintilla"), default="menu")
|
||||
parser.add_argument("--fixture", choices=("menu", "edit", "notepad", "list", "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 ("list", "delphi", "controls"):
|
||||
parser.error("32-bit checks currently require --fixture list, delphi or controls")
|
||||
if args.is32Bit and args.fixture not in ("menu", "list", "delphi", "controls"):
|
||||
parser.error("32-bit checks currently require --fixture menu, list, 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,
|
||||
*(["--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 [])]))
|
||||
@@ -60,10 +64,14 @@ connection.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus",
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
||||
env = os.environ.copy()
|
||||
env.update(WINEPREFIX=str(Path(temporary) / "prefix"), WINEDEBUG="-all",
|
||||
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.
|
||||
@@ -80,7 +88,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
||||
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 else "x86_64-w64-mingw32-gcc"
|
||||
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"
|
||||
@@ -88,7 +96,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
||||
*(["-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"], check=True)
|
||||
"-luser32", "-lcomctl32", "-loleacc", "-lole32", "-ladvapi32"], check=True)
|
||||
helper = None
|
||||
app = None
|
||||
try:
|
||||
@@ -121,7 +129,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
||||
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"
|
||||
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):
|
||||
@@ -134,6 +142,18 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
||||
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])
|
||||
|
||||
Reference in new issue
Block a user