diff --git a/tools/wptrunner/wptrunner/executors/executorchrome.py b/tools/wptrunner/wptrunner/executors/executorchrome.py index 1898695fb3ff2e..58422bc92f83d7 100644 --- a/tools/wptrunner/wptrunner/executors/executorchrome.py +++ b/tools/wptrunner/wptrunner/executors/executorchrome.py @@ -15,6 +15,7 @@ from .executorwebdriver import ( WebDriverBaseProtocolPart, WebDriverCrashtestExecutor, + WebDriverAccessibilityProtocolPart, WebDriverFedCMProtocolPart, WebDriverPrintRefTestExecutor, WebDriverProtocol, @@ -28,6 +29,8 @@ here = os.path.dirname(__file__) +AXNode = Mapping[str, Any] + def _update_capabilities_if_extension_test( browser: Any, capabilities: Optional[MutableMapping[str, Any]] ) -> Optional[MutableMapping[str, Any]]: @@ -191,6 +194,105 @@ def confirm_idp_login(self): f"{self.parent.vendor_prefix}/fedcm/confirmidplogin") +class ChromeDriverAccessibilityProtocolPart(WebDriverAccessibilityProtocolPart): + def setup(self): + super().setup() + self._nodes_by_id = {} + + def teardown(self): + try: + self.parent.cdp.execute_cdp_command("Accessibility.disable") + except error.WebDriverException: + pass + + def get_accessibility_properties_for_element(self, element): + node = self._get_ax_node_for_element(element) + return self._serialize_node(node) if node else {} + + def get_accessibility_properties_for_accessibility_node(self, id): + node = self._find_ax_node_by_ax_node_id(id) + return self._serialize_node(node) if node else {} + + def _get_full_ax_tree(self) -> Mapping[str, AXNode]: + self.parent.cdp.execute_cdp_command("Accessibility.enable") + node_array = self.parent.cdp.execute_cdp_command( + "Accessibility.getFullAXTree", + {} + ).get("nodes", []) + + return {node["nodeId"]: node for node in node_array} + + def _find_ax_node_by_ax_node_id(self, ax_node_id: str) -> Optional[AXNode]: + full_ax_tree = self._get_full_ax_tree() + node = full_ax_tree.get(ax_node_id, None) + + return node + + def _get_ax_node_for_element(self, element: Any) -> Optional[AXNode]: + # Parse the ID, then hand it off to the shared helper + parsed_ids = self._extract_chromedriver_ids(element.id) + + if parsed_ids and parsed_ids.get("element"): + return self._get_ax_node_by_backend_node_id(parsed_ids["element"]) + + return None + + def _get_ax_node_by_backend_node_id(self, backend_node_id: str) -> Optional[AXNode]: + """Shared CDP call to fetch an accessibility node by its backend ID.""" + ax_tree = self.parent.cdp.execute_cdp_command( + "Accessibility.getPartialAXTree", + { + "backendNodeId": int(backend_node_id), + "fetchRelatives": False, + } + ) + nodes: list[AXNode] = ax_tree.get("nodes", []) + return nodes[0] if nodes else None + + def _serialize_node(self, node: AXNode) -> Mapping[str, Any]: + # TODO: Define an approach to handle ignored items as this is different + # browsers by browser and might make testing the subtree harder. + + rv: dict[str,Any] = { + "accessibilityId": node["nodeId"], + "children": node.get("childIds", []), + } + + if "parentId" in node: + rv["parent"] = node["parentId"] + + # Parse native fields + if "role" in node: + rv["role"] = node["role"].get("value") + if "name" in node: + rv["label"] = node["name"].get("value") + if "value" in node: + rv["value"] = node["value"].get("value") + if "description" in node: + rv["description"] = node["description"].get("value") + + # We only support a subset of properties for now outside of the native fields. + allowed_properties = {'checked', 'pressed', 'level', + 'multiline', 'orientation', 'required', + 'roledescription', 'selected'} + + for prop in node.get("properties", []): + if prop["name"] in allowed_properties: + rv[prop["name"]] = prop["value"].get("value") + + return rv + + @staticmethod + def _extract_chromedriver_ids(element_id_string: str) -> Optional[Mapping[str, str]]: + """ + Extracts Frame, Document, and Element IDs from a ChromeDriver id. + Expected format: f.[hash].d.[hash].e.[id] + """ + pattern = r"^f\.(?P[^.]+)\.d\.(?P[^.]+)\.e\.(?P.+)$" + match = re.match(pattern, element_id_string) + + return match.groupdict() if match else None + class ChromeDriverDevToolsProtocolPart(ProtocolPart): """A low-level API for sending Chrome DevTools Protocol [0] commands directly to the browser. @@ -244,6 +346,7 @@ def get_trace(self): class ChromeDriverProtocol(WebDriverProtocol): implements = [ + ChromeDriverAccessibilityProtocolPart, ChromeDriverBaseProtocolPart, ChromeDriverDevToolsProtocolPart, ChromeDriverFedCMProtocolPart, @@ -269,6 +372,7 @@ def __init__(self, executor, browser, capabilities, **kwargs): class ChromeDriverBidiProtocol(WebDriverBidiProtocol): implements = [ + ChromeDriverAccessibilityProtocolPart, ChromeDriverBaseProtocolPart, ChromeDriverDevToolsProtocolPart, ChromeDriverFedCMProtocolPart, diff --git a/wai-aria/scripts/aria-utils.js b/wai-aria/scripts/aria-utils.js index 29942f27cfe25a..37d88c31427343 100644 --- a/wai-aria/scripts/aria-utils.js +++ b/wai-aria/scripts/aria-utils.js @@ -225,7 +225,7 @@ const AriaUtils = { promise_test(async t => { const actual = await test_driver.get_accessibility_properties_for_element(el); for (const key in expected) { - assert_equals(actual[key], expected[key], `${key}: ${el.outerHTML}`); + assert_equals(String(actual[key]), expected[key], `${key}: ${el.outerHTML}`); } }, testName); }