diff options
Diffstat (limited to 'accessible/tests/browser/windows')
9 files changed, 321 insertions, 11 deletions
diff --git a/accessible/tests/browser/windows/a11y_setup.py b/accessible/tests/browser/windows/a11y_setup.py index d6dc19f0fb..726eea07a4 100644 --- a/accessible/tests/browser/windows/a11y_setup.py +++ b/accessible/tests/browser/windows/a11y_setup.py @@ -9,19 +9,28 @@ import ctypes import os from ctypes import POINTER, byref from ctypes.wintypes import BOOL, HWND, LPARAM, POINT # noqa: F401 +from dataclasses import dataclass +import comtypes.automation import comtypes.client import psutil from comtypes import COMError, IServiceProvider +CHILDID_SELF = 0 +COWAIT_DEFAULT = 0 +EVENT_OBJECT_FOCUS = 0x8005 +GA_ROOT = 2 +NAVRELATION_EMBEDS = 0x1009 +OBJID_CLIENT = -4 +RPC_S_CALLPENDING = -2147417835 +WINEVENT_OUTOFCONTEXT = 0 +WM_CLOSE = 0x0010 + user32 = ctypes.windll.user32 oleacc = ctypes.oledll.oleacc oleaccMod = comtypes.client.GetModule("oleacc.dll") IAccessible = oleaccMod.IAccessible del oleaccMod -OBJID_CLIENT = -4 -CHILDID_SELF = 0 -NAVRELATION_EMBEDS = 0x1009 # This is the path if running locally. ia2Tlb = os.path.join( os.getcwd(), @@ -65,6 +74,13 @@ def AccessibleObjectFromWindow(hwnd, objectID=OBJID_CLIENT): return p +def getWindowClass(hwnd): + MAX_CHARS = 257 + buffer = ctypes.create_unicode_buffer(MAX_CHARS) + user32.GetClassNameW(hwnd, buffer, MAX_CHARS) + return buffer.value + + def getFirefoxHwnd(): """Search all top level windows for the Firefox instance being tested. @@ -78,9 +94,7 @@ def getFirefoxHwnd(): @ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) def callback(hwnd, lParam): - name = ctypes.create_unicode_buffer(100) - user32.GetClassNameW(hwnd, name, 100) - if name.value != "MozillaWindowClass": + if getWindowClass(hwnd) != "MozillaWindowClass": return True pid = ctypes.wintypes.DWORD() user32.GetWindowThreadProcessId(hwnd, byref(pid)) @@ -127,6 +141,106 @@ def findIa2ByDomId(root, id): return descendant +@dataclass +class WinEvent: + event: int + hwnd: int + objectId: int + childId: int + + def getIa2(self): + acc = ctypes.POINTER(IAccessible)() + child = comtypes.automation.VARIANT() + ctypes.oledll.oleacc.AccessibleObjectFromEvent( + self.hwnd, + self.objectId, + self.childId, + ctypes.byref(acc), + ctypes.byref(child), + ) + if child.value != CHILDID_SELF: + # This isn't an IAccessible2 object. + return None + return toIa2(acc) + + +class WaitForWinEvent: + """Wait for a win event, usually for IAccessible2. + This should be used as follows: + 1. Create an instance to wait for the desired event. + 2. Perform the action that should fire the event. + 3. Call wait() on the instance you created in 1) to wait for the event. + """ + + def __init__(self, eventId, match): + """event is the event id to wait for. + match is either None to match any object, an str containing the DOM id + of the desired object, or a function taking a WinEvent which should + return True if this is the requested event. + """ + self._matched = None + # A kernel event used to signal when we get the desired event. + self._signal = ctypes.windll.kernel32.CreateEventW(None, True, False, None) + + # We define this as a nested function because it has to be a static + # function, but we need a reference to self. + @ctypes.WINFUNCTYPE( + None, + ctypes.wintypes.HANDLE, + ctypes.wintypes.DWORD, + ctypes.wintypes.HWND, + ctypes.wintypes.LONG, + ctypes.wintypes.LONG, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ) + def winEventProc(hook, eventId, hwnd, objectId, childId, thread, time): + event = WinEvent(eventId, hwnd, objectId, childId) + if isinstance(match, str): + try: + ia2 = event.getIa2() + if f"id:{match};" in ia2.attributes: + self._matched = event + except (comtypes.COMError, TypeError): + pass + elif callable(match): + try: + if match(event): + self._matched = event + except Exception as e: + self._matched = e + if self._matched: + ctypes.windll.kernel32.SetEvent(self._signal) + + self._hook = user32.SetWinEventHook( + eventId, eventId, None, winEventProc, 0, 0, WINEVENT_OUTOFCONTEXT + ) + # Hold a reference to winEventProc so it doesn't get destroyed. + self._proc = winEventProc + + def wait(self): + """Wait for and return the desired WinEvent.""" + # Pump Windows messages until we get the desired event, which will be + # signalled using a kernel event. + handles = (ctypes.c_void_p * 1)(self._signal) + index = ctypes.wintypes.DWORD() + TIMEOUT = 10000 + try: + ctypes.oledll.ole32.CoWaitForMultipleHandles( + COWAIT_DEFAULT, TIMEOUT, 1, handles, ctypes.byref(index) + ) + except WindowsError as e: + if e.winerror == RPC_S_CALLPENDING: + raise TimeoutError("Timeout before desired event received") + raise + finally: + user32.UnhookWinEvent(self._hook) + self._proc = None + if isinstance(self._matched, Exception): + raise self._matched from self._matched + return self._matched + + def getDocUia(): """Get the IUIAutomationElement for the document being tested.""" # We start with IAccessible2 because there's no efficient way to diff --git a/accessible/tests/browser/windows/ia2/browser.toml b/accessible/tests/browser/windows/ia2/browser.toml index d72b5f8a2d..d6226b73cc 100644 --- a/accessible/tests/browser/windows/ia2/browser.toml +++ b/accessible/tests/browser/windows/ia2/browser.toml @@ -6,4 +6,6 @@ skip-if = [ ] support-files = ["head.js"] +["browser_osPicker.js"] + ["browser_role.js"] diff --git a/accessible/tests/browser/windows/ia2/browser_osPicker.js b/accessible/tests/browser/windows/ia2/browser_osPicker.js new file mode 100644 index 0000000000..b14f2d0a5f --- /dev/null +++ b/accessible/tests/browser/windows/ia2/browser_osPicker.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +addAccessibleTask( + `<input id="file" type="file">`, + async function (browser, docAcc) { + info("Focusing file input"); + await runPython(` + global focused + focused = WaitForWinEvent(EVENT_OBJECT_FOCUS, "file") + `); + const file = findAccessibleChildByID(docAcc, "file"); + file.takeFocus(); + await runPython(` + focused.wait() + `); + ok(true, "file input got focus"); + info("Opening file picker"); + await runPython(` + global focused + focused = WaitForWinEvent( + EVENT_OBJECT_FOCUS, + lambda evt: getWindowClass(evt.hwnd) == "Edit" + ) + `); + file.doAction(0); + await runPython(` + global event + event = focused.wait() + `); + ok(true, "Picker got focus"); + info("Dismissing picker"); + await runPython(` + # If the picker is dismissed too quickly, it seems to re-enable the root + # window before we do. This sleep isn't ideal, but it's more likely to + # reproduce the case that our root window gets focus before it is enabled. + # See bug 1883568 for further details. + import time + time.sleep(1) + focused = WaitForWinEvent(EVENT_OBJECT_FOCUS, "file") + # Sending key presses to the picker is unreliable, so use WM_CLOSE. + pickerRoot = user32.GetAncestor(event.hwnd, GA_ROOT) + user32.SendMessageW(pickerRoot, WM_CLOSE, 0, 0) + focused.wait() + `); + ok(true, "file input got focus"); + } +); diff --git a/accessible/tests/browser/windows/ia2/browser_role.js b/accessible/tests/browser/windows/ia2/browser_role.js index 08e44c280f..89b560ab49 100644 --- a/accessible/tests/browser/windows/ia2/browser_role.js +++ b/accessible/tests/browser/windows/ia2/browser_role.js @@ -12,7 +12,7 @@ addAccessibleTask( ` <p id="p">p</p> `, - async function (browser, docAcc) { + async function () { let role = await runPython(` global doc doc = getDocIa2() diff --git a/accessible/tests/browser/windows/uia/browser.toml b/accessible/tests/browser/windows/uia/browser.toml index f7974d69c7..d1513c1822 100644 --- a/accessible/tests/browser/windows/uia/browser.toml +++ b/accessible/tests/browser/windows/uia/browser.toml @@ -9,3 +9,5 @@ support-files = ["head.js"] ["browser_controlType.js"] ["browser_elementFromPoint.js"] + +["browser_tree.js"] diff --git a/accessible/tests/browser/windows/uia/browser_controlType.js b/accessible/tests/browser/windows/uia/browser_controlType.js index 16db892581..3bb994f437 100644 --- a/accessible/tests/browser/windows/uia/browser_controlType.js +++ b/accessible/tests/browser/windows/uia/browser_controlType.js @@ -9,11 +9,11 @@ const UIA_ButtonControlTypeId = 50000; const UIA_DocumentControlTypeId = 50030; /* eslint-enable camelcase */ -addAccessibleTask( +addUiaTask( ` <button id="button">button</button> `, - async function (browser, docAcc) { + async function () { let controlType = await runPython(` global doc doc = getDocUia() diff --git a/accessible/tests/browser/windows/uia/browser_elementFromPoint.js b/accessible/tests/browser/windows/uia/browser_elementFromPoint.js index e2fda4ab30..acf6fe91c7 100644 --- a/accessible/tests/browser/windows/uia/browser_elementFromPoint.js +++ b/accessible/tests/browser/windows/uia/browser_elementFromPoint.js @@ -4,12 +4,12 @@ "use strict"; -addAccessibleTask( +addUiaTask( ` <button id="button">button</p> <a id="a" href="#">a</a> `, - async function (browser, docAcc) { + async function () { ok( await runPython(` global doc diff --git a/accessible/tests/browser/windows/uia/browser_tree.js b/accessible/tests/browser/windows/uia/browser_tree.js new file mode 100644 index 0000000000..778609bedb --- /dev/null +++ b/accessible/tests/browser/windows/uia/browser_tree.js @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +async function testIsControl(pyVar, isControl) { + const result = await runPython(`bool(${pyVar}.CurrentIsControlElement)`); + if (isControl) { + ok(result, `${pyVar} is a control element`); + } else { + ok(!result, `${pyVar} isn't a control element`); + } +} + +/** + * Define a global Python variable and assign it to a given Python expression. + */ +function definePyVar(varName, expression) { + return runPython(` + global ${varName} + ${varName} = ${expression} + `); +} + +/** + * Get the UIA element with the given id and assign it to a global Python + * variable using the id as the variable name. + */ +function assignPyVarToUiaWithId(id) { + return definePyVar(id, `findUiaByDomId(doc, "${id}")`); +} + +addUiaTask( + ` +<p id="p">paragraph</p> +<div id="div">div</div> +<!-- The spans are because the UIA -> IA2 proxy seems to remove a single text + leaf child from even the raw tree. + --> +<a id="link" href="#">link<span> </span>></a> +<h1 id="h1">h1<span> </span></h1> +<h1 id="h1WithDiv"><div>h1 with div<span> </span></div></h1> +<input id="range" type="range"> +<div onclick=";" id="clickable">clickable</div> +<div id="editable" contenteditable>editable</div> +<table id="table"><tr><th>th</th></tr></table> + `, + async function (browser, docAcc) { + await definePyVar("doc", `getDocUia()`); + await assignPyVarToUiaWithId("p"); + await testIsControl("p", false); + await definePyVar( + "pTextLeaf", + `uiaClient.RawViewWalker.GetFirstChildElement(p)` + ); + await testIsControl("pTextLeaf", true); + await assignPyVarToUiaWithId("div"); + await testIsControl("div", false); + await definePyVar( + "divTextLeaf", + `uiaClient.RawViewWalker.GetFirstChildElement(div)` + ); + await testIsControl("divTextLeaf", true); + await assignPyVarToUiaWithId("link"); + await testIsControl("link", true); + await assignPyVarToUiaWithId("range"); + await testIsControl("range", true); + await assignPyVarToUiaWithId("editable"); + await testIsControl("editable", true); + await assignPyVarToUiaWithId("table"); + await testIsControl("table", true); + if (!gIsUiaEnabled) { + // The remaining tests are broken with the UIA -> IA2 proxy. + return; + } + await definePyVar( + "linkTextLeaf", + `uiaClient.RawViewWalker.GetFirstChildElement(link)` + ); + await testIsControl("linkTextLeaf", false); + await assignPyVarToUiaWithId("h1"); + await testIsControl("h1", true); + await definePyVar( + "h1TextLeaf", + `uiaClient.RawViewWalker.GetFirstChildElement(h1)` + ); + await testIsControl("h1TextLeaf", false); + await assignPyVarToUiaWithId("h1WithDiv"); + await testIsControl("h1WithDiv", true); + // h1WithDiv's text leaf is its grandchild. + await definePyVar( + "h1WithDivTextLeaf", + `uiaClient.RawViewWalker.GetFirstChildElement( + uiaClient.RawViewWalker.GetFirstChildElement( + h1WithDiv + ) + )` + ); + await testIsControl("h1WithDivTextLeaf", false); + await assignPyVarToUiaWithId("clickable"); + await testIsControl("clickable", true); + } +); diff --git a/accessible/tests/browser/windows/uia/head.js b/accessible/tests/browser/windows/uia/head.js index afc50984bd..e659354c7c 100644 --- a/accessible/tests/browser/windows/uia/head.js +++ b/accessible/tests/browser/windows/uia/head.js @@ -4,6 +4,8 @@ "use strict"; +/* exported gIsUiaEnabled, addUiaTask */ + // Load the shared-head file first. Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", @@ -16,3 +18,38 @@ loadScripts( { name: "common.js", dir: MOCHITESTS_DIR }, { name: "promisified-events.js", dir: MOCHITESTS_DIR } ); + +let gIsUiaEnabled = false; + +/** + * This is like addAccessibleTask, but takes two additional boolean options: + * - uiaEnabled: Whether to run a variation of this test with Gecko UIA enabled. + * - uiaDisabled: Whether to run a variation of this test with UIA disabled. In + * this case, UIA will rely entirely on the IA2 -> UIA proxy. + * If both are set, the test will be run twice with different configurations. + * You can determine which variant is currently running using the gIsUiaEnabled + * variable. This is useful for conditional tests; e.g. if Gecko UIA supports + * something that the IA2 -> UIA proxy doesn't support. + */ +function addUiaTask(doc, task, options = {}) { + const { uiaEnabled = true, uiaDisabled = true } = options; + + function addTask(shouldEnable) { + async function uiaTask(browser, docAcc, topDocAcc) { + await SpecialPowers.pushPrefEnv({ + set: [["accessibility.uia.enable", shouldEnable]], + }); + gIsUiaEnabled = shouldEnable; + info(shouldEnable ? "Gecko UIA enabled" : "Gecko UIA disabled"); + await task(browser, docAcc, topDocAcc); + } + addAccessibleTask(doc, uiaTask, options); + } + + if (uiaEnabled) { + addTask(true); + } + if (uiaDisabled) { + addTask(false); + } +} |