"
+}
+
+@pytest.mark.parametrize("test_html", TEST_HTML.values(), ids=TEST_HTML.keys())
+def test_atspi(atspi, session, inline, test_html):
+ session.url = inline(test_html)
+
+ # Spec:
+ # Role: ROLE_TOGGLE_BUTTON
+
+ node = atspi.find_node("test", session.url)
+ assert atspi.Accessible.get_role(node) == atspi.Role.TOGGLE_BUTTON
+
+# @pytest.mark.parametrize("test_html", TEST_HTML.values(), ids=TEST_HTML.keys())
+# def test_axapi(axapi, session, inline, test_html):
+# session.url = inline(test_html)
+#
+# # Spec:
+# # AXRole: AXCheckBox
+# # AXSubrole: AXToggle
+
+# @pytest.mark.parametrize("test_html", TEST_HTML.values(), ids=TEST_HTML.keys())
+# def test_ia2(ia2, session, inline, test_html):
+# session.url = inline(test_html)
+#
+# # Spec:
+# # Role: ROLE_SYSTEM_PUSHBUTTON
+# # Role: IA2_ROLE_TOGGLE_BUTTON
+
+# @pytest.mark.parametrize("test_html", TEST_HTML.values(), ids=TEST_HTML.keys())
+# def test_uia(uia, session, inline, test_html):
+# session.url = inline(test_html)
+#
+# # Spec:
+# # Control Type: Button
diff --git a/core-aam/aamtests/support/__init__.py b/core-aam/aamtests/support/__init__.py
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/core-aam/aamtests/support/api_wrapper.py b/core-aam/aamtests/support/api_wrapper.py
new file mode 100644
index 00000000000000..3bec6f23489919
--- /dev/null
+++ b/core-aam/aamtests/support/api_wrapper.py
@@ -0,0 +1,53 @@
+import abc
+import time
+from typing import Any, Callable, Generic, Optional, TypeVar
+
+ApiNode = TypeVar('ApiNode')
+PollResult = TypeVar('PollResult')
+
+class ApiWrapper(Generic[ApiNode], abc.ABC):
+ def __init__(self, pid: int, product_name: str, timeout: float) -> None:
+ """Setup for accessibility API testing.
+
+ :pid: The PID of the process which exposes the accessibility API.
+ :product_name: The name of the browser, used to find the browser in the accessibility API.
+ :timeout: The timeout the test harness has set for this test, local timeouts can be set based on it.
+ """
+ self.product_name: str = product_name
+ self.pid: int = pid
+ self.root: Optional[Any] = None
+ self.document: Optional[ApiNode] = None
+ self.test_url: Optional[str] = None
+ self.timeout: float = timeout
+
+ self.root = self._find_browser()
+
+ if not self.root:
+ raise Exception(
+ f"Couldn't find browser {self.product_name} in accessibility API {self.api_name}."
+ )
+
+ @property
+ @abc.abstractmethod
+ def api_name(self) -> str:
+ pass
+
+ @abc.abstractmethod
+ def _find_browser(self) -> Optional[ApiNode]:
+ pass
+
+ def _poll_for(self, find: Callable[[], Optional[PollResult]], error: str) -> PollResult:
+ """Poll until the `find` function returns something.
+
+ :param url: The url of the test.
+ :return: Whatever find returns.
+ """
+ found = find()
+ stop = time.time() + self.timeout
+ while not found:
+ if time.time() > stop:
+ raise TimeoutError(error)
+ time.sleep(0.01)
+ found = find()
+
+ return found
diff --git a/core-aam/aamtests/support/atspi_wrapper.py b/core-aam/aamtests/support/atspi_wrapper.py
new file mode 100644
index 00000000000000..145b2b647ae3cd
--- /dev/null
+++ b/core-aam/aamtests/support/atspi_wrapper.py
@@ -0,0 +1,166 @@
+from __future__ import annotations
+from typing import Any, Optional, List, Dict
+
+import gi
+
+gi.require_version("Atspi", "2.0")
+from gi.repository import Atspi
+
+from .api_wrapper import ApiWrapper
+
+
+class AtspiWrapper(ApiWrapper[Atspi.Accessible]):
+
+ @property
+ def api_name(self) -> str:
+ return "ATSPI"
+
+ def __getattr__(self, name: str) -> Any:
+ return getattr(Atspi, name)
+
+ def find_node(self, dom_id: str, url: str) -> Atspi.Accessible:
+ """
+ :param dom_id: The dom id of the node to test.
+ :param url: The url of the test.
+ """
+ if self.test_url != url or not self.document:
+ self.test_url = url
+ self.document = self._poll_for(
+ self._find_fully_loaded_tab, f"Timeout looking for url: {self.test_url}"
+ )
+
+ test_node = self._find_node_by_id(self.document, dom_id);
+ if not test_node:
+ raise Exception(f"Did not find node with id '{dom_id}' in accessibility API ATSPI.")
+
+ return test_node
+
+ def get_relations_dictionary_helper(
+ self, node: Atspi.Accessible
+ ) -> Dict[str, List[str]]:
+ """
+ :returns: A dictionary with relations as keys and the values, DOM ids.
+ """
+ relations_dict: Dict[str, List[str]] = {}
+ relations = Atspi.Accessible.get_relation_set(node)
+ for relation in relations:
+ name = relation.get_relation_type().value_name.removeprefix("ATSPI_")
+ relations_dict[name] = []
+ num_targets = relation.get_n_targets()
+
+ for i in range(num_targets):
+ target = relation.get_target(i)
+ attributes = Atspi.Accessible.get_attributes(target)
+ relations_dict[name].append(attributes.get("id", "[unknown id]"))
+
+ return relations_dict
+
+ def get_state_list_helper(self, node: Atspi.Accessible) -> List[str]:
+ """
+ :returns: A list of states for this Atspi.Accessible.
+ """
+ state_list = Atspi.Accessible.get_state_set(node).get_states()
+ return [state.value_name.removeprefix("ATSPI_") for state in state_list]
+
+ def _find_browser(self) -> Optional[Atspi.Accessible]:
+ if self.pid and self.pid != 0:
+ return self._find_browser_by_pid()
+ else:
+ return self._find_browser_by_name()
+
+ def _find_browser_by_pid(self) -> Optional[Atspi.Accessible]:
+ """Find the Atspi.Accessible representing the browser.
+
+ :param pid: The PID of the browser.
+ :return: Atspi.Accessible or None.
+ """
+ desktop = Atspi.get_desktop(0)
+ child_count = Atspi.Accessible.get_child_count(desktop)
+ for i in range(child_count):
+ app = Atspi.Accessible.get_child_at_index(desktop, i)
+ if self.pid == Atspi.Accessible.get_process_id(app):
+ return app
+ return None
+
+ def _find_browser_by_name(self) -> Optional[Atspi.Accessible]:
+ """Find the Atspi.Accessible representing the browser.
+
+ :param name: The name of the browser.
+ :return: Atspi.Accessible or None.
+ """
+ desktop = Atspi.get_desktop(0)
+ child_count = Atspi.Accessible.get_child_count(desktop)
+ for i in range(child_count):
+ app = Atspi.Accessible.get_child_at_index(desktop, i)
+ full_app_name = Atspi.Accessible.get_name(app)
+ if self.product_name in full_app_name.lower():
+ return app
+ return None
+
+ def _find_fully_loaded_tab(self) -> Optional[Atspi.Accessible]:
+ """Find the tab with the test url. Only returns the tab when the tab is ready.
+
+ :param url: The url of the test.
+ :return: Atspi.Accessible representing test document or None.
+ """
+ stack = [self.root]
+ while stack:
+ node = stack.pop()
+ if Atspi.Accessible.get_role_name(node) == "frame":
+ relationset = Atspi.Accessible.get_relation_set(node)
+ for relation in relationset:
+ if relation.get_relation_type() == Atspi.RelationType.EMBEDS:
+ tab = relation.get_target(0)
+ if self._is_ready(tab, self.test_url):
+ return tab
+ else:
+ return None
+ continue
+
+ for i in range(Atspi.Accessible.get_child_count(node)):
+ child = Atspi.Accessible.get_child_at_index(node, i)
+ stack.append(child)
+
+ return None
+
+ def _is_ready(self, tab: Atspi.Accessible, url: str) -> bool:
+ """Test whether tab is fully loaded.
+
+ :param tab: Atspi.Accessible representing test document.
+ :param url: The url of the test.
+ :return: Boolean.
+ """
+ # Firefox uses the "BUSY" state to indicate the page is not ready.
+ if self.product_name == "firefox":
+ state_set = Atspi.Accessible.get_state_set(tab)
+ return not Atspi.StateSet.contains(state_set, Atspi.StateType.BUSY)
+
+ # Chromium family browsers do not use "BUSY", but you can
+ # tell if the document can be queried by URL attribute. If the 'URI'
+ # attribute is not here, we need to query for a new accessible object.
+ document = Atspi.Accessible.get_document_iface(tab)
+ document_attributes = Atspi.Document.get_document_attributes(document)
+
+ return "URI" in document_attributes and document_attributes["URI"] == url
+
+ def _find_node_by_id(
+ self, root: Atspi.Accessible, dom_id: str
+ ) -> Optional[Atspi.Accessible]:
+ """Find the Atspi.Accessible with a specified dom_id.
+
+ :param root: The root node to search from.
+ :param dom_id: The dom ID.
+ :return: Atspi.Accessible or None if not found.
+ """
+ stack = [root]
+ while stack:
+ node = stack.pop()
+ attributes = Atspi.Accessible.get_attributes(node)
+ if "id" in attributes and attributes["id"] == dom_id:
+ return node
+
+ for i in range(Atspi.Accessible.get_child_count(node)):
+ child = Atspi.Accessible.get_child_at_index(node, i)
+ stack.append(child)
+
+ return None
diff --git a/core-aam/aamtests/support/axapi_wrapper.py b/core-aam/aamtests/support/axapi_wrapper.py
new file mode 100644
index 00000000000000..d891ec7ca03c97
--- /dev/null
+++ b/core-aam/aamtests/support/axapi_wrapper.py
@@ -0,0 +1,127 @@
+from __future__ import annotations
+
+from typing import Any, Optional
+
+from ApplicationServices import (
+ AXUIElementCopyAttributeNames,
+ AXUIElementCopyAttributeValue,
+ AXUIElementCreateApplication,
+)
+
+from Cocoa import (
+ NSApplicationActivationPolicyRegular,
+ NSPredicate,
+ NSWorkspace,
+)
+
+from .api_wrapper import ApiWrapper
+
+AXUIElement = Any
+
+class AxapiWrapper(ApiWrapper[AXUIElement]):
+
+ @property
+ def api_name(self) -> str:
+ return "AXAPI"
+
+ @property
+ def AXUIElementCopyAttributeValue(self):
+ return AXUIElementCopyAttributeValue
+
+ def find_node(self, dom_id: str, url: str) -> AXUIElement:
+ """
+ :param dom_id: The dom id of the node to test.
+ :param url: The url of the test.
+ """
+ if self.test_url != url or not self.document:
+ self.test_url = url
+ self.document = self._poll_for(
+ self._find_tab,
+ f"Timeout looking for url: {self.test_url}",
+ )
+
+ test_node = self._poll_for(
+ lambda: self._find_node_by_id(self.document, dom_id),
+ f"Timeout looking for node with id {dom_id} in accessibility API AXAPI.",
+ )
+
+ return test_node
+
+ def _find_browser(self) -> Optional[AXUIElement]:
+ """Find the AXUIElement representing the browser.
+
+ :return: AXUIElement or None.
+ """
+ if self.pid and self.pid != 0:
+ return AXUIElementCreateApplication(self.pid)
+
+ ws = NSWorkspace.sharedWorkspace()
+ regular_predicate = NSPredicate.predicateWithFormat_(
+ f"activationPolicy == {NSApplicationActivationPolicyRegular}"
+ )
+ running_apps = ws.runningApplications().filteredArrayUsingPredicate_(
+ regular_predicate
+ )
+ name_predicate = NSPredicate.predicateWithFormat_(
+ f"localizedName contains[c] '{self.product_name}'"
+ )
+ filtered_apps = running_apps.filteredArrayUsingPredicate_(name_predicate)
+ if filtered_apps.count() == 0:
+ return None
+ app = filtered_apps[0]
+ pid = app.processIdentifier()
+ if pid == -1:
+ return None
+ return AXUIElementCreateApplication(pid)
+
+ def _find_tab(self) -> Optional[AXUIElement]:
+ """Find the active tab of the browser.
+
+ :return: AXUIElement representing test document or None.
+ """
+ stack = [self.root]
+ while stack:
+ node = stack.pop()
+
+ err, role = AXUIElementCopyAttributeValue(node, "AXRole", None)
+ if err:
+ continue
+ if role == "AXWebArea":
+ # TODO: AtspiWrapper will check that the found tab is the correct
+ # tab by checking the URL. Perform this check here.
+ return node
+
+ err, children = AXUIElementCopyAttributeValue(node, "AXChildren", None)
+ if err:
+ continue
+ stack.extend(children)
+
+ return None
+
+ def _find_node_by_id(self, root: Any, dom_id: str) -> Optional[AXUIElement]:
+ """Find the AXUIElement with a specified dom_id.
+
+ :param root: The root node to search from.
+ :param dom_id: The dom ID.
+ :return: AXUIElement or None if not found.
+ """
+ stack = [root]
+ while stack:
+ node = stack.pop()
+
+ err, attributes = AXUIElementCopyAttributeNames(node, None)
+ if err:
+ continue
+ if "AXDOMIdentifier" in attributes:
+ err, value = AXUIElementCopyAttributeValue(
+ node, "AXDOMIdentifier", None
+ )
+ if not err and value == dom_id:
+ return node
+
+ err, children = AXUIElementCopyAttributeValue(node, "AXChildren", None)
+ if err:
+ continue
+ stack.extend(children)
+
+ return None
diff --git a/core-aam/aamtests/support/fixtures_a11y_api.py b/core-aam/aamtests/support/fixtures_a11y_api.py
new file mode 100644
index 00000000000000..de64518d0d65fe
--- /dev/null
+++ b/core-aam/aamtests/support/fixtures_a11y_api.py
@@ -0,0 +1,60 @@
+import pytest
+from sys import platform
+
+
+def pid_from(capabilities):
+ # TODO: add support for Edge, Safari.
+ if capabilities["browserName"] == "chrome":
+ return capabilities["goog:processID"], "chrome"
+ if capabilities["browserName"] == "firefox":
+ return capabilities["moz:processID"], "firefox"
+ if capabilities["browserName"] == "servo":
+ return 0, "servo"
+
+
+@pytest.fixture
+def default_timeout(full_configuration):
+ if not full_configuration["timeout"] or full_configuration["timeout"] == 0:
+ return 60
+ return full_configuration["timeout"] * 0.5
+
+
+@pytest.fixture
+def atspi(session, default_timeout):
+ if platform != "linux":
+ pytest.skip("NOT_APPLICABLE")
+
+ from .atspi_wrapper import AtspiWrapper
+
+ pid, product_name = pid_from(session.capabilities)
+ return AtspiWrapper(pid, product_name, default_timeout)
+
+
+@pytest.fixture
+def axapi(session, default_timeout):
+ if platform != "darwin":
+ pytest.skip("NOT_APPLICABLE")
+
+ from .axapi_wrapper import AxapiWrapper
+
+ pid, product_name = pid_from(session.capabilities)
+ return AxapiWrapper(pid, product_name, default_timeout)
+
+
+@pytest.fixture
+def uia(session):
+ if platform != "win32":
+ pytest.skip("NOT_APPLICABLE")
+
+ # TODO: Make UiaWrapper and return it
+
+
+@pytest.fixture
+def ia2(session, default_timeout):
+ if platform != "win32":
+ pytest.skip("NOT_APPLICABLE")
+
+ from .ia2_wrapper import Ia2Wrapper
+
+ pid, product_name = pid_from(session.capabilities)
+ return Ia2Wrapper(pid, product_name, default_timeout)
diff --git a/core-aam/aamtests/support/ia2_wrapper.py b/core-aam/aamtests/support/ia2_wrapper.py
new file mode 100644
index 00000000000000..6952b3abc0d649
--- /dev/null
+++ b/core-aam/aamtests/support/ia2_wrapper.py
@@ -0,0 +1,169 @@
+from __future__ import annotations
+
+from typing import Any, List, Optional
+
+import ctypes
+from ctypes import POINTER, byref
+from ctypes.wintypes import BOOL, HWND, LPARAM
+
+# Type aliases for COM interface pointers.
+# These are dynamically generated by comtypes at runtime.
+IAccessiblePtr = Any
+IAccessible2Ptr = Any
+
+import comtypes.client
+from comtypes import IServiceProvider
+
+from .api_wrapper import ApiWrapper
+from ia2 import ( # type: ignore[attr-defined]
+ IAccessible2_2,
+ Role,
+ msaa_state_list_to_string,
+ state_list_to_string,
+)
+
+CHILDID_SELF = 0
+OBJID_CLIENT = -4
+
+user32 = ctypes.windll.user32 # type: ignore
+oleacc = ctypes.oledll.oleacc # type: ignore
+oleacc_mod = comtypes.client.GetModule("oleacc.dll")
+IAccessible = oleacc_mod.IAccessible # noqa: N816
+
+
+def accessible_object_from_window(hwnd: HWND) -> IAccessiblePtr:
+ p = POINTER(IAccessible)()
+ oleacc.AccessibleObjectFromWindow(
+ hwnd, OBJID_CLIENT, byref(IAccessible._iid_), byref(p)
+ )
+ return p
+
+
+def name_from_hwnd(hwnd: HWND) -> str:
+ MAX_CHARS = 257
+ buffer = ctypes.create_unicode_buffer(MAX_CHARS)
+ user32.GetWindowTextW(hwnd, buffer, MAX_CHARS)
+ return buffer.value
+
+
+def get_browser_hwnd(product_name: str) -> HWND:
+ found: List[HWND] = []
+
+ @ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) # type: ignore[attr-defined, misc]
+ def check_window_name(hwnd: HWND, lParam: LPARAM) -> bool: # noqa: N803
+ window_name = name_from_hwnd(hwnd)
+ if product_name not in window_name.lower():
+ # EnumWindows should continue enumerating
+ return True
+ found.append(hwnd)
+ # EnumWindows should stop enumerating (since we found the right window)
+ return False
+
+ user32.EnumWindows(check_window_name, LPARAM(0))
+ if not found:
+ raise LookupError(f"Couldn't find {product_name} HWND")
+ return found[0]
+
+
+def to_ia2(node: IAccessiblePtr) -> IAccessible2Ptr:
+ service = node.QueryInterface(IServiceProvider)
+ return service.QueryService(IAccessible._iid_, IAccessible2_2)
+
+
+class Ia2Wrapper(ApiWrapper[IAccessible2Ptr]):
+
+ @property
+ def api_name(self) -> str:
+ return "IA2"
+
+ def find_node(self, dom_id: str, url: str) -> IAccessible2Ptr:
+ """
+ :param dom_id: The dom id of the node to test.
+ :param url: The url of the test.
+ """
+ if self.test_url != url or not self.document:
+ self.test_url = url
+ self.document = self._poll_for(
+ self._find_tab,
+ f"Timeout looking for url: {self.test_url}",
+ )
+
+ test_node = self._poll_for(
+ lambda: self._find_node_by_id(self.document, dom_id),
+ f"Timeout looking for node with id {dom_id} in accessibility API IA2.",
+ )
+
+ return test_node
+
+ def get_role(self, node: IAccessible2Ptr) -> str:
+ return Role(node.role()).name
+
+ def get_msaa_role(self, node: IAccessible2Ptr) -> str:
+ return Role(node.accRole(CHILDID_SELF)).name
+
+ def get_state_list(self, node: IAccessible2Ptr) -> str:
+ return state_list_to_string(node.states)
+
+ def get_msaa_state_list(self, node: IAccessible2Ptr) -> str:
+ return msaa_state_list_to_string(node.accState(CHILDID_SELF))
+
+ def _find_browser(self) -> Optional[IAccessible2Ptr]:
+ """Find the IAccessible2 node representing the browser.
+
+ :return: IAccessible2Ptr.
+ """
+ hwnd = get_browser_hwnd(self.product_name)
+ root = accessible_object_from_window(hwnd)
+ return to_ia2(root)
+
+ def _find_tab(self) -> Optional[IAccessible2Ptr]:
+ """Find the tab with the test url. Only returns the tab when the tab is ready.
+
+ :return: IAccessible2Ptr representing test document or None.
+ """
+ targets, count = self.root.relationTargetsOfType("embeds", 1)
+ if count >= 1:
+ ia2 = to_ia2(targets[0])
+ if ia2.accValue(CHILDID_SELF) == self.test_url:
+ return ia2
+
+ return self._find_tab_by_searching_tree(self.root)
+
+ def _find_tab_by_searching_tree(
+ self, root: IAccessible2Ptr
+ ) -> Optional[IAccessible2Ptr]:
+ """Find the tab by searching the accessibility tree.
+
+ :param root: The root node to search from.
+ :return: IAccessible2Ptr representing test document or None.
+ """
+ for i in range(1, root.accChildCount + 1):
+ child = to_ia2(root.accChild(i))
+ if child.accRole(CHILDID_SELF) == Role.ROLE_SYSTEM_DOCUMENT:
+ if child.accValue(CHILDID_SELF) == self.test_url:
+ return child
+ # No need to search within documents.
+ return None
+ descendant = self._find_tab_by_searching_tree(child)
+ if descendant:
+ return descendant
+ return None
+
+ def _find_node_by_id(
+ self, root: IAccessible2Ptr, dom_id: str
+ ) -> Optional[IAccessible2Ptr]:
+ """Find the IAccessible2 node with a specified dom_id.
+
+ :param root: The root node to search from.
+ :param dom_id: The dom ID.
+ :return: IAccessible2Ptr or None if not found.
+ """
+ id_attribute = f"id:{dom_id};"
+ for i in range(1, root.accChildCount + 1):
+ child = to_ia2(root.accChild(i))
+ if child.attributes and id_attribute in child.attributes:
+ return child
+ descendant = self._find_node_by_id(child, dom_id)
+ if descendant:
+ return descendant
+ return None
diff --git a/docs/test-suite-design.md b/docs/test-suite-design.md
index 6a104e2f1d42fd..1043ca62335c38 100644
--- a/docs/test-suite-design.md
+++ b/docs/test-suite-design.md
@@ -68,6 +68,10 @@ expectations:
* [wdspec][] tests are written in Python and test [the WebDriver browser
automation protocol](https://w3c.github.io/webdriver/)
+* [aamtest][] tests are written in Python and test accessibility API mappings,
+ such as [Core Accessibility API Mappings](https://w3c.github.io/core-aam/)
+ or [HTML Accessibility API Mappings](https://w3c.github.io/html-aam/).
+
* [Manual tests][manual] rely on a human to run them and determine their
result.
@@ -77,3 +81,4 @@ expectations:
[manual]: writing-tests/manual
[running-from-local-system]: running-tests/from-local-system
[wdspec]: writing-tests/wdspec
+[aamtest]: writing-tests/aamtest
diff --git a/docs/writing-tests/aamtest.md b/docs/writing-tests/aamtest.md
new file mode 100644
index 00000000000000..273dff7c9f96e0
--- /dev/null
+++ b/docs/writing-tests/aamtest.md
@@ -0,0 +1,148 @@
+# aamtest tests
+
+The aamtest tests are used to verify the mapping of web content to
+browser-exposed platform-specific accessibility APIs. These mappings are
+specified by working groups of the W3C in the following specifications:
+
+* [Core-AAM](https://w3c.github.io/core-aam)
+* [HTML-AAM](https://w3c.github.io/html-aam)
+* [DPUB-AAM](https://w3c.github.io/dpub-aam)
+* [MathML-AAM](https://w3c.github.io/mathml-aam)
+* [Graphics-AAM](https://w3c.github.io/graphics-aam)
+* [SVG-AAM](https://w3c.github.io/svg-aam/)
+
+These tests are written in [the Python programming
+language](https://www.python.org/) and structured with [the pytest testing
+framework](https://docs.pytest.org/en/latest/).
+
+The aamtest type is built on the [wdspec](wdspec) test type, and has access to
+all the Python fixtures defined for wdspec tests. It uses the web-platform-tests
+maintained WebDriver client library to load HTML and to send other WebDriver
+commands to the browser.
+
+The `wptrunner` will know a Python file is an aamtest if it is contained within
+an `aamtests` directory.
+
+## Platform-Specific Accessibility APIs
+
+Accessibility APIs are platform (or operating system) specific, each platform
+has their own API (sometimes more than one). Assistive technologies, such as
+screen readers, interact with the browser on behalf of a user via these
+APIs. The AAM specifications explain how to expose web content through these
+APIs. You can read more about [the APIs in
+Core-AAM](https://w3c.github.io/core-aam/#intro_aapi).
+
+The table below lists:
+
+* **API Name**: APIs supported by the aamtest framework
+* **Fixture Name**: The name of the pytest fixture that returns access to the API (if you are
+ on the correct platform).
+* **Platform**: The platform of that API.
+* **Python Bindings**: The Python library that provides bindings to query the API.
+
+```eval_rst
+.. list-table::
+ :header-rows: 1
+
+ * - API Name
+ - Fixture Name
+ - Platform
+ - Python Bindings
+ * - Accessibility Toolkit (`ATK `_) and Assistive Technology Service Provider Interface (`AT-SPI `_)
+ - ``atspi``
+ - Linux
+ - `Provided through PyGObject `_
+ * - The NSAccessibility Protocol for macOS (`AX API `_)
+ - ``axapi``
+ - macOS
+ - `pyobjc-framework-Accessibility `_
+ * - MSAA with IAccessible2 1.3 (`IA2 `_)
+ - ``ia2``
+ - Windows
+ - Loading module `ia2_api_all.idl `_ with `comtypes `_
+```
+
+The APIs are exposed through a pytest fixture with the name in the table
+above. The pytest fixture returns a wrapped version of the API. Requesting this
+fixture on a platform where it is not supported will result in a `MISSING`
+subtest. It is expected that each test file run on a given platform will have
+one or more subtests that run to completion, as well as several subtests who's
+results will not be recorded. This is because each test file should show how the
+**same markup** is exposed in each supporting accessibility APIs/platforms.
+
+### Package dependencies for Linux API AT-SPI Python Bindings
+
+In order to test the Linux API AT-SPI, you need to have the following packages
+installed:
+
+```
+sudo apt install libatspi2.0-dev libcairo2-dev libgirepository1.0-dev
+```
+
+## Adding new tests
+
+If you would like to add a new aamtest to a specification that does not yet have
+coverage, add the tests to an `aamtests` subfolder. This subfolder indicates the
+Python files within it will be run as an aamtest. This includes restarting the
+browser with accessibility enabled, if you are doing a full run of the test
+suite.
+
+In the `aamtests` subfolder, the `conftest.py` will need to add the
+`webdriver/test/support` path and `core-aam/aamtests/support` path to the
+sys.path and add the paths as `pytest_plugins` in order to have access to the
+appropriate fixtures. See `core-aam/aamtests/conftest.py`.
+
+### Test design
+
+Similar to [testharness.js](testharness) tests, each file is a test, and a
+function that begins with the name "test_" is a subtest of that test.
+
+A typical test file contains some html markup and several subtests. Each subtest
+will test how that markup is exposed in a single accessibility API. For example,
+if you are testing how `
foobar widget
` is exposed in
+accessibility APIs, and foobars are supported in the Linux API AT-SPI and macOS
+API AX API, you should add a subtest called `test_atspi` and `test_axapi`,
+respectively. Both subtests will load the same HTML, then query their respective
+accessibility APIs. The subtest name should include, at least, the name of the
+API being tested for ease of understanding the test results.
+
+For example, the file `foobar.py`:
+```python
+TEST_HTML = ""
+
+# Test of the Linux accessibility API: AT-SPI
+def test_atspi(atspi, session, inline):
+ # The `session` and `inline` fixtures are provided from the `wdspec` test infrastructure.
+ session.url = inline(TEST_HTML)
+
+ # The `atspi` fixture wraps the AT-SPI Python bindings and provides some helper functions,
+ # such as `find_node`, which finds a node by DOM ID.
+ node = atspi.find_node("test", session.url)
+ assert atspi.Accessible.get_role(node) == atspi.Role.FOOBAR
+
+# Test of the macOS accessibility API: AX API
+def test_axapi(axapi, session, inline):
+ # The `session` and `inline` fixtures are provided from the `wdspec` test infrastructure.
+ session.url = inline(TEST_HTML)
+
+ # The `axapi` fixture wraps the AX API Python bindings and provides some helper functions,
+ # such as `find_node`, which finds a node by DOM ID.
+ node = axapi.find_node("test", session.url)
+ role = axapi.AXUIElementCopyAttributeValue(node, "AXRole", None)[1]
+ assert role == "AXFoobar"
+```
+
+## Adding support for an unsupported API
+
+To add an unsupported API:
+
+1. Add the Python package that provides the Python bindings for that API to
+ `tools/wptrunner/requirements_platform_accessibility.txt`.
+2. Create a wrapper object for the new API in `newapi_wrapper.py` in
+ `core-aam/aamtests/support/`. It must inherit from `ApiWrapper` and follow
+ the same conventions as the other API wrappers, as appropriate.
+3. Add the fixture for that API in
+ `core-aam/aamtests/support/fixtures_a11y_api.py`.
+4. Update the table in the "Platform-Specific Accessibility APIs" section of this
+ document.
+5. Add a new subtest to all the files that contain markup you would like to test.
diff --git a/lint.ignore b/lint.ignore
index 408729b9df62f0..7b21a36c4585f5 100644
--- a/lint.ignore
+++ b/lint.ignore
@@ -52,6 +52,7 @@ TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.wbn
TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.avif
TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.annexb
TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.crx
+TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.tlb
## .gitignore
W3C-TEST.ORG: .gitignore
diff --git a/tools/ci/tc/tasks/test.yml b/tools/ci/tc/tasks/test.yml
index 8973d3beb49f9e..ef6d3f021fa8a0 100644
--- a/tools/ci/tc/tasks/test.yml
+++ b/tools/ci/tc/tasks/test.yml
@@ -4,7 +4,7 @@ components:
workerType: ci
schedulerId: taskcluster-github
deadline: "24 hours"
- image: ghcr.io/web-platform-tests/wpt:2
+ image: ghcr.io/web-platform-tests/wpt:3
maxRunTime: 7200
artifacts:
public/results:
diff --git a/tools/docker/Dockerfile b/tools/docker/Dockerfile
index a936ad9167f058..53f1c9f3d7a5be 100644
--- a/tools/docker/Dockerfile
+++ b/tools/docker/Dockerfile
@@ -20,6 +20,9 @@ RUN apt-get -qqy update \
glib-networking-services \
gstreamer1.0-plugins-bad \
gstreamer1.0-gl \
+ libatspi2.0-dev \
+ libcairo2-dev \
+ libgirepository1.0-dev \
libosmesa6-dev \
libproxy1-plugin-webkit \
libvirt-daemon-system \
diff --git a/tools/iaccessible2/README.txt b/tools/iaccessible2/README.txt
new file mode 100644
index 00000000000000..8b3bbcee9a059b
--- /dev/null
+++ b/tools/iaccessible2/README.txt
@@ -0,0 +1,12 @@
+This folder contains IAccessible2 (and MSAA) support files for tests of type
+`aamtest`.
+
+ia2_api_all.tlb
+
+ Contains the IAccessible2 COM interfaces, generated from
+ https://github.com/LinuxA11y/IAccessible2.
+
+ia2.py
+
+ Exposes IAccessible2_2 Interface and helpful enums and functions for
+ converting IAccessible, IAccessible2 and MSAA constants to strings.
diff --git a/tools/iaccessible2/__init__.py b/tools/iaccessible2/__init__.py
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/tools/iaccessible2/ia2.py b/tools/iaccessible2/ia2.py
new file mode 100644
index 00000000000000..2cc3f14e73354e
--- /dev/null
+++ b/tools/iaccessible2/ia2.py
@@ -0,0 +1,210 @@
+# type: ignore
+
+import enum
+import os
+import comtypes.client
+
+# Get IAccessible2 constants for helper functions below
+ia2_tlb = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)),
+ "ia2_api_all.tlb",
+)
+ia2_mod = comtypes.client.GetModule(ia2_tlb)
+# Add to globals the many IAccessible2 constants. These will also be imported
+# when this file is imported.
+globals().update((k, getattr(ia2_mod, k)) for k in ia2_mod.__all__)
+
+
+class Role(enum.IntEnum):
+ # MSAA roles - constants not provided via oleacc
+ ROLE_SYSTEM_TITLEBAR = 1
+ ROLE_SYSTEM_MENUBAR = 2
+ ROLE_SYSTEM_SCROLLBAR = 3
+ ROLE_SYSTEM_GRIP = 4
+ ROLE_SYSTEM_SOUND = 5
+ ROLE_SYSTEM_CURSOR = 6
+ ROLE_SYSTEM_CARET = 7
+ ROLE_SYSTEM_ALERT = 8
+ ROLE_SYSTEM_WINDOW = 9
+ ROLE_SYSTEM_CLIENT = 10
+ ROLE_SYSTEM_MENUPOPUP = 11
+ ROLE_SYSTEM_MENUITEM = 12
+ ROLE_SYSTEM_TOOLTIP = 13
+ ROLE_SYSTEM_APPLICATION = 14
+ ROLE_SYSTEM_DOCUMENT = 15
+ ROLE_SYSTEM_PANE = 16
+ ROLE_SYSTEM_CHART = 17
+ ROLE_SYSTEM_DIALOG = 18
+ ROLE_SYSTEM_BORDER = 19
+ ROLE_SYSTEM_GROUPING = 20
+ ROLE_SYSTEM_SEPARATOR = 21
+ ROLE_SYSTEM_TOOLBAR = 22
+ ROLE_SYSTEM_STATUSBAR = 23
+ ROLE_SYSTEM_TABLE = 24
+ ROLE_SYSTEM_COLUMNHEADER = 25
+ ROLE_SYSTEM_ROWHEADER = 26
+ ROLE_SYSTEM_COLUMN = 27
+ ROLE_SYSTEM_ROW = 28
+ ROLE_SYSTEM_CELL = 29
+ ROLE_SYSTEM_LINK = 30
+ ROLE_SYSTEM_HELPBALLOON = 31
+ ROLE_SYSTEM_CHARACTER = 32
+ ROLE_SYSTEM_LIST = 33
+ ROLE_SYSTEM_LISTITEM = 34
+ ROLE_SYSTEM_OUTLINE = 35
+ ROLE_SYSTEM_OUTLINEITEM = 36
+ ROLE_SYSTEM_PAGETAB = 37
+ ROLE_SYSTEM_PROPERTYPAGE = 38
+ ROLE_SYSTEM_INDICATOR = 39
+ ROLE_SYSTEM_GRAPHIC = 40
+ ROLE_SYSTEM_STATICTEXT = 41
+ ROLE_SYSTEM_TEXT = 42
+ ROLE_SYSTEM_PUSHBUTTON = 43
+ ROLE_SYSTEM_CHECKBUTTON = 44
+ ROLE_SYSTEM_RADIOBUTTON = 45
+ ROLE_SYSTEM_COMBOBOX = 46
+ ROLE_SYSTEM_DROPLIST = 47
+ ROLE_SYSTEM_PROGRESSBAR = 48
+ ROLE_SYSTEM_DIAL = 49
+ ROLE_SYSTEM_HOTKEYFIELD = 50
+ ROLE_SYSTEM_SLIDER = 51
+ ROLE_SYSTEM_SPINBUTTON = 52
+ ROLE_SYSTEM_DIAGRAM = 53
+ ROLE_SYSTEM_ANIMATION = 54
+ ROLE_SYSTEM_EQUATION = 55
+ ROLE_SYSTEM_BUTTONDROPDOWN = 56
+ ROLE_SYSTEM_BUTTONMENU = 57
+ ROLE_SYSTEM_BUTTONDROPDOWNGRID = 58
+ ROLE_SYSTEM_WHITESPACE = 59
+ ROLE_SYSTEM_PAGETABLIST = 60
+ ROLE_SYSTEM_CLOCK = 61
+ ROLE_SYSTEM_SPLITBUTTON = 62
+ ROLE_SYSTEM_IPADDRESS = 63
+ ROLE_SYSTEM_OUTLINEBUTTON = 64
+ # IAccessible2 roles
+ IA2_ROLE_CANVAS = ia2_mod.IA2_ROLE_CANVAS
+ IA2_ROLE_CAPTION = ia2_mod.IA2_ROLE_CAPTION
+ IA2_ROLE_CHECK_MENU_ITEM = ia2_mod.IA2_ROLE_CHECK_MENU_ITEM
+ IA2_ROLE_COLOR_CHOOSER = ia2_mod.IA2_ROLE_COLOR_CHOOSER
+ IA2_ROLE_DATE_EDITOR = ia2_mod.IA2_ROLE_DATE_EDITOR
+ IA2_ROLE_DESKTOP_ICON = ia2_mod.IA2_ROLE_DESKTOP_ICON
+ IA2_ROLE_DESKTOP_PANE = ia2_mod.IA2_ROLE_DESKTOP_PANE
+ IA2_ROLE_DIRECTORY_PANE = ia2_mod.IA2_ROLE_DIRECTORY_PANE
+ IA2_ROLE_EDITBAR = ia2_mod.IA2_ROLE_EDITBAR
+ IA2_ROLE_EMBEDDED_OBJECT = ia2_mod.IA2_ROLE_EMBEDDED_OBJECT
+ IA2_ROLE_ENDNOTE = ia2_mod.IA2_ROLE_ENDNOTE
+ IA2_ROLE_FILE_CHOOSER = ia2_mod.IA2_ROLE_FILE_CHOOSER
+ IA2_ROLE_FONT_CHOOSER = ia2_mod.IA2_ROLE_FONT_CHOOSER
+ IA2_ROLE_FOOTER = ia2_mod.IA2_ROLE_FOOTER
+ IA2_ROLE_FOOTNOTE = ia2_mod.IA2_ROLE_FOOTNOTE
+ IA2_ROLE_FORM = ia2_mod.IA2_ROLE_FORM
+ IA2_ROLE_FRAME = ia2_mod.IA2_ROLE_FRAME
+ IA2_ROLE_GLASS_PANE = ia2_mod.IA2_ROLE_GLASS_PANE
+ IA2_ROLE_HEADER = ia2_mod.IA2_ROLE_HEADER
+ IA2_ROLE_HEADING = ia2_mod.IA2_ROLE_HEADING
+ IA2_ROLE_ICON = ia2_mod.IA2_ROLE_ICON
+ IA2_ROLE_IMAGE_MAP = ia2_mod.IA2_ROLE_IMAGE_MAP
+ IA2_ROLE_INPUT_METHOD_WINDOW = ia2_mod.IA2_ROLE_INPUT_METHOD_WINDOW
+ IA2_ROLE_INTERNAL_FRAME = ia2_mod.IA2_ROLE_INTERNAL_FRAME
+ IA2_ROLE_LABEL = ia2_mod.IA2_ROLE_LABEL
+ IA2_ROLE_LAYERED_PANE = ia2_mod.IA2_ROLE_LAYERED_PANE
+ IA2_ROLE_NOTE = ia2_mod.IA2_ROLE_NOTE
+ IA2_ROLE_OPTION_PANE = ia2_mod.IA2_ROLE_OPTION_PANE
+ IA2_ROLE_PAGE = ia2_mod.IA2_ROLE_PAGE
+ IA2_ROLE_PARAGRAPH = ia2_mod.IA2_ROLE_PARAGRAPH
+ IA2_ROLE_RADIO_MENU_ITEM = ia2_mod.IA2_ROLE_RADIO_MENU_ITEM
+ IA2_ROLE_REDUNDANT_OBJECT = ia2_mod.IA2_ROLE_REDUNDANT_OBJECT
+ IA2_ROLE_ROOT_PANE = ia2_mod.IA2_ROLE_ROOT_PANE
+ IA2_ROLE_RULER = ia2_mod.IA2_ROLE_RULER
+ IA2_ROLE_SCROLL_PANE = ia2_mod.IA2_ROLE_SCROLL_PANE
+ IA2_ROLE_SECTION = ia2_mod.IA2_ROLE_SECTION
+ IA2_ROLE_SHAPE = ia2_mod.IA2_ROLE_SHAPE
+ IA2_ROLE_SPLIT_PANE = ia2_mod.IA2_ROLE_SPLIT_PANE
+ IA2_ROLE_TEAR_OFF_MENU = ia2_mod.IA2_ROLE_TEAR_OFF_MENU
+ IA2_ROLE_TERMINAL = ia2_mod.IA2_ROLE_TERMINAL
+ IA2_ROLE_TEXT_FRAME = ia2_mod.IA2_ROLE_TEXT_FRAME
+ IA2_ROLE_TOGGLE_BUTTON = ia2_mod.IA2_ROLE_TOGGLE_BUTTON
+ IA2_ROLE_UNKNOWN = ia2_mod.IA2_ROLE_UNKNOWN
+ IA2_ROLE_VIEW_PORT = ia2_mod.IA2_ROLE_VIEW_PORT
+ IA2_ROLE_COMPLEMENTARY_CONTENT = ia2_mod.IA2_ROLE_COMPLEMENTARY_CONTENT
+ IA2_ROLE_LANDMARK = ia2_mod.IA2_ROLE_LANDMARK
+ IA2_ROLE_LEVEL_BAR = ia2_mod.IA2_ROLE_LEVEL_BAR
+ IA2_ROLE_CONTENT_DELETION = ia2_mod.IA2_ROLE_CONTENT_DELETION
+ IA2_ROLE_CONTENT_INSERTION = ia2_mod.IA2_ROLE_CONTENT_INSERTION
+ IA2_ROLE_BLOCK_QUOTE = ia2_mod.IA2_ROLE_BLOCK_QUOTE
+ IA2_ROLE_MARK = ia2_mod.IA2_ROLE_MARK
+ IA2_ROLE_SUGGESTION = ia2_mod.IA2_ROLE_SUGGESTION
+ IA2_ROLE_COMMENT = ia2_mod.IA2_ROLE_COMMENT
+
+
+class MsaaState(enum.IntFlag):
+ STATE_SYSTEM_UNAVAILABLE = 0x00000001
+ STATE_SYSTEM_SELECTED = 0x00000002
+ STATE_SYSTEM_FOCUSED = 0x00000004
+ STATE_SYSTEM_PRESSED = 0x00000008
+ STATE_SYSTEM_CHECKED = 0x00000010
+ STATE_SYSTEM_MIXED = 0x00000020
+ STATE_SYSTEM_READONLY = 0x00000040
+ STATE_SYSTEM_HOTTRACKED = 0x00000080
+ STATE_SYSTEM_DEFAULT = 0x00000100
+ STATE_SYSTEM_EXPANDED = 0x00000200
+ STATE_SYSTEM_COLLAPSED = 0x00000400
+ STATE_SYSTEM_BUSY = 0x00000800
+ STATE_SYSTEM_FLOATING = 0x00001000
+ STATE_SYSTEM_MARQUEED = 0x00002000
+ STATE_SYSTEM_ANIMATED = 0x00004000
+ STATE_SYSTEM_INVISIBLE = 0x00008000
+ STATE_SYSTEM_OFFSCREEN = 0x00010000
+ STATE_SYSTEM_SIZEABLE = 0x00020000
+ STATE_SYSTEM_MOVEABLE = 0x00040000
+ STATE_SYSTEM_SELFVOICING = 0x00080000
+ STATE_SYSTEM_FOCUSABLE = 0x00100000
+ STATE_SYSTEM_SELECTABLE = 0x00200000
+ STATE_SYSTEM_LINKED = 0x00400000
+ STATE_SYSTEM_TRAVERSED = 0x00800000
+ STATE_SYSTEM_MULTISELECTABLE = 0x01000000
+ STATE_SYSTEM_EXTSELECTABLE = 0x02000000
+ STATE_SYSTEM_ALERT_LOW = 0x04000000
+ STATE_SYSTEM_ALERT_MEDIUM = 0x08000000
+ STATE_SYSTEM_ALERT_HIGH = 0x10000000
+ STATE_SYSTEM_PROTECTED = 0x20000000
+ STATE_SYSTEM_HASPOPUP = 0x40000000
+
+
+class Ia2State(enum.IntFlag):
+ IA2_STATE_ACTIVE = ia2_mod.IA2_STATE_ACTIVE
+ IA2_STATE_ARMED = ia2_mod.IA2_STATE_ARMED
+ IA2_STATE_CHECKABLE = ia2_mod.IA2_STATE_CHECKABLE
+ IA2_STATE_DEFUNCT = ia2_mod.IA2_STATE_DEFUNCT
+ IA2_STATE_EDITABLE = ia2_mod.IA2_STATE_EDITABLE
+ IA2_STATE_HORIZONTAL = ia2_mod.IA2_STATE_HORIZONTAL
+ IA2_STATE_ICONIFIED = ia2_mod.IA2_STATE_ICONIFIED
+ IA2_STATE_INVALID_ENTRY = ia2_mod.IA2_STATE_INVALID_ENTRY
+ IA2_STATE_MANAGES_DESCENDANTS = ia2_mod.IA2_STATE_MANAGES_DESCENDANTS
+ IA2_STATE_MODAL = ia2_mod.IA2_STATE_MODAL
+ IA2_STATE_MULTI_LINE = ia2_mod.IA2_STATE_MULTI_LINE
+ IA2_STATE_OPAQUE = ia2_mod.IA2_STATE_OPAQUE
+ IA2_STATE_PINNED = ia2_mod.IA2_STATE_PINNED
+ IA2_STATE_REQUIRED = ia2_mod.IA2_STATE_REQUIRED
+ IA2_STATE_SELECTABLE_TEXT = ia2_mod.IA2_STATE_SELECTABLE_TEXT
+ IA2_STATE_SINGLE_LINE = ia2_mod.IA2_STATE_SINGLE_LINE
+ IA2_STATE_STALE = ia2_mod.IA2_STATE_STALE
+ IA2_STATE_SUPPORTS_AUTOCOMPLETION = ia2_mod.IA2_STATE_SUPPORTS_AUTOCOMPLETION
+ IA2_STATE_TRANSIENT = ia2_mod.IA2_STATE_TRANSIENT
+ IA2_STATE_VERTICAL = ia2_mod.IA2_STATE_VERTICAL
+
+
+def msaa_state_list_to_string(states):
+ state_strings = []
+ for state in MsaaState:
+ if states & state:
+ state_strings.append(state.name.removeprefix("STATE_SYSTEM_"))
+ return state_strings
+
+
+def state_list_to_string(states):
+ state_strings = []
+ for state in Ia2State:
+ if states & state:
+ state_strings.append(state.name.removeprefix("IA2_STATE_"))
+ return state_strings
diff --git a/tools/iaccessible2/ia2_api_all.tlb b/tools/iaccessible2/ia2_api_all.tlb
new file mode 100644
index 00000000000000..449c10b258dd7b
Binary files /dev/null and b/tools/iaccessible2/ia2_api_all.tlb differ
diff --git a/tools/localpaths.py b/tools/localpaths.py
index 34e997fc5a21c0..510b5275f81c36 100644
--- a/tools/localpaths.py
+++ b/tools/localpaths.py
@@ -34,3 +34,4 @@
sys.path.insert(0, os.path.join(here, "webdriver"))
sys.path.insert(0, os.path.join(here, "wptrunner"))
sys.path.insert(0, os.path.join(here, "webtransport"))
+sys.path.insert(0, os.path.join(here, "iaccessible2"))
diff --git a/tools/manifest/item.py b/tools/manifest/item.py
index 6aa9f6c8e68347..ec3a28c8d3c78d 100644
--- a/tools/manifest/item.py
+++ b/tools/manifest/item.py
@@ -360,6 +360,22 @@ def to_json(self) -> Tuple[Optional[Text], Dict[Text, Any]]:
return rv
+class AccessibilityAPIMappingTest(URLManifestItem):
+ __slots__ = ()
+
+ item_type = "aamtest"
+
+ @property
+ def timeout(self) -> Optional[Text]:
+ return self._extras.get("timeout")
+
+ def to_json(self) -> Tuple[Optional[Text], Dict[Text, Any]]:
+ rv = super().to_json()
+ if self.timeout is not None:
+ rv[-1]["timeout"] = self.timeout
+ return rv
+
+
class SupportFile(ManifestItem):
__slots__ = ()
diff --git a/tools/manifest/manifest.py b/tools/manifest/manifest.py
index 12260192c3297c..9cc04785836ebf 100644
--- a/tools/manifest/manifest.py
+++ b/tools/manifest/manifest.py
@@ -8,7 +8,8 @@
from . import jsonlib
from . import vcs
-from .item import (ConformanceCheckerTest,
+from .item import (AccessibilityAPIMappingTest,
+ ConformanceCheckerTest,
CrashTest,
ManifestItem,
ManualTest,
@@ -47,6 +48,7 @@ class InvalidCacheError(Exception):
"crashtest": CrashTest,
"manual": ManualTest,
"wdspec": WebDriverSpecTest,
+ "aamtest": AccessibilityAPIMappingTest,
"conformancechecker": ConformanceCheckerTest,
"visual": VisualTest,
"spec": SpecItem,
diff --git a/tools/manifest/sourcefile.py b/tools/manifest/sourcefile.py
index 101f28dbdb510a..e649429c6cc214 100644
--- a/tools/manifest/sourcefile.py
+++ b/tools/manifest/sourcefile.py
@@ -16,7 +16,8 @@
import html5lib
from . import XMLParser
-from .item import (ConformanceCheckerTest,
+from .item import (AccessibilityAPIMappingTest,
+ ConformanceCheckerTest,
CrashTest,
ManifestItem,
ManualTest,
@@ -37,7 +38,7 @@
# because relative import beyond toplevel throws *ImportError*!
from metadata.webfeatures.schema import WEB_FEATURES_YML_FILENAME # type: ignore
-wd_pattern = "*.py"
+py_pattern = "*.py"
js_meta_re = re.compile(br"//\s*META:\s*(\w*)=(.*)$")
python_meta_re = re.compile(br"#\s*META:\s*(\w*)=(.*)$")
@@ -399,7 +400,16 @@ def name_is_webdriver(self) -> bool:
(rel_path_parts[:2] == ("infrastructure", "webdriver") and
len(rel_path_parts) > 2)) and
self.filename not in ("__init__.py", "conftest.py") and
- fnmatch(self.filename, wd_pattern))
+ fnmatch(self.filename, py_pattern))
+
+ @property
+ def name_is_aamtest(self) -> bool:
+ """Check if the file name matches the conditions for the file to
+ be an accessibility API test"""
+ rel_path_parts = self.rel_path_parts
+ return ("aamtests" in rel_path_parts and
+ self.filename not in ("__init__.py", "conftest.py") and
+ fnmatch(self.filename, py_pattern))
@property
def name_is_reference(self) -> bool:
@@ -492,7 +502,7 @@ def test262_test_record(self) -> Optional[test262.TestRecord]:
def script_metadata(self) -> Optional[List[Tuple[Text, Text]]]:
if self.name_is_worker or self.name_is_multi_global or self.name_is_window or self.name_is_extension:
regexp = js_meta_re
- elif self.name_is_webdriver:
+ elif self.name_is_webdriver or self.name_is_aamtest:
regexp = python_meta_re
elif self.name_is_test262:
if self.test262_test_record is None:
@@ -931,6 +941,9 @@ def possible_types(self) -> Set[Text]:
if self.name_is_webdriver:
return {WebDriverSpecTest.item_type}
+ if self.name_is_aamtest:
+ return {AccessibilityAPIMappingTest.item_type}
+
if self.name_is_visual:
return {VisualTest.item_type}
@@ -1021,6 +1034,16 @@ def manifest_items(self) -> Tuple[Text, List[ManifestItem]]:
timeout=self.timeout
)]
+ elif self.name_is_aamtest:
+ rv = AccessibilityAPIMappingTest.item_type, [
+ AccessibilityAPIMappingTest(
+ self.tests_root,
+ self.rel_path,
+ self.url_base,
+ self.rel_url,
+ timeout=self.timeout
+ )]
+
elif self.name_is_visual:
rv = VisualTest.item_type, [
VisualTest(
diff --git a/tools/mypy.ini b/tools/mypy.ini
index c7376bc4f144af..d39878b0b0c2bc 100644
--- a/tools/mypy.ini
+++ b/tools/mypy.ini
@@ -42,6 +42,9 @@ follow_imports = silent
follow_imports = silent
# Ignore missing or untyped libraries.
+[mypy-ApplicationServices.*]
+ignore_missing_imports = True
+
[mypy-Cocoa.*]
ignore_missing_imports = True
@@ -51,9 +54,15 @@ ignore_missing_imports = True
[mypy-Quartz.*]
ignore_missing_imports = True
+[mypy-comtypes.*]
+ignore_missing_imports = True
+
[mypy-dnslib.*]
ignore_missing_imports = True
+[mypy-gi.*]
+ignore_missing_imports = True
+
[mypy-marionette_driver.*]
ignore_missing_imports = True
diff --git a/tools/wpt/run.py b/tools/wpt/run.py
index fc51c67e9bf1ac..10f6a2156fd566 100644
--- a/tools/wpt/run.py
+++ b/tools/wpt/run.py
@@ -947,6 +947,10 @@ def setup_wptrunner(venv, **kwargs):
if not venv.skip_virtualenv_setup:
requirements = [os.path.join(wpt_root, "tools", "wptrunner", "requirements.txt")]
requirements.extend(setup_cls.requirements())
+
+ if "aamtest" in kwargs["test_types"]:
+ requirements.append(os.path.join(wpt_root, "tools", "wptrunner", "requirements_platform_accessibility.txt"))
+
venv.install_requirements(*requirements)
affected_revish = kwargs.get("affected")
diff --git a/tools/wpt/testfiles.py b/tools/wpt/testfiles.py
index 604014855f1caa..3a070057e13280 100644
--- a/tools/wpt/testfiles.py
+++ b/tools/wpt/testfiles.py
@@ -238,7 +238,7 @@ def affected_testfiles(files_changed: Iterable[Text],
nontests_changed = set(files_changed)
wpt_manifest = load_manifest(manifest_path, manifest_update)
- test_types = ["crashtest", "print-reftest", "reftest", "test262", "testharness", "wdspec"]
+ test_types = ["crashtest", "print-reftest", "reftest", "test262", "testharness", "wdspec", "aamtest"]
support_files = {os.path.join(wpt_root, path)
for _, path, _ in wpt_manifest.itertypes("support")}
wdspec_test_files = {os.path.join(wpt_root, path)
diff --git a/tools/wptrunner/requirements_platform_accessibility.txt b/tools/wptrunner/requirements_platform_accessibility.txt
new file mode 100644
index 00000000000000..b14331f7e25e93
--- /dev/null
+++ b/tools/wptrunner/requirements_platform_accessibility.txt
@@ -0,0 +1,8 @@
+comtypes==1.4.11; sys_platform == 'win32'
+PyGObject==3.50.1; sys_platform == 'linux' and python_version >= '3.9'
+PyGObject==3.48.2; sys_platform == 'linux' and python_version < '3.9'
+pyobjc-framework-Accessibility==11.1; sys_platform == 'darwin' and python_version >= '3.9'
+pyobjc-framework-ApplicationServices==11.1; sys_platform == 'darwin' and python_version >= '3.9'
+pyobjc==10.3.2; sys_platform == 'darwin' and python_version < '3.9'
+pyobjc-framework-Accessibility==10.3.2; sys_platform == 'darwin' and python_version < '3.9'
+pyobjc-framework-ApplicationServices==10.3.2; sys_platform == 'darwin' and python_version < '3.9'
diff --git a/tools/wptrunner/wptrunner/browsers/android_webview.py b/tools/wptrunner/wptrunner/browsers/android_webview.py
index d2812e86795226..f6657cd2328440 100644
--- a/tools/wptrunner/wptrunner/browsers/android_webview.py
+++ b/tools/wptrunner/wptrunner/browsers/android_webview.py
@@ -5,7 +5,7 @@
from .base import get_timeout_multiplier # noqa: F401
from .chrome import executor_kwargs as chrome_executor_kwargs
from .chrome_android import ChromeAndroidBrowserBase
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executorchrome import (ChromeDriverPrintRefTestExecutor, # noqa: F401
ChromeDriverTestharnessExecutor) # noqa: F401
from ..executors.executorwebdriver import (WebDriverCrashtestExecutor, # noqa: F401
@@ -18,7 +18,7 @@
"executor": {"testharness": "ChromeDriverTestharnessExecutor",
"reftest": "WebDriverRefTestExecutor",
"print-reftest": "ChromeDriverPrintRefTestExecutor",
- "wdspec": "WdspecExecutor",
+ "wdspec": "PytestExecutor",
"crashtest": "WebDriverCrashtestExecutor",
"test262": "ChromeDriverTestharnessExecutor"},
"browser_kwargs": "browser_kwargs",
diff --git a/tools/wptrunner/wptrunner/browsers/base.py b/tools/wptrunner/wptrunner/browsers/base.py
index dab13dfab79d4e..c44ff8e6d0c52b 100644
--- a/tools/wptrunner/wptrunner/browsers/base.py
+++ b/tools/wptrunner/wptrunner/browsers/base.py
@@ -321,7 +321,7 @@ def __init__(self,
self._supports_pac = supports_pac
self.base_path = base_path
- self.env = os.environ.copy() if env is None else env
+ self.env = {**os.environ, **env} if env else os.environ.copy()
self.webdriver_args = webdriver_args if webdriver_args is not None else []
self.init_deadline: Optional[float] = None
diff --git a/tools/wptrunner/wptrunner/browsers/chrome.py b/tools/wptrunner/wptrunner/browsers/chrome.py
index 6f7be4eebdbff5..1eca1e59fc7bd8 100644
--- a/tools/wptrunner/wptrunner/browsers/chrome.py
+++ b/tools/wptrunner/wptrunner/browsers/chrome.py
@@ -14,7 +14,7 @@
from .base import get_timeout_multiplier # noqa: F401
from .base import cmd_arg
from ..executors import executor_kwargs as base_executor_kwargs
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executorchrome import ( # noqa: F401
ChromeDriverPrintRefTestExecutor,
ChromeDriverRefTestExecutor,
@@ -32,7 +32,8 @@
"executor": {"testharness": "ChromeDriverTestharnessExecutor",
"reftest": "ChromeDriverRefTestExecutor",
"print-reftest": "ChromeDriverPrintRefTestExecutor",
- "wdspec": "WdspecExecutor",
+ "wdspec": "PytestExecutor",
+ "aamtest": "PytestExecutor",
"crashtest": "ChromeDriverCrashTestExecutor",
"test262": "ChromeDriverTestharnessExecutor"},
"browser_kwargs": "browser_kwargs",
@@ -50,10 +51,18 @@ def check_args(**kwargs):
def browser_kwargs(logger, test_type, run_info_data, config, **kwargs):
- return {"binary": kwargs["binary"],
- "webdriver_binary": kwargs["webdriver_binary"],
- "webdriver_args": kwargs.get("webdriver_args"),
- "leak_check": kwargs.get("leak_check", False)}
+ browser_kwargs = {
+ "binary": kwargs["binary"],
+ "webdriver_binary": kwargs["webdriver_binary"],
+ "webdriver_args": kwargs.get("webdriver_args"),
+ "leak_check": kwargs.get("leak_check", False),
+ }
+
+ if test_type == "aamtest":
+ # Necessary to force chrome to register in AT-SPI2.
+ browser_kwargs["env"] = {"ACCESSIBILITY_ENABLED": "1"}
+ return browser_kwargs
+
def executor_kwargs(logger, test_type, test_environment, run_info_data, subsuite,
@@ -225,6 +234,11 @@ def executor_kwargs(logger, test_type, test_environment, run_info_data, subsuite
if test_type == "wdspec":
executor_kwargs["binary_args"] = chrome_options["args"]
+ if test_type == "aamtest":
+ if "--force-renderer-accessibility" not in chrome_options["args"]:
+ chrome_options["args"].append("--force-renderer-accessibility")
+ executor_kwargs["binary_args"] = chrome_options["args"]
+
executor_kwargs["capabilities"] = capabilities
return executor_kwargs
@@ -259,11 +273,10 @@ def __init__(self,
self._require_webdriver_bidi: Optional[bool] = None
def restart_on_test_type_change(self, new_test_type: str, old_test_type: str) -> bool:
- # Restart the test runner when switch from/to wdspec tests. Wdspec test
- # is using a different protocol class so a restart is always needed.
- if "wdspec" in [old_test_type, new_test_type]:
- return True
- return False
+ # Restart the test runner when switch from/to wdspec or aamtest tests.
+ # These tests use a different protocol class so a restart is always needed.
+ wdspec_types = {"wdspec", "aamtest"}
+ return old_test_type in wdspec_types or new_test_type in wdspec_types
def create_output_handler(self, cmd: List[str]) -> OutputHandler:
return ChromeDriverOutputHandler(
diff --git a/tools/wptrunner/wptrunner/browsers/chrome_android.py b/tools/wptrunner/wptrunner/browsers/chrome_android.py
index 2b6218c6aa65a6..b375e543cb2f71 100644
--- a/tools/wptrunner/wptrunner/browsers/chrome_android.py
+++ b/tools/wptrunner/wptrunner/browsers/chrome_android.py
@@ -7,7 +7,7 @@
from .base import get_timeout_multiplier # noqa: F401
from .base import WebDriverBrowser # noqa: F401
from .chrome import executor_kwargs as chrome_executor_kwargs
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executorchrome import ChromeDriverPrintRefTestExecutor # noqa: F401
from ..executors.executorwebdriver import (WebDriverCrashtestExecutor, # noqa: F401
WebDriverTestharnessExecutor, # noqa: F401
@@ -20,7 +20,7 @@
"executor": {"testharness": "WebDriverTestharnessExecutor",
"reftest": "WebDriverRefTestExecutor",
"print-reftest": "ChromeDriverPrintRefTestExecutor",
- "wdspec": "WdspecExecutor",
+ "wdspec": "PytestExecutor",
"crashtest": "WebDriverCrashtestExecutor",
"test262": "WebDriverTestharnessExecutor"},
"browser_kwargs": "browser_kwargs",
diff --git a/tools/wptrunner/wptrunner/browsers/chrome_ios.py b/tools/wptrunner/wptrunner/browsers/chrome_ios.py
index 35ecc5b1a6d633..24074cfa00802d 100644
--- a/tools/wptrunner/wptrunner/browsers/chrome_ios.py
+++ b/tools/wptrunner/wptrunner/browsers/chrome_ios.py
@@ -3,7 +3,7 @@
from .base import WebDriverBrowser, require_arg
from .base import get_timeout_multiplier # noqa: F401
from ..executors import executor_kwargs as base_executor_kwargs
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executorwebdriver import (WebDriverCrashtestExecutor, # noqa: F401
WebDriverTestharnessExecutor, # noqa: F401
WebDriverRefTestExecutor) # noqa: F401
diff --git a/tools/wptrunner/wptrunner/browsers/chromium.py b/tools/wptrunner/wptrunner/browsers/chromium.py
index a459835a6d1a96..13f45f99dc2da1 100644
--- a/tools/wptrunner/wptrunner/browsers/chromium.py
+++ b/tools/wptrunner/wptrunner/browsers/chromium.py
@@ -6,7 +6,7 @@
from ..executors.executorwebdriver import (WebDriverTestharnessExecutor, # noqa: F401
WebDriverRefTestExecutor, # noqa: F401
WebDriverCrashtestExecutor) # noqa: F401
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executorchrome import ChromeDriverPrintRefTestExecutor # noqa: F401
@@ -16,7 +16,8 @@
"executor": {"testharness": "WebDriverTestharnessExecutor",
"reftest": "WebDriverRefTestExecutor",
"print-reftest": "ChromeDriverPrintRefTestExecutor",
- "wdspec": "WdspecExecutor",
+ "wdspec": "PytestExecutor",
+ "aamtest": "PytestExecutor",
"crashtest": "WebDriverCrashtestExecutor",
"test262": "WebDriverTestharnessExecutor"},
"browser_kwargs": "browser_kwargs",
diff --git a/tools/wptrunner/wptrunner/browsers/edge.py b/tools/wptrunner/wptrunner/browsers/edge.py
index 6145916f626096..804a9c76595954 100644
--- a/tools/wptrunner/wptrunner/browsers/edge.py
+++ b/tools/wptrunner/wptrunner/browsers/edge.py
@@ -4,7 +4,7 @@
from .base import cmd_arg
from .chrome import executor_kwargs as chrome_executor_kwargs
from ..executors.executorwebdriver import WebDriverCrashtestExecutor # noqa: F401
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executoredge import ( # noqa: F401
EdgeDriverPrintRefTestExecutor,
EdgeDriverRefTestExecutor,
@@ -18,9 +18,11 @@
"executor": {"testharness": "EdgeDriverTestharnessExecutor",
"reftest": "EdgeDriverRefTestExecutor",
"print-reftest": "EdgeDriverPrintRefTestExecutor",
- "wdspec": "WdspecExecutor",
+ "wdspec": "PytestExecutor",
+ "aamtest": "PytestExecutor",
"crashtest": "WebDriverCrashtestExecutor",
"test262": "EdgeDriverTestharnessExecutor"},
+
"browser_kwargs": "browser_kwargs",
"executor_kwargs": "executor_kwargs",
"env_extras": "env_extras",
diff --git a/tools/wptrunner/wptrunner/browsers/epiphany.py b/tools/wptrunner/wptrunner/browsers/epiphany.py
index 5daf9a377e70e9..4ed6b37a72d3fe 100644
--- a/tools/wptrunner/wptrunner/browsers/epiphany.py
+++ b/tools/wptrunner/wptrunner/browsers/epiphany.py
@@ -6,7 +6,7 @@
maybe_add_args)
from .webkit import WebKitBrowser # noqa: F401
from ..executors import executor_kwargs as base_executor_kwargs
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executorwebdriver import (WebDriverTestharnessExecutor, # noqa: F401
WebDriverRefTestExecutor, # noqa: F401
WebDriverCrashtestExecutor) # noqa: F401
@@ -18,7 +18,7 @@
"browser_kwargs": "browser_kwargs",
"executor": {"testharness": "WebDriverTestharnessExecutor",
"reftest": "WebDriverRefTestExecutor",
- "wdspec": "WdspecExecutor",
+ "wdspec": "PytestExecutor",
"crashtest": "WebDriverCrashtestExecutor",
"test262": "WebDriverTestharnessExecutor"},
"executor_kwargs": "executor_kwargs",
diff --git a/tools/wptrunner/wptrunner/browsers/firefox.py b/tools/wptrunner/wptrunner/browsers/firefox.py
index 609afc02e253ca..816f71825889c2 100644
--- a/tools/wptrunner/wptrunner/browsers/firefox.py
+++ b/tools/wptrunner/wptrunner/browsers/firefox.py
@@ -33,19 +33,21 @@
from ..executors.executormarionette import (MarionetteTestharnessExecutor, # noqa: F401
MarionetteRefTestExecutor, # noqa: F401
MarionettePrintRefTestExecutor, # noqa: F401
- MarionetteWdspecExecutor, # noqa: F401
+ MarionettePytestExecutor, # noqa: F401
MarionetteCrashtestExecutor) # noqa: F401
__wptrunner__ = {"product": "firefox",
"check_args": "check_args",
"browser": {None: "FirefoxBrowser",
- "wdspec": "FirefoxWdSpecBrowser"},
+ "wdspec": "FirefoxPytestBrowser",
+ "aamtest": "FirefoxPytestBrowser"},
"executor": {"crashtest": "MarionetteCrashtestExecutor",
"testharness": "MarionetteTestharnessExecutor",
"reftest": "MarionetteRefTestExecutor",
"print-reftest": "MarionettePrintRefTestExecutor",
- "wdspec": "MarionetteWdspecExecutor",
+ "wdspec": "MarionettePytestExecutor",
+ "aamtest": "MarionettePytestExecutor",
"test262": "MarionetteTestharnessExecutor"},
"browser_kwargs": "browser_kwargs",
"executor_kwargs": "executor_kwargs",
@@ -72,7 +74,7 @@ def get_timeout_multiplier(test_type, run_info_data, **kwargs):
return 4 * multiplier
else:
return 2 * multiplier
- elif test_type == "wdspec":
+ elif test_type in ("wdspec", "aamtest"):
if (run_info_data.get("asan") or
run_info_data.get("ccov") or
run_info_data.get("debug")):
@@ -127,7 +129,7 @@ def browser_kwargs(logger, test_type, run_info_data, config, subsuite, **kwargs)
"gmp_path": kwargs["gmp_path"] if "gmp_path" in kwargs else None,
"debug_test": kwargs["debug_test"]}
- if test_type == "wdspec":
+ if test_type in ("wdspec", "aamtest"):
browser_kwargs["webdriver_binary"] = kwargs["webdriver_binary"]
browser_kwargs["webdriver_args"] = kwargs["webdriver_args"].copy()
@@ -146,6 +148,15 @@ def browser_kwargs(logger, test_type, run_info_data, config, subsuite, **kwargs)
browser_kwargs["test_type"] = test_type
browser_kwargs["timeout_multiplier"] = get_timeout_multiplier(test_type, run_info_data, **kwargs)
+ if test_type == "aamtest":
+ # Enable accessibility in the browser.
+ if ('accessibility.force_disabled', '-1') not in browser_kwargs["extra_prefs"]:
+ browser_kwargs["extra_prefs"].append(('accessibility.force_disabled', '-1'))
+ # Cache all attributes immediately for testing.
+ if ('accessibility.enable_all_cache_domains', 'true') not in browser_kwargs["extra_prefs"]:
+ browser_kwargs["extra_prefs"].append(('accessibility.enable_all_cache_domains', 'true'))
+
+
browser_kwargs["extra_prefs"].extend(subsuite.config.get("prefs", []))
return browser_kwargs
@@ -173,7 +184,8 @@ def executor_kwargs(logger, test_type, test_environment, run_info_data,
else:
cache_screenshots = major_version < 14
executor_kwargs["cache_screenshots"] = cache_screenshots
- if test_type == "wdspec":
+
+ if test_type in ("wdspec", "aamtest"):
options = {"args": []}
if kwargs["binary"]:
executor_kwargs["webdriver_args"].extend(["--binary", kwargs["binary"]])
@@ -773,7 +785,7 @@ def _get_default_prefs(self):
if self.test_type == "print-reftest":
prefs["print.always_print_silent"] = True
- if self.test_type == "wdspec":
+ if self.test_type in ("wdspec", "aamtest"):
prefs.update(
{
"remote.prefs.recommended": True,
@@ -952,7 +964,7 @@ def check_crash(self, process, test):
self.stackwalk_binary)
-class FirefoxWdSpecBrowser(WebDriverBrowser):
+class FirefoxPytestBrowser(WebDriverBrowser):
def __init__(self, logger, binary, package_name, prefs_root, webdriver_binary, webdriver_args,
extra_prefs=None, debug_info=None, symbols_path=None, stackwalk_binary=None,
certutil_binary=None, ca_certificate_path=None, e10s=False,
@@ -976,6 +988,7 @@ def __init__(self, logger, binary, package_name, prefs_root, webdriver_binary, w
self.env = self.get_env(binary, debug_info, headless, gmp_path, chaos_mode_flags, e10s)
+ # Todo: need test type to use "aam" test in profile_creator_cls
profile_creator = profile_creator_cls(logger,
prefs_root,
config,
diff --git a/tools/wptrunner/wptrunner/browsers/firefox_android.py b/tools/wptrunner/wptrunner/browsers/firefox_android.py
index 68333929c3c3b4..7d44716e185981 100644
--- a/tools/wptrunner/wptrunner/browsers/firefox_android.py
+++ b/tools/wptrunner/wptrunner/browsers/firefox_android.py
@@ -13,7 +13,7 @@
from ..executors.executormarionette import (MarionetteTestharnessExecutor, # noqa: F401
MarionetteRefTestExecutorAndroid, # noqa: F401
MarionetteCrashtestExecutor, # noqa: F401
- MarionetteWdspecExecutor) # noqa: F401
+ MarionettePytestExecutor) # noqa: F401
from .base import (Browser,
ExecutorBrowser)
from .firefox import (get_timeout_multiplier, # noqa: F401
@@ -21,7 +21,7 @@
run_info_extras as fx_run_info_extras,
update_properties, # noqa: F401
executor_kwargs as fx_executor_kwargs, # noqa: F401
- FirefoxWdSpecBrowser,
+ FirefoxPytestBrowser,
ProfileCreator as FirefoxProfileCreator)
@@ -32,7 +32,7 @@
"executor": {"testharness": "MarionetteTestharnessExecutor",
"reftest": "MarionetteRefTestExecutorAndroid",
"crashtest": "MarionetteCrashtestExecutor",
- "wdspec": "MarionetteWdspecExecutor",
+ "wdspec": "MarionettePytestExecutor",
"test262": "MarionetteTestharnessExecutor"},
"browser_kwargs": "browser_kwargs",
"executor_kwargs": "executor_kwargs",
@@ -398,7 +398,7 @@ def check_crash(self, process, test):
return False
-class FirefoxAndroidWdSpecBrowser(FirefoxWdSpecBrowser):
+class FirefoxAndroidWdSpecBrowser(FirefoxPytestBrowser):
def __init__(self, logger, config=None, device_serial=None, adb_binary=None, **kwargs):
if "profile_creator_cls" not in kwargs:
diff --git a/tools/wptrunner/wptrunner/browsers/headless_shell.py b/tools/wptrunner/wptrunner/browsers/headless_shell.py
index d0fc8ca7166c35..593d5b5297c9e9 100644
--- a/tools/wptrunner/wptrunner/browsers/headless_shell.py
+++ b/tools/wptrunner/wptrunner/browsers/headless_shell.py
@@ -5,7 +5,7 @@
from .chrome import ChromeBrowser # noqa: F401
from .chrome import browser_kwargs as browser_kwargs # noqa: F401
from .chrome import executor_kwargs as chrome_executor_kwargs
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executorchrome import ( # noqa: F401
ChromeDriverCrashTestExecutor,
ChromeDriverPrintRefTestExecutor,
@@ -23,7 +23,7 @@
"reftest": "ChromeDriverRefTestExecutor",
"test262": "ChromeDriverTestharnessExecutor",
"testharness": "ChromeDriverTestharnessExecutor",
- "wdspec": "WdspecExecutor",
+ "wdspec": "PytestExecutor",
},
"browser_kwargs": "browser_kwargs",
"executor_kwargs": "executor_kwargs",
diff --git a/tools/wptrunner/wptrunner/browsers/ladybird.py b/tools/wptrunner/wptrunner/browsers/ladybird.py
index 7b1eff5e6b3167..453e45f990fdcd 100644
--- a/tools/wptrunner/wptrunner/browsers/ladybird.py
+++ b/tools/wptrunner/wptrunner/browsers/ladybird.py
@@ -4,7 +4,7 @@
get_timeout_multiplier, # noqa: F401
require_arg)
from ..executors import executor_kwargs as base_executor_kwargs
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executorwebdriver import (WebDriverTestharnessExecutor, # noqa: F401
WebDriverRefTestExecutor, # noqa: F401
WebDriverCrashtestExecutor) # noqa: F401
@@ -21,7 +21,7 @@
"executor": {
"testharness": "WebDriverTestharnessExecutor",
"reftest": "WebDriverRefTestExecutor",
- "wdspec": "WdspecExecutor",
+ "wdspec": "PytestExecutor",
"crashtest": "WebDriverCrashtestExecutor",
"test262": "WebDriverTestharnessExecutor"
}
diff --git a/tools/wptrunner/wptrunner/browsers/opera.py b/tools/wptrunner/wptrunner/browsers/opera.py
index e2855b3c26fd0c..49c4e6ec65e672 100644
--- a/tools/wptrunner/wptrunner/browsers/opera.py
+++ b/tools/wptrunner/wptrunner/browsers/opera.py
@@ -4,7 +4,7 @@
from .base import get_timeout_multiplier # noqa: F401
from .chrome import ChromeBrowser
from ..executors import executor_kwargs as base_executor_kwargs
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executorselenium import (SeleniumTestharnessExecutor, # noqa: F401
SeleniumRefTestExecutor) # noqa: F401
@@ -14,7 +14,7 @@
"browser": "OperaBrowser",
"executor": {"testharness": "SeleniumTestharnessExecutor",
"reftest": "SeleniumRefTestExecutor",
- "wdspec": "WdspecExecutor",
+ "wdspec": "PytestExecutor",
"test262": "SeleniumTestharnessExecutor"},
"browser_kwargs": "browser_kwargs",
"executor_kwargs": "executor_kwargs",
diff --git a/tools/wptrunner/wptrunner/browsers/safari.py b/tools/wptrunner/wptrunner/browsers/safari.py
index ca88da8e9ea97c..81af4ecf3cd0bf 100644
--- a/tools/wptrunner/wptrunner/browsers/safari.py
+++ b/tools/wptrunner/wptrunner/browsers/safari.py
@@ -11,7 +11,7 @@
from .base import WebDriverBrowser, require_arg
from .base import get_timeout_multiplier # noqa: F401
from ..executors import executor_kwargs as base_executor_kwargs
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executorwebdriver import (WebDriverTestharnessExecutor, # noqa: F401
WebDriverRefTestExecutor, # noqa: F401
WebDriverCrashtestExecutor) # noqa: F401
@@ -22,7 +22,7 @@
"browser": "SafariBrowser",
"executor": {"testharness": "WebDriverTestharnessExecutor",
"reftest": "WebDriverRefTestExecutor",
- "wdspec": "WdspecExecutor",
+ "wdspec": "PytestExecutor",
"crashtest": "WebDriverCrashtestExecutor",
"test262": "WebDriverTestharnessExecutor"},
"browser_kwargs": "browser_kwargs",
diff --git a/tools/wptrunner/wptrunner/browsers/servo.py b/tools/wptrunner/wptrunner/browsers/servo.py
index 950a921ac7d2f5..3b081fe20d7955 100644
--- a/tools/wptrunner/wptrunner/browsers/servo.py
+++ b/tools/wptrunner/wptrunner/browsers/servo.py
@@ -11,7 +11,7 @@
require_arg)
from .base import get_timeout_multiplier # noqa: F401
from ..executors import executor_kwargs as base_executor_kwargs
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executorservo import (ServoTestharnessExecutor, # noqa: F401
ServoRefTestExecutor, # noqa: F401
ServoCrashtestExecutor) # noqa: F401
@@ -26,7 +26,8 @@
"testharness": "ServoTestharnessExecutor",
"reftest": "ServoRefTestExecutor",
"crashtest": "ServoCrashtestExecutor",
- "wdspec": "WdspecExecutor",
+ "wdspec": "PytestExecutor",
+ "aamtest": "PytestExecutor",
"test262": "ServoTestharnessExecutor",
},
"browser_kwargs": "browser_kwargs",
diff --git a/tools/wptrunner/wptrunner/browsers/servo_legacy.py b/tools/wptrunner/wptrunner/browsers/servo_legacy.py
index 801f29e088c6f8..fbd269083350e1 100644
--- a/tools/wptrunner/wptrunner/browsers/servo_legacy.py
+++ b/tools/wptrunner/wptrunner/browsers/servo_legacy.py
@@ -5,7 +5,7 @@
from .base import ExecutorBrowser, NullBrowser, WebDriverBrowser, require_arg
from .base import get_timeout_multiplier # noqa: F401
from ..executors import executor_kwargs as base_executor_kwargs
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executorservolegacy import (ServoLegacyCrashtestExecutor, # noqa: F401
ServoLegacyTestharnessExecutor, # noqa: F401
ServoLegacyRefTestExecutor) # noqa: F401
@@ -22,7 +22,7 @@
"crashtest": "ServoLegacyCrashtestExecutor",
"testharness": "ServoLegacyTestharnessExecutor",
"reftest": "ServoLegacyRefTestExecutor",
- "wdspec": "WdspecExecutor",
+ "wdspec": "PytestExecutor",
},
"browser_kwargs": "browser_kwargs",
"executor_kwargs": "executor_kwargs",
diff --git a/tools/wptrunner/wptrunner/browsers/webkit.py b/tools/wptrunner/wptrunner/browsers/webkit.py
index 183c685e64b62e..d145a80d263e46 100644
--- a/tools/wptrunner/wptrunner/browsers/webkit.py
+++ b/tools/wptrunner/wptrunner/browsers/webkit.py
@@ -3,7 +3,7 @@
from .base import WebDriverBrowser, require_arg
from .base import get_timeout_multiplier, certificate_domain_list # noqa: F401
from ..executors import executor_kwargs as base_executor_kwargs
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executorwebdriver import (WebDriverTestharnessExecutor, # noqa: F401
WebDriverRefTestExecutor, # noqa: F401
WebDriverCrashtestExecutor) # noqa: F401
@@ -15,7 +15,7 @@
"browser_kwargs": "browser_kwargs",
"executor": {"testharness": "WebDriverTestharnessExecutor",
"reftest": "WebDriverRefTestExecutor",
- "wdspec": "WdspecExecutor",
+ "wdspec": "PytestExecutor",
"crashtest": "WebDriverCrashtestExecutor",
"test262": "WebDriverTestharnessExecutor"},
"executor_kwargs": "executor_kwargs",
diff --git a/tools/wptrunner/wptrunner/browsers/webkitgtk_minibrowser.py b/tools/wptrunner/wptrunner/browsers/webkitgtk_minibrowser.py
index da1ea60944eafd..00b00896a9815d 100644
--- a/tools/wptrunner/wptrunner/browsers/webkitgtk_minibrowser.py
+++ b/tools/wptrunner/wptrunner/browsers/webkitgtk_minibrowser.py
@@ -6,7 +6,7 @@
maybe_add_args)
from .webkit import WebKitBrowser
from ..executors import executor_kwargs as base_executor_kwargs
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executorwebdriver import (WebDriverTestharnessExecutor, # noqa: F401
WebDriverRefTestExecutor, # noqa: F401
WebDriverCrashtestExecutor) # noqa: F401
@@ -17,7 +17,7 @@
"browser_kwargs": "browser_kwargs",
"executor": {"testharness": "WebDriverTestharnessExecutor",
"reftest": "WebDriverRefTestExecutor",
- "wdspec": "WdspecExecutor",
+ "wdspec": "PytestExecutor",
"crashtest": "WebDriverCrashtestExecutor",
"test262": "WebDriverTestharnessExecutor"},
"executor_kwargs": "executor_kwargs",
diff --git a/tools/wptrunner/wptrunner/browsers/wpewebkit_minibrowser.py b/tools/wptrunner/wptrunner/browsers/wpewebkit_minibrowser.py
index 2f4d04b5f6564e..4ce370745afddc 100644
--- a/tools/wptrunner/wptrunner/browsers/wpewebkit_minibrowser.py
+++ b/tools/wptrunner/wptrunner/browsers/wpewebkit_minibrowser.py
@@ -6,7 +6,7 @@
maybe_add_args)
from .webkit import WebKitBrowser
from ..executors import executor_kwargs as base_executor_kwargs
-from ..executors.base import WdspecExecutor # noqa: F401
+from ..executors.base import PytestExecutor # noqa: F401
from ..executors.executorwebdriver import (WebDriverTestharnessExecutor, # noqa: F401
WebDriverRefTestExecutor, # noqa: F401
WebDriverCrashtestExecutor) # noqa: F401
@@ -17,7 +17,7 @@
"browser_kwargs": "browser_kwargs",
"executor": {"testharness": "WebDriverTestharnessExecutor",
"reftest": "WebDriverRefTestExecutor",
- "wdspec": "WdspecExecutor",
+ "wdspec": "PytestExecutor",
"crashtest": "WebDriverCrashtestExecutor",
"test262": "WebDriverTestharnessExecutor"},
"executor_kwargs": "executor_kwargs",
diff --git a/tools/wptrunner/wptrunner/executors/base.py b/tools/wptrunner/wptrunner/executors/base.py
index fc4cb1abfaa301..10b0e16dcae14a 100644
--- a/tools/wptrunner/wptrunner/executors/base.py
+++ b/tools/wptrunner/wptrunner/executors/base.py
@@ -17,7 +17,7 @@
from . import pytestrunner
from .actions import actions
from .asyncactions import async_actions
-from .protocol import Protocol, WdspecProtocol, merge_dicts
+from .protocol import Protocol, PytestProtocol, merge_dicts
here = os.path.dirname(__file__)
@@ -47,7 +47,7 @@ def executor_kwargs(test_type, test_environment, run_info_data, subsuite, **kwar
executor_kwargs["screenshot_cache"] = screenshot_cache
executor_kwargs["reftest_screenshot"] = kwargs["reftest_screenshot"]
- if test_type == "wdspec":
+ if test_type in ("wdspec", "aamtest"):
executor_kwargs["binary"] = kwargs["binary"]
executor_kwargs["binary_args"] = kwargs["binary_args"].copy()
executor_kwargs["webdriver_binary"] = kwargs["webdriver_binary"]
@@ -674,9 +674,9 @@ def get_screenshot_list(self, node, viewport_size, dpi, page_ranges):
return success, data
-class WdspecExecutor(TestExecutor):
+class PytestExecutor(TestExecutor):
convert_result = pytest_result_converter
- protocol_cls: ClassVar[Type[Protocol]] = WdspecProtocol
+ protocol_cls: ClassVar[Type[Protocol]] = PytestProtocol
def __init__(
self,
@@ -743,7 +743,7 @@ def on_environment_change(self, new_environment):
def do_test(self, test):
timeout = test.timeout * self.timeout_multiplier + self.extra_timeout
- success, data = WdspecRun(self.do_wdspec,
+ success, data = PytestRun(self.do_pytest,
test.abs_path,
timeout).run()
@@ -752,7 +752,7 @@ def do_test(self, test):
return (test.make_result(*data), [])
- def do_wdspec(self, path, timeout):
+ def do_pytest(self, path, timeout):
session_config = {
"host": self.browser.host,
"port": self.browser.port,
@@ -775,7 +775,7 @@ def do_wdspec(self, path, timeout):
timeout=timeout)
-class WdspecRun:
+class PytestRun:
def __init__(self, func, path, timeout):
self.func = func
self.result = (None, None)
diff --git a/tools/wptrunner/wptrunner/executors/executormarionette.py b/tools/wptrunner/wptrunner/executors/executormarionette.py
index 16cfbb703149eb..76355e695dcfe3 100644
--- a/tools/wptrunner/wptrunner/executors/executormarionette.py
+++ b/tools/wptrunner/wptrunner/executors/executormarionette.py
@@ -20,7 +20,7 @@
RefTestImplementation,
TestharnessExecutor,
TimedRunner,
- WdspecExecutor,
+ PytestExecutor,
get_pages,
strip_server)
from .protocol import (AccessibilityProtocolPart,
@@ -1469,7 +1469,7 @@ def _render(self, protocol, url, timeout):
return screenshots
-class MarionetteWdspecExecutor(WdspecExecutor):
+class MarionettePytestExecutor(PytestExecutor):
def __init__(self, logger, browser, *args, **kwargs):
super().__init__(logger, browser, *args, **kwargs)
diff --git a/tools/wptrunner/wptrunner/executors/protocol.py b/tools/wptrunner/wptrunner/executors/protocol.py
index 8e124923d86ec5..64feb981a47d41 100644
--- a/tools/wptrunner/wptrunner/executors/protocol.py
+++ b/tools/wptrunner/wptrunner/executors/protocol.py
@@ -1216,7 +1216,7 @@ def after_connect(self):
pass
-class WdspecProtocol(ConnectionlessProtocol):
+class PytestProtocol(ConnectionlessProtocol):
implements = [ConnectionlessBaseProtocolPart]
def __init__(self, executor, browser):
diff --git a/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py b/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py
index df7b7c40132267..ed5ac3492864e2 100644
--- a/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py
+++ b/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py
@@ -52,6 +52,7 @@ def run(path, server_config, session_config, timeout=0):
config = session_config.copy()
config["wptserve"] = server_config.as_dict()
+ config["timeout"] = timeout
with open(config_path, "w") as f:
json.dump(config, f)
@@ -144,6 +145,11 @@ def record_error(self, report, message):
self.record(report.nodeid, "ERROR", message, report.longrepr)
def record_skip(self, report):
+ # Do not record a not applicable subtest, used
+ # for an `aamtest` subtest that is not applicable
+ # to the current platform.
+ if "NOT_APPLICABLE" in report.longrepr[2]:
+ return
self.record(
report.nodeid,
"ERROR",
diff --git a/tools/wptrunner/wptrunner/wpttest.py b/tools/wptrunner/wptrunner/wpttest.py
index b05adaff2f89a5..cc9e30a8af5706 100644
--- a/tools/wptrunner/wptrunner/wpttest.py
+++ b/tools/wptrunner/wptrunner/wpttest.py
@@ -10,8 +10,7 @@
from .wptmanifest.parser import atoms
atom_reset = atoms["Reset"]
-enabled_tests = {"testharness", "reftest", "wdspec", "crashtest", "print-reftest", "test262"}
-
+enabled_tests = {"testharness", "reftest", "wdspec", "crashtest", "print-reftest", "test262", "aamtest"}
class Result(ABC):
default_expected: ClassVar[str]
@@ -75,7 +74,17 @@ class WdspecResult(Result):
class WdspecSubtestResult(SubtestResult):
default_expected = "PASS"
- statuses = {"PASS", "FAIL", "ERROR"}
+ statuses = {"PASS", "FAIL", "ERROR", "PRECONDITION_FAILED"}
+
+
+class AamSpecResult(Result):
+ default_expected = "OK"
+ statuses = {"OK", "ERROR", "INTERNAL-ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"}
+
+
+class AamSpecSubtestResult(SubtestResult):
+ default_expected = "PASS"
+ statuses = {"PASS", "FAIL", "ERROR", "PRECONDITION_FAILED"}
class CrashtestResult(Result):
@@ -736,6 +745,15 @@ class WdspecTest(Test):
long_timeout = 180 # 3 minutes
+class AamSpecTest(Test):
+ result_cls = AamSpecResult
+ subtest_result_cls = AamSpecSubtestResult
+ test_type = "aamtest"
+
+ default_timeout = 25
+ long_timeout = 180 # 3 minutes
+
+
class CrashTest(Test):
result_cls = CrashtestResult
test_type = "crashtest"
@@ -765,6 +783,7 @@ def from_manifest(cls, manifest_file, manifest_item, inherit_metadata, test_meta
"print-reftest": PrintReftestTest,
"testharness": TestharnessTest,
"wdspec": WdspecTest,
+ "aamtest": AamSpecTest,
"crashtest": CrashTest,
"test262": Test262Test}