diff --git a/.taskcluster.yml b/.taskcluster.yml index 5bef41d7d97406..d120d54d3960f1 100644 --- a/.taskcluster.yml +++ b/.taskcluster.yml @@ -57,7 +57,7 @@ tasks: owner: ${owner} source: ${event.repository.clone_url} payload: - image: ghcr.io/web-platform-tests/wpt:2 + image: ghcr.io/web-platform-tests/wpt:3 maxRunTime: 7200 artifacts: public/results: diff --git a/core-aam/aamtests/__init__.py b/core-aam/aamtests/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/core-aam/aamtests/attribute/aria_autocomplete_inline_list_both.py b/core-aam/aamtests/attribute/aria_autocomplete_inline_list_both.py new file mode 100644 index 00000000000000..540f7b8719ed80 --- /dev/null +++ b/core-aam/aamtests/attribute/aria_autocomplete_inline_list_both.py @@ -0,0 +1,33 @@ +import pytest + +# Testing: https://w3c.github.io/core-aam/#ariaAutocompleteInlineListBoth + +TEST_HTML = { + "both": "", + "inline": "", + "list": "", +} + +@pytest.mark.parametrize("test_value,test_html", TEST_HTML.items(), ids=TEST_HTML.keys()) +def test_atspi(atspi, session, inline, test_value, test_html): + session.url = inline(test_html) + + # Spec: + # Object Attribute: autocomplete: + # State: STATE_SUPPORTS_AUTOCOMPLETION + + node = atspi.find_node("test", session.url) + assert f"autocomplete:{test_value}" in atspi.Accessible.get_attributes_as_array(node) + assert "STATE_SUPPORTS_AUTOCOMPLETION" in atspi.get_state_list_helper(node) + +# Intentionally no AX API test. AX API does not surface this attribute. + +# @pytest.mark.parametrize("test_value,test_html", TEST_HTML.items(), ids=TEST_HTML.keys()) +# def test_ia2(ia2, session, inline, test_value, test_html): +# session.url = inline(test_html) +# +# # Spec: +# # Object Attribute: autocomplete: +# # State: IA2_STATE_SUPPORTS_AUTOCOMPLETION + +# Intentionally no UIA test. UIA does not surface this attribute. diff --git a/core-aam/aamtests/attribute/aria_braillelabel.py b/core-aam/aamtests/attribute/aria_braillelabel.py new file mode 100644 index 00000000000000..34896fc47de769 --- /dev/null +++ b/core-aam/aamtests/attribute/aria_braillelabel.py @@ -0,0 +1,30 @@ +# Testing: https://w3c.github.io/core-aam/#ariaBraillelabel + +TEST_HTML = "" + +def test_atspi(atspi, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Object Attribute: braillelabel: + + node = atspi.find_node("test", session.url) + assert "braillelabel:foobar" in atspi.Accessible.get_attributes_as_array(node) + +# def test_axapi(axapi, session, inline): +# session.url = inline(TEST_HTML) +# +# # Spec: +# # AXBrailleLabel: + +# def test_ia2(ia2, session, inline): +# session.url = inline(TEST_HTML) +# +# # Spec: +# # Object Attribute: braillelabel: + +# def test_uia(uia, session, inline): +# session.url = inline(TEST_HTML) +# +# # Spec: +# # Property: AriaProperties.braillelabel: diff --git a/core-aam/aamtests/attribute/aria_error_message.py b/core-aam/aamtests/attribute/aria_error_message.py new file mode 100644 index 00000000000000..4e4a1e03332831 --- /dev/null +++ b/core-aam/aamtests/attribute/aria_error_message.py @@ -0,0 +1,39 @@ +# Testing: https://w3c.github.io/core-aam/#ariaErrorMessage + +TEST_HTML = "
hello world
" + +def test_atspi(atspi, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Relation: RELATION_ERROR_MESSAGE: points to accessible nodes matching IDREFs, if the referenced objects are in the accessibility tree + # Reverse Relation: RELATION_ERROR_FOR: points to element + + node = atspi.find_node("test", session.url) + relations = atspi.get_relations_dictionary_helper(node) + assert 'RELATION_ERROR_MESSAGE' in relations + assert 'error' in relations['RELATION_ERROR_MESSAGE'] + reverse_node = atspi.find_node('error', session.url) + reverse_relations = atspi.get_relations_dictionary_helper(reverse_node) + assert 'RELATION_ERROR_FOR' in reverse_relations + assert 'test' in reverse_relations['RELATION_ERROR_FOR'] + +# def test_axapi(axapi, session, inline): +# session.url = inline(TEST_HTML) +# +# # Spec: +# # Property: AXErrorMessageElements: pointers to accessible nodes matching IDREFs + +# def test_ia2(ia2, session, inline): +# session.url = inline(TEST_HTML) +# +# # Spec: +# # Relation: IA2_RELATION_ERROR: points to accessible nodes matching IDREFs, if the referenced objects are in the accessibility tree +# # Reverse Relation: IA2_RELATION_ERROR_FOR: points to element +# # See also: Mapping Additional Relations + +# def test_uia(uia, session, inline): +# session.url = inline(TEST_HTML) +# +# # Spec: +# # Property: ControllerFor: pointer to the target accessible object diff --git a/core-aam/aamtests/conftest.py b/core-aam/aamtests/conftest.py new file mode 100644 index 00000000000000..7e7cb00234a294 --- /dev/null +++ b/core-aam/aamtests/conftest.py @@ -0,0 +1,12 @@ +import sys +from pathlib import Path + +# Add webdriver to the Python path so we can import the fixtures +webdriver_tests_path = Path(__file__).parent.parent.parent / "webdriver" +sys.path.insert(0, str(webdriver_tests_path)) + +pytest_plugins = ( + "tests.support.fixtures", + "tests.support.classic.fixtures", + "aamtests.support.fixtures_a11y_api", +) diff --git a/core-aam/aamtests/role/blockquote.py b/core-aam/aamtests/role/blockquote.py new file mode 100644 index 00000000000000..2d2793aa4b7730 --- /dev/null +++ b/core-aam/aamtests/role/blockquote.py @@ -0,0 +1,45 @@ +# Testing: https://w3c.github.io/core-aam/#role-map-blockquote + +TEST_HTML = "
content
" + +def test_atspi(atspi, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Role: ROLE_BLOCK_QUOTE + + node = atspi.find_node("test", session.url) + assert atspi.Accessible.get_role(node) == atspi.Role.BLOCK_QUOTE + +def test_axapi(axapi, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # AXRole: AXGroup + # AXSubrole: + + node = axapi.find_node("test", session.url) + role = axapi.AXUIElementCopyAttributeValue(node, "AXRole", None)[1] + assert role == "AXGroup" + role = axapi.AXUIElementCopyAttributeValue(node, "AXSubrole", None)[1] + assert role == "AXUnknown" + +def test_ia2(ia2, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Role: ROLE_SYSTEM_GROUPING + # Role: IA2_ROLE_BLOCK_QUOTE + + node = ia2.find_node("test", session.url) + assert ia2.get_role(node) == "IA2_ROLE_BLOCK_QUOTE" + assert ia2.get_msaa_role(node) == "ROLE_SYSTEM_GROUPING" + + +# def test_uia(uia, session, inline): +# session.url = inline(TEST_HTML) +# node = uia.find_node("test", session.url) +# +# # Spec: +# # Control Type: Group +# # Localized Control Type: blockquote diff --git a/core-aam/aamtests/role/button.py b/core-aam/aamtests/role/button.py new file mode 100644 index 00000000000000..3b2d82e54c214b --- /dev/null +++ b/core-aam/aamtests/role/button.py @@ -0,0 +1,42 @@ +import pytest + +# Testing: https://w3c.github.io/core-aam/#role-map-button + +TEST_HTML = { + "no-attributes": "
click me
", + "aria-pressed-undefined": "
click me
", + "aria-haspopup-undefined": "
click me
", + "aria-haspopup-false": "
click me
", +} + +@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_PUSH_BUTTON + + node = atspi.find_node("test", session.url) + assert atspi.Accessible.get_role(node) == atspi.Role.PUSH_BUTTON + +# @pytest.mark.parametrize("test_html", TEST_HTML.values(), ids=TEST_HTML.keys()) +# def test_axapi(axapi, session, inline): +# session.url = inline(test_html) +# +# # Spec: +# # AXRole: AXButton +# # AXSubrole: + +# @pytest.mark.parametrize("test_html", TEST_HTML.values(), ids=TEST_HTML.keys()) +# def test_ia2(ia2, session, inline): +# session.url = inline(test_html) +# +# # Spec: +# # Role: ROLE_SYSTEM_PUSHBUTTON + +# @pytest.mark.parametrize("test_html", TEST_HTML.values(), ids=TEST_HTML.keys()) +# def test_uia(uia, session, inline): +# session.url = inline(test_html) +# +# # Spec: +# # Control Type: Button diff --git a/core-aam/aamtests/role/button_haspopup.py b/core-aam/aamtests/role/button_haspopup.py new file mode 100644 index 00000000000000..f655ee77fd1bac --- /dev/null +++ b/core-aam/aamtests/role/button_haspopup.py @@ -0,0 +1,44 @@ +import pytest + +# Testing: https://w3c.github.io/core-aam/#role-map-button-haspopup + +TEST_HTML = { + "true": "
click me
", + "menu": "
click me
", + "listbox": "
click me
", + "tree": "
click me
", + "grid": "
click me
", + "dialog": "
click me
" +} + +@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_PUSH_BUTTON + + node = atspi.find_node("test", session.url) + assert atspi.Accessible.get_role(node) == atspi.Role.PUSH_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: AXPopUpButton +# # AXSubrole: + +# @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_BUTTONMENU + +# @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/role/button_pressed.py b/core-aam/aamtests/role/button_pressed.py new file mode 100644 index 00000000000000..9bd6ea749c8165 --- /dev/null +++ b/core-aam/aamtests/role/button_pressed.py @@ -0,0 +1,41 @@ +import pytest + +# Testing: https://w3c.github.io/core-aam/#role-map-button-pressed + +TEST_HTML = { + "true": "
press me
", + "false": "
press me
" +} + +@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}