зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
bd293df36c
Коммит
232e8c986c
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче