Bug 1852190 part 3: Add Windows MSAA/IA2 interfaces, constants and utility functions to the Python environment. r=eeejay,jmaher

This also adds a simple role test which shows all of this working.

Differential Revision: https://phabricator.services.mozilla.com/D187746
This commit is contained in:
James Teh 2023-10-04 23:58:45 +00:00
Родитель bd293df36c
Коммит 232e8c986c
12 изменённых файлов: 254 добавлений и 2 удалений

Просмотреть файл

@ -48,6 +48,7 @@ BROWSER_CHROME_MANIFESTS += [
"tests/browser/telemetry/browser.toml",
"tests/browser/text/browser.toml",
"tests/browser/tree/browser.toml",
"tests/browser/windows/ia2/browser.toml",
]
with Files("**"):

Просмотреть файл

@ -7,6 +7,8 @@ support-files = [
"head.js",
"python_runner_wsh.py",
"shared-head.js",
"windows/a11y_setup.py",
"windows/a11y_setup_requirements.txt",
]
prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"]

Просмотреть файл

@ -9,6 +9,8 @@ It is intended to be called from JS browser tests.
"""
import json
import os
import sys
import traceback
from mod_pywebsocket import msgutil
@ -23,9 +25,27 @@ def web_socket_transfer_data(request):
"""Send a response to the client as a JSON array."""
msgutil.send_message(request, json.dumps(args))
# XXX Anything that should be accessible to code run
# via the WebSocket should be added to cleanNamespace.
cleanNamespace = {}
testDir = None
if sys.platform == "win32":
testDir = "windows"
elif sys.platform == "linux":
# XXX ATK code goes here.
pass
if testDir:
sys.path.append(
os.path.join(
os.getcwd(), "browser", "accessible", "tests", "browser", testDir
)
)
try:
import a11y_setup
cleanNamespace = a11y_setup.__dict__
setupExc = None
except Exception:
setupExc = traceback.format_exc()
sys.path.pop()
def info(message):
"""Log an info message."""
@ -43,6 +63,11 @@ def web_socket_transfer_data(request):
namespace = cleanNamespace.copy()
continue
if setupExc:
# a11y_setup failed. Report an exception immediately.
send("exception", setupExc)
continue
# Wrap the code in a function called run(). This allows the code to
# return a result by simply using the return statement.
if "\n" not in code and not code.lstrip().startswith("return "):

Просмотреть файл

@ -935,6 +935,8 @@ let gPythonSocket = null;
* be resolved with the deserialized result. If the Python code raises an
* exception, the JS Promise will be rejected with the Python traceback.
* An info() function is provided in Python to log an info message.
* See windows/a11y_setup.py for other things available in the Python
* environment.
*/
function runPython(code) {
if (!gPythonSocket) {

Просмотреть файл

@ -0,0 +1,116 @@
# 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/.
"""Python environment for Windows a11y browser tests.
"""
import ctypes
import os
from ctypes import POINTER, byref
from ctypes.wintypes import BOOL, HWND, LPARAM
import comtypes.client
import psutil
from comtypes import COMError, IServiceProvider
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(),
"..",
"..",
"..",
"accessible",
"interfaces",
"ia2",
"IA2Typelib.tlb",
)
if not os.path.isfile(ia2Tlb):
# This is the path if running in CI.
ia2Tlb = os.path.join(os.getcwd(), "ia2Typelib.tlb")
ia2Mod = comtypes.client.GetModule(ia2Tlb)
del ia2Tlb
# Shove all the IAccessible* interfaces and IA2_* constants directly
# into our namespace for convenience.
globals().update((k, getattr(ia2Mod, k)) for k in ia2Mod.__all__)
# We use this below. The linter doesn't understand our globals() update hack.
IAccessible2 = ia2Mod.IAccessible2
del ia2Mod
def AccessibleObjectFromWindow(hwnd, objectID=OBJID_CLIENT):
p = POINTER(IAccessible)()
oleacc.AccessibleObjectFromWindow(
hwnd, objectID, byref(IAccessible._iid_), byref(p)
)
return p
def getFirefoxHwnd():
"""Search all top level windows for the Firefox instance being
tested.
We search by window class name and window title prefix.
"""
# We can compare the grandparent process ids to find the Firefox started by
# the test harness.
commonPid = psutil.Process().parent().ppid()
# We need something mutable to store the result from the callback.
found = []
@ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
def callback(hwnd, lParam):
name = ctypes.create_unicode_buffer(100)
user32.GetClassNameW(hwnd, name, 100)
if name.value != "MozillaWindowClass":
return True
pid = ctypes.wintypes.DWORD()
user32.GetWindowThreadProcessId(hwnd, byref(pid))
if psutil.Process(pid.value).parent().ppid() != commonPid:
return True # Not the Firefox being tested.
found.append(hwnd)
return False
user32.EnumWindows(callback, LPARAM(0))
if not found:
raise LookupError("Couldn't find Firefox HWND")
return found[0]
def toIa2(obj):
serv = obj.QueryInterface(IServiceProvider)
return serv.QueryService(IAccessible2._iid_, IAccessible2)
def getDocIa2():
"""Get the IAccessible2 for the document being tested."""
hwnd = getFirefoxHwnd()
root = AccessibleObjectFromWindow(hwnd)
doc = root.accNavigate(NAVRELATION_EMBEDS, 0)
try:
child = toIa2(doc.accChild(1))
if "id:default-iframe-id;" in child.attributes:
# This is an iframe or remoteIframe test.
doc = child.accChild(1)
except COMError:
pass # No child.
return toIa2(doc)
def findIa2ByDomId(root, id):
search = f"id:{id};"
# Child ids begin at 1.
for i in range(1, root.accChildCount + 1):
child = toIa2(root.accChild(i))
if search in child.attributes:
return child
descendant = findIa2ByDomId(child, id)
if descendant:
return descendant

Просмотреть файл

@ -0,0 +1 @@
comtypes==1.2.0

Просмотреть файл

@ -0,0 +1,9 @@
[DEFAULT]
subsuite = "a11y"
skip-if = [
"os != 'win'",
"headless",
]
support-files = ["head.js"]
["browser_role.js"]

Просмотреть файл

@ -0,0 +1,39 @@
/* 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";
const ROLE_SYSTEM_DOCUMENT = 15;
const ROLE_SYSTEM_GROUPING = 20;
const IA2_ROLE_PARAGRAPH = 1054;
addAccessibleTask(
`
<p id="p">p</p>
`,
async function (browser, docAcc) {
let role = await runPython(`
global doc
doc = getDocIa2()
return doc.accRole(CHILDID_SELF)
`);
is(role, ROLE_SYSTEM_DOCUMENT, "doc has correct MSAA role");
role = await runPython(`doc.role()`);
is(role, ROLE_SYSTEM_DOCUMENT, "doc has correct IA2 role");
ok(
await runPython(`
global p
p = findIa2ByDomId(doc, "p")
firstChild = toIa2(doc.accChild(1))
return p == firstChild
`),
"doc's first child is p"
);
role = await runPython(`p.accRole(CHILDID_SELF)`);
is(role, ROLE_SYSTEM_GROUPING, "p has correct MSAA role");
role = await runPython(`p.role()`);
is(role, IA2_ROLE_PARAGRAPH, "p has correct IA2 role");
},
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
);

Просмотреть файл

@ -0,0 +1,18 @@
/* 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";
// Load the shared-head file first.
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
this
);
// Loading and common.js from accessible/tests/mochitest/ for all tests, as
// well as promisified-events.js.
loadScripts(
{ name: "common.js", dir: MOCHITESTS_DIR },
{ name: "promisified-events.js", dir: MOCHITESTS_DIR }
);

Просмотреть файл

@ -287,6 +287,13 @@ ARCHIVE_FILES = {
"pattern": "specialpowers/**",
"dest": "mochitest/extensions",
},
# Needed by Windows a11y browser tests.
{
"source": buildconfig.topobjdir,
"base": "accessible/interfaces/ia2",
"pattern": "IA2Typelib.tlb",
"dest": "mochitest",
},
],
"mozharness": [
{

Просмотреть файл

@ -403,6 +403,20 @@ def run_mochitest_general(
# reason it doesn't get set when calling `activate_this.py` in the virtualenv.
sys.executable = command_context.virtualenv_manager.python_path
if ("browser-chrome", "a11y") in suites and sys.platform == "win32":
# Only Windows a11y browser tests need this.
req = os.path.join(
"accessible",
"tests",
"browser",
"windows",
"a11y_setup_requirements.txt",
)
command_context.virtualenv_manager.activate()
command_context.virtualenv_manager.install_pip_requirements(
req, require_hashes=False
)
# This is a hack to introduce an option in mach to not send
# filtered tests to the mochitest harness. Mochitest harness will read
# the master manifest in that case.

Просмотреть файл

@ -527,6 +527,24 @@ class DesktopUnittest(TestingMixin, MercurialScript, MozbaseMixin, CodeCoverageM
)
)
if (
self._query_specified_suites("mochitest", "mochitest-browser-a11y")
is not None
and sys.platform == "win32"
):
# Only Windows a11y browser tests need this.
requirements_files.append(
os.path.join(
dirs["abs_mochitest_dir"],
"browser",
"accessible",
"tests",
"browser",
"windows",
"a11y_setup_requirements.txt",
)
)
for requirements_file in requirements_files:
self.register_virtualenv_module(
requirements=[requirements_file], two_pass=True