summaryrefslogtreecommitdiffstats
path: root/accessible/tests/browser/windows
diff options
context:
space:
mode:
Diffstat (limited to 'accessible/tests/browser/windows')
-rw-r--r--accessible/tests/browser/windows/a11y_setup.py126
-rw-r--r--accessible/tests/browser/windows/ia2/browser.toml2
-rw-r--r--accessible/tests/browser/windows/ia2/browser_osPicker.js51
-rw-r--r--accessible/tests/browser/windows/ia2/browser_role.js2
-rw-r--r--accessible/tests/browser/windows/uia/browser.toml2
-rw-r--r--accessible/tests/browser/windows/uia/browser_controlType.js4
-rw-r--r--accessible/tests/browser/windows/uia/browser_elementFromPoint.js4
-rw-r--r--accessible/tests/browser/windows/uia/browser_tree.js104
-rw-r--r--accessible/tests/browser/windows/uia/head.js37
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);
+ }
+}