Battle.net installer now working. It crashes when the login page loads, but that seems to be fixed in upcoming version and the controls did read.

This commit is contained in:
Storm Dragon committed 2026-10-16 12:26:59 -04:00
1 parent 38358ad27a
commit e0c83859f5
13 files changed
+753 -11

No files matched your search

+47
View File
@@ -41,6 +41,53 @@ Check the runner without Wine:
python3 tests/wine_test_environment_test.py
```
## Battle.net bootstrapper controls
```sh
xvfb-run -a python3 tests/menu_integration.py --build-dir build --fixture bootstrapper --32-bit
xvfb-run -a python3 tests/menu_integration.py --build-dir build --fixture bootstrapper
```
Run these sequentially. The fixture supplies an actual custom UIA provider
without native child controls. A private recording reader verifies language
value changes, Continue focus despite a radio item retaining its focus flag,
return to the selector, duplicate suppression, background exclusion and return,
and exclusion of protected or unknown-protection values before querying them.
The real native input hook also reads a Toggle provider whose state is absent
from the provider's simple properties. It checks unchecked, checked and mixed
states, changes without focus movement, focus return and background exclusion.
The fixture fails if the helper invokes Toggle rather than reading its state.
The test does not use the real installer or send speech to the running reader.
Read-only inspection on October 16 retrieved the actual running setup's
`Select a Language, combo box, English (United States)` and `Continue, push
button` through the new query code during Tab navigation. This verifies the
query path, not installed-helper speech or physical Braille.
A later read-only native probe on the second page retrieved the startup
checkbox's real Toggle state while Wine's client state query returned
`E_NOTIMPL`. The first installed build was confirmed by the user to read the
language page and allow progress to the second screen; audible checkbox-state
acceptance remains separate.
After building/installing Wine Access, restart Cthulhu and reopen the wizard
to load the new native hook. Return to the
wizard on workspace 2. Tab/Shift+Tab should distinguish the language selector
from Continue. With the selector focused, Down/Up should announce the current
language; restore the intended language afterward. Pause to check for repeated
announcements, then switch to Linux and back to check background silence and
focus recovery. Only activate Continue when ready to advance the installer;
later pages and the launcher remain unverified. Check ordinary Wine Notepad
typing/arrows, menus/Escape, native combo-box arrows and reader shutdown for
regressions. Verify audible speech and physical Braille separately.
On the second page, Tab to `Launch Battle.net when you start your computer`:
expect its current checked/unchecked state. Press Space twice and verify each
new state immediately, leaving the original setting restored. Tab away and
Shift+Tab back should retain the correct state. Switching away must silence
background changes; returning should reestablish the state. Choose the desired
startup setting before continuing installation.
## UIA notifications
UIA notification routing uses the actual native application hooks and a private
+143
View File
@@ -0,0 +1,143 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include <windows.h>
#include <uiautomation.h>
#include <cstdio>
static HWND window;
static int focusedIndex = 1, selectedIndex = 3, step = 0;
static ToggleState checkboxState = ToggleState_Off;
static bool checkboxEnabled = true;
class Provider : public IRawElementProviderSimple,
public IRawElementProviderFragment, public IRawElementProviderFragmentRoot, public IToggleProvider {
public:
int index;
explicit Provider(int value) : index(value) {}
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void **out) override {
*out = nullptr;
if (IsEqualIID(iid, IID_IUnknown) || IsEqualIID(iid, IID_IRawElementProviderSimple))
*out = static_cast<IRawElementProviderSimple *>(this);
else if (IsEqualIID(iid, IID_IRawElementProviderFragment))
*out = static_cast<IRawElementProviderFragment *>(this);
else if (index == 0 && IsEqualIID(iid, IID_IRawElementProviderFragmentRoot))
*out = static_cast<IRawElementProviderFragmentRoot *>(this);
else if (index == 7 && IsEqualIID(iid, IID_IToggleProvider))
*out = static_cast<IToggleProvider *>(this);
return *out ? S_OK : E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE AddRef() override { return 2; }
ULONG STDMETHODCALLTYPE Release() override { return 1; }
HRESULT STDMETHODCALLTYPE get_ProviderOptions(ProviderOptions *out) override {
*out = ProviderOptions_ServerSideProvider; return S_OK;
}
HRESULT STDMETHODCALLTYPE GetPatternProvider(PATTERNID id, IUnknown **out) override {
*out = index == 7 && id == UIA_TogglePatternId ? static_cast<IToggleProvider *>(this) : nullptr;
return S_OK;
}
HRESULT STDMETHODCALLTYPE Toggle() override { ExitProcess(9); }
HRESULT STDMETHODCALLTYPE get_ToggleState(ToggleState *out) override { *out = checkboxState; return S_OK; }
HRESULT STDMETHODCALLTYPE GetPropertyValue(PROPERTYID id, VARIANT *out) override {
VariantInit(out);
static const WCHAR *names[] = {L"Bootstrapper fixture", L"Select a Language", L"Continue",
L"English (United States)", L"French", L"Password", L"Unknown protection", L"Launch on startup"};
if (id == UIA_NamePropertyId) {
out->vt = VT_BSTR; out->bstrVal = SysAllocString(names[index]);
} else if (id == UIA_ControlTypePropertyId) {
out->vt = VT_I4;
out->lVal = index == 7 ? UIA_CheckBoxControlTypeId : index == 0 ? UIA_WindowControlTypeId : index == 1 ? UIA_GroupControlTypeId :
index == 2 ? UIA_ButtonControlTypeId : index < 5 ? UIA_RadioButtonControlTypeId : UIA_EditControlTypeId;
} else if (id == UIA_HasKeyboardFocusPropertyId || id == UIA_IsKeyboardFocusablePropertyId ||
id == UIA_IsOffscreenPropertyId || id == UIA_IsEnabledPropertyId ||
id == UIA_IsPasswordPropertyId) {
if (id == UIA_IsPasswordPropertyId && index == 6) return S_OK;
out->vt = VT_BOOL;
bool value = id == UIA_HasKeyboardFocusPropertyId ? (index == focusedIndex || index == selectedIndex) :
id == UIA_IsKeyboardFocusablePropertyId ? index != 0 :
id == UIA_IsOffscreenPropertyId ? false :
id == UIA_IsPasswordPropertyId ? index == 5 : index != 7 || checkboxEnabled;
out->boolVal = value ? VARIANT_TRUE : VARIANT_FALSE;
} else if (id == UIA_ValueValuePropertyId && (index == 5 || index == 6)) {
// Protected and unknown-protection values must never even be queried.
ExitProcess(8);
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE get_HostRawElementProvider(IRawElementProviderSimple **out) override {
*out = nullptr; return index == 0 ? UiaHostProviderFromHwnd(window, out) : S_OK;
}
HRESULT STDMETHODCALLTYPE Navigate(NavigateDirection direction, IRawElementProviderFragment **out) override;
HRESULT STDMETHODCALLTYPE GetRuntimeId(SAFEARRAY **out) override {
*out = nullptr;
if (index == 0) return S_OK;
*out = SafeArrayCreateVector(VT_I4, 0, 2);
LONG position = 0, value = UiaAppendRuntimeId;
SafeArrayPutElement(*out, &position, &value);
position = 1; value = index; SafeArrayPutElement(*out, &position, &value); return S_OK;
}
HRESULT STDMETHODCALLTYPE get_BoundingRectangle(UiaRect *out) override {
*out = {100, 100.0 + index * 20, 200, 20}; return S_OK;
}
HRESULT STDMETHODCALLTYPE GetEmbeddedFragmentRoots(SAFEARRAY **out) override { *out = nullptr; return S_OK; }
HRESULT STDMETHODCALLTYPE SetFocus() override { return E_NOTIMPL; }
HRESULT STDMETHODCALLTYPE get_FragmentRoot(IRawElementProviderFragmentRoot **out) override;
HRESULT STDMETHODCALLTYPE ElementProviderFromPoint(double, double, IRawElementProviderFragment **out) override {
*out = nullptr; return S_OK;
}
HRESULT STDMETHODCALLTYPE GetFocus(IRawElementProviderFragment **out) override;
};
static Provider providers[] = {Provider(0), Provider(1), Provider(2), Provider(3), Provider(4), Provider(5), Provider(6), Provider(7)};
HRESULT Provider::Navigate(NavigateDirection direction, IRawElementProviderFragment **out) {
*out = nullptr;
if (direction == NavigateDirection_Parent && index) *out = &providers[0];
else if (direction == NavigateDirection_FirstChild && index == 0) *out = &providers[1];
else if (direction == NavigateDirection_LastChild && index == 0) *out = &providers[7];
else if (direction == NavigateDirection_NextSibling && index > 0 && index < 7) *out = &providers[index + 1];
else if (direction == NavigateDirection_PreviousSibling && index > 1) *out = &providers[index - 1];
return S_OK;
}
HRESULT Provider::get_FragmentRoot(IRawElementProviderFragmentRoot **out) { *out = &providers[0]; return S_OK; }
HRESULT Provider::GetFocus(IRawElementProviderFragment **out) { *out = &providers[focusedIndex]; return S_OK; }
static LRESULT CALLBACK window_proc(HWND hwnd, UINT message, WPARAM wp, LPARAM lp) {
if (message == WM_GETOBJECT && static_cast<LONG>(lp) == UiaRootObjectId)
return UiaReturnRawElementProvider(hwnd, wp, lp, &providers[0]);
if (message == WM_TIMER) {
switch (++step) {
case 1: break;
case 2: selectedIndex = 4; break;
case 3: focusedIndex = 2; break;
case 4: focusedIndex = 1; break;
case 5: focusedIndex = 5; break;
case 6: focusedIndex = 6; break;
case 7: focusedIndex = 1; selectedIndex = 3; break;
case 8: focusedIndex = 7; break;
case 9: checkboxState = ToggleState_On; break;
case 10: checkboxState = ToggleState_Off; break;
case 11: checkboxState = ToggleState_Indeterminate; checkboxEnabled = false; break;
case 12: focusedIndex = 2; break;
case 13: focusedIndex = 7; break;
case 14: ShowWindow(hwnd, SW_HIDE); checkboxState = ToggleState_On; checkboxEnabled = true; break;
case 15: ShowWindow(hwnd, SW_SHOW); SetForegroundWindow(hwnd); SetFocus(hwnd); break;
default: DestroyWindow(hwnd); break;
}
return 0;
}
if (message == WM_DESTROY) { PostQuitMessage(0); return 0; }
return DefWindowProcW(hwnd, message, wp, lp);
}
int WINAPI WinMain(HINSTANCE instance, HINSTANCE, LPSTR, int) {
if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED))) return 1;
WNDCLASSW cls{};
cls.hInstance = instance; cls.lpfnWndProc = window_proc; cls.lpszClassName = L"Blizzard Bootstrapper";
RegisterClassW(&cls);
window = CreateWindowW(cls.lpszClassName, L"Bootstrapper fixture", WS_OVERLAPPEDWINDOW,
100, 100, 400, 300, nullptr, nullptr, instance, nullptr);
ShowWindow(window, SW_SHOW); SetForegroundWindow(window); SetFocus(window);
SetTimer(window, 1, 800, nullptr);
MSG message{};
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
TranslateMessage(&message); DispatchMessageW(&message);
}
CoUninitialize();
return 0;
}
+47 -5
View File
@@ -20,7 +20,7 @@ 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("--fixture", choices=("menu", "edit", "notepad", "list", "listbox", "delphi", "winforms", "controls", "scintilla", "goldwave", "bootstrapper"), 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")
@@ -28,8 +28,8 @@ 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.is32Bit and args.fixture not in ("menu", "edit", "winforms", "list", "listbox", "delphi", "controls", "bootstrapper"):
parser.error("32-bit checks currently require --fixture menu, edit, winforms, list, listbox, delphi, controls or bootstrapper")
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()):
@@ -108,11 +108,16 @@ with wine_test_directory(prefix="wine-access-menus-") as temporary:
sourceName = "edit" if args.fixture == "winforms" else args.fixture
if args.fixture == "delphi":
sourceName = "list"
isBootstrapper = args.fixture == "bootstrapper"
if isBootstrapper:
compiler = compiler.replace("gcc", "g++")
subprocess.run([compiler, "-Wall", "-Wextra", "-mwindows",
*(["-static"] if isBootstrapper else []),
*(["-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)
"-o", str(fixture), str(Path(__file__).with_name(sourceName + ("_fixture.cpp" if isBootstrapper else "_fixture.c"))),
"-luser32", "-lcomctl32", "-loleacc", "-lole32", "-ladvapi32",
*(["-luiautomationcore", "-loleaut32", "-luuid"] if isBootstrapper else [])], check=True)
helper = None
app = None
try:
@@ -160,6 +165,43 @@ with wine_test_directory(prefix="wine-access-menus-") as temporary:
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 == "bootstrapper":
uiaEvents = [e for e in events if ":bootstrapper-uia:" in e[0]]
for event in uiaEvents:
print(event[1:9])
# Focus can arrive before the native hook's state reply. It must
# then be followed by a known state for that same focused source.
for index, event in enumerate(uiaEvents):
if event[3] == "check box" and not event[5]:
assert event[1] == "focus", f"lost a previously known checkbox state: {event!r}"
assert index + 1 < len(uiaEvents) and uiaEvents[index + 1][0] == event[0] and uiaEvents[index + 1][5], \
f"checkbox state never arrived after focus: {uiaEvents[index:]!r}"
knownEvents = [e for e in uiaEvents if e[3] != "check box" or e[5]]
assert [e[2:5] for e in knownEvents] == [
("Select a Language", "combo box", "English (United States)"),
("Select a Language", "combo box", "French"),
("Continue", "push button", ""),
("Select a Language", "combo box", "French"),
("Password", "edit", ""),
("Unknown protection", "edit", ""),
("Select a Language", "combo box", "English (United States)"),
("Launch on startup", "check box", ""),
("Launch on startup", "check box", ""),
("Launch on startup", "check box", ""),
("Launch on startup", "check box", ""),
("Continue", "push button", ""),
("Launch on startup", "check box", ""),
("Launch on startup", "check box", ""),
], f"missing, repeated or background bootstrapper feedback: {uiaEvents!r}"
assert [e[1] for e in knownEvents[:7]] == ["focus", "value"] + ["focus"] * 5
checkboxEvents = [e for e in knownEvents if e[3] == "check box"]
assert [e[1] for e in checkboxEvents[1:4]] == ["state"] * 3
assert [e[5] for e in checkboxEvents] == [
"not checked", "checked", "not checked", "half checked, unavailable", "half checked, unavailable", "checked"]
assert "password" in uiaEvents[4][5], "protected control lost its state"
assert len({e[0] for e in uiaEvents if e[3] == "combo box"}) == 1
print("PASS: UIA selector values, native Toggle checked/unchecked/mixed, focus/return, deduplication, protected/unknown values, background exclusion")
sys.exit(0)
if args.fixture == "goldwave":
for event in events:
print(event[1:9])