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:
1 parent
38358ad27a
commit
e0c83859f5
13 files changed
+753
-11
No files matched your search
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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])
|
||||
|
||||
Reference in new issue
Block a user