From 5fbd9d7635e52298962e1b938515d5a6d18d5346 Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Tue, 30 Apr 2024 17:18:04 -0700 Subject: [PATCH] Extend testdriver to add accessibility API testing --- core-aam/acacia/test-testdriver.html | 18 ++++ resources/testdriver.js | 16 ++++ tools/webdriver/webdriver/client.py | 4 + tools/wpt/run.py | 3 +- tools/wptrunner/wptrunner/browsers/base.py | 3 +- .../wptrunner/wptrunner/executors/actions.py | 15 ++- .../wptrunner/executors/executoracacia.py | 91 +++++++++++++++++++ .../wptrunner/executors/executormarionette.py | 6 +- .../wptrunner/executors/executorwebdriver.py | 6 +- .../wptrunner/wptrunner/executors/protocol.py | 14 +++ tools/wptrunner/wptrunner/testdriver-extra.js | 4 + tools/wptrunner/wptrunner/testrunner.py | 11 ++- tools/wptrunner/wptrunner/wptcommandline.py | 2 + tools/wptrunner/wptrunner/wptrunner.py | 1 + 14 files changed, 186 insertions(+), 8 deletions(-) create mode 100644 core-aam/acacia/test-testdriver.html create mode 100644 tools/wptrunner/wptrunner/executors/executoracacia.py diff --git a/core-aam/acacia/test-testdriver.html b/core-aam/acacia/test-testdriver.html new file mode 100644 index 00000000000000..33c0dec11677af --- /dev/null +++ b/core-aam/acacia/test-testdriver.html @@ -0,0 +1,18 @@ + + +core-aam: acacia test using testdriver + + + + + + + +
+ + diff --git a/resources/testdriver.js b/resources/testdriver.js index 2d1a89690cc25f..9bc0db5e208c96 100644 --- a/resources/testdriver.js +++ b/resources/testdriver.js @@ -1066,6 +1066,18 @@ */ clear_device_posture: function(context=null) { return window.test_driver_internal.clear_device_posture(context); + }, + + /** + * Get a serialized object representing the accessibility API's accessibility node. + * + * @param {id} id of element + * @returns {Promise} Fullfilled with object representing accessibilty node, + * rejected in the cases of failures. + */ + get_accessibility_api_node: async function(dom_id) { + let jsonresult = await window.test_driver_internal.get_accessibility_api_node(dom_id); + return JSON.parse(jsonresult); } }; @@ -1254,6 +1266,10 @@ async clear_device_posture(context=null) { throw new Error("clear_device_posture() is not implemented by testdriver-vendor.js"); + }, + + async get_accessibility_api_node(dom_id) { + throw new Error("not implemented, whoops!"); } }; })(); diff --git a/tools/webdriver/webdriver/client.py b/tools/webdriver/webdriver/client.py index e41df7f57640a6..1c9fd88965169a 100644 --- a/tools/webdriver/webdriver/client.py +++ b/tools/webdriver/webdriver/client.py @@ -879,6 +879,10 @@ def get_computed_label(self): def get_computed_role(self): return self.send_element_command("GET", "computedrole") + @command + def get_accessibility_api_node(self): + return self.send_element_command("GET", "accessibilityapinode") + # This MUST come last because otherwise @property decorators above # will be overridden by this. @command diff --git a/tools/wpt/run.py b/tools/wpt/run.py index 2fb7f97f498c4a..6a7bf64fabd794 100644 --- a/tools/wpt/run.py +++ b/tools/wpt/run.py @@ -519,7 +519,8 @@ def setup_kwargs(self, kwargs): # We are on Taskcluster, where our Docker container does not have # enough capabilities to run Chrome with sandboxing. (gh-20133) kwargs["binary_args"].append("--no-sandbox") - + if kwargs["force_renderer_accessibility"]: + kwargs["binary_args"].append("--force-renderer-accessibility") class ContentShell(BrowserSetup): name = "content_shell" diff --git a/tools/wptrunner/wptrunner/browsers/base.py b/tools/wptrunner/wptrunner/browsers/base.py index 6b1465cde8aca9..f9e2661285d33d 100644 --- a/tools/wptrunner/wptrunner/browsers/base.py +++ b/tools/wptrunner/wptrunner/browsers/base.py @@ -430,7 +430,8 @@ def executor_browser(self) -> Tuple[Type[ExecutorBrowser], Mapping[str, Any]]: "host": self.host, "port": self.port, "pac": self.pac, - "env": self.env} + "env": self.env, + "pid": self.pid} def settings(self, test: Test) -> BrowserSettings: self._pac = test.environment.get("pac", None) if self._supports_pac else None diff --git a/tools/wptrunner/wptrunner/executors/actions.py b/tools/wptrunner/wptrunner/executors/actions.py index 6e0c081b48f752..90ae943ac25829 100644 --- a/tools/wptrunner/wptrunner/executors/actions.py +++ b/tools/wptrunner/wptrunner/executors/actions.py @@ -464,6 +464,18 @@ def __init__(self, logger, protocol): def __call__(self, payload): return self.protocol.device_posture.clear_device_posture() +class GetAccessibilityAPINodeAction: + name = "get_accessibility_api_node" + + def __init__(self, logger, protocol): + self.logger = logger + self.protocol = protocol + + def __call__(self, payload): + dom_id = payload["dom_id"] + return self.protocol.platform_accessibility.get_accessibility_api_node(dom_id) + + actions = [ClickAction, DeleteAllCookiesAction, GetAllCookiesAction, @@ -499,4 +511,5 @@ def __call__(self, payload): RemoveVirtualSensorAction, GetVirtualSensorInformationAction, SetDevicePostureAction, - ClearDevicePostureAction] + ClearDevicePostureAction, + GetAccessibilityAPINodeAction] diff --git a/tools/wptrunner/wptrunner/executors/executoracacia.py b/tools/wptrunner/wptrunner/executors/executoracacia.py new file mode 100644 index 00000000000000..b67180b956ede5 --- /dev/null +++ b/tools/wptrunner/wptrunner/executors/executoracacia.py @@ -0,0 +1,91 @@ +import acacia_atspi +import json +from .protocol import (PlatformAccessibilityProtocolPart) + +# When running against chrome family browser: +# self.parent is WebDriverProtocol +# self.parent.webdriver is webdriver + +def findActiveTab(root): + stack = [root] + while stack: + node = stack.pop() + + if node.getRoleName() == 'frame': + relations = node.getRelations() + if 'ATSPI_RELATION_EMBEDS' in relations: + index = relations.index('ATSPI_RELATION_EMBEDS') + target = node.getTargetForRelationAtIndex(index) + print(target.getRoleName()) + print(target.getName()) + return target + continue + + for i in range(node.getChildCount()): + child = node.getChildAtIndex(i) + stack.append(child) + + return None + +def serialize_node(node): + node_dictionary = {} + node_dictionary['role'] = node.getRoleName() + node_dictionary['name'] = node.getName() + node_dictionary['description'] = node.getDescription() + node_dictionary['states'] = sorted(node.getStates()) + node_dictionary['interfaces'] = sorted(node.getInterfaces()) + node_dictionary['attributes'] = sorted(node.getAttributes()) + + # TODO: serialize other attributes + + return node_dictionary + +def find_node(root, dom_id): + stack = [root] + while stack: + node = stack.pop() + + attributes = node.getAttributes() + for attribute_pair in attributes: + [attribute, value] = attribute_pair.split(':', 1) + if attribute == 'id': + if value == dom_id: + return node + + for i in range(node.getChildCount()): + child = node.getChildAtIndex(i) + stack.append(child) + + return None + +class AcaciaPlatformAccessibilityProtocolPart(PlatformAccessibilityProtocolPart): + def setup(self): + self.product_name = self.parent.product_name + self.root = None + self.errormsg = None + + self.root = acacia_atspi.findRootAtspiNodeForName(self.product_name); + if self.root.isNull(): + error = f"Cannot find root accessibility node for {self.product_name} - did you turn on accessibility?" + print(error) + self.errormsg = error + + + def get_accessibility_api_node(self, dom_id): + if self.root.isNull(): + return json.dumps({"role": self.errormsg}) + + active_tab = findActiveTab(self.root) + + # This will fail sometimes when accessibilty is off. + if not active_tab or active_tab.isNull(): + return json.dumps({"role": "couldn't find active tab"}) + + # This fails sometimes for unknown reasons. + node = find_node(active_tab, dom_id) + if not node or node.isNull(): + return json.dumps({"role": "couldn't find the node with that ID"}) + + return json.dumps(serialize_node(node)) + + diff --git a/tools/wptrunner/wptrunner/executors/executormarionette.py b/tools/wptrunner/wptrunner/executors/executormarionette.py index 05a9fc1ae4b874..e3f29959858a90 100644 --- a/tools/wptrunner/wptrunner/executors/executormarionette.py +++ b/tools/wptrunner/wptrunner/executors/executormarionette.py @@ -24,6 +24,7 @@ WdspecExecutor, get_pages, strip_server) + from .protocol import (AccessibilityProtocolPart, ActionSequenceProtocolPart, AssertsProtocolPart, @@ -48,6 +49,7 @@ DevicePostureProtocolPart, merge_dicts) +from .executoracacia import (AcaciaPlatformAccessibilityProtocolPart) def do_delayed_imports(): global errors, marionette, Addons, WebAuthn @@ -782,12 +784,14 @@ class MarionetteProtocol(Protocol): MarionetteDebugProtocolPart, MarionetteAccessibilityProtocolPart, MarionetteVirtualSensorProtocolPart, - MarionetteDevicePostureProtocolPart] + MarionetteDevicePostureProtocolPart, + AcaciaPlatformAccessibilityProtocolPart] def __init__(self, executor, browser, capabilities=None, timeout_multiplier=1, e10s=True, ccov=False): do_delayed_imports() super().__init__(executor, browser) + self.product_name = browser.product_name self.marionette = None self.marionette_port = browser.marionette_port self.capabilities = capabilities diff --git a/tools/wptrunner/wptrunner/executors/executorwebdriver.py b/tools/wptrunner/wptrunner/executors/executorwebdriver.py index 69013e5e796979..7982ded4b42ca4 100644 --- a/tools/wptrunner/wptrunner/executors/executorwebdriver.py +++ b/tools/wptrunner/wptrunner/executors/executorwebdriver.py @@ -38,6 +38,8 @@ DevicePostureProtocolPart, merge_dicts) +from .executoracacia import (AcaciaPlatformAccessibilityProtocolPart) + from webdriver.client import Session from webdriver import error @@ -462,10 +464,12 @@ class WebDriverProtocol(Protocol): WebDriverFedCMProtocolPart, WebDriverDebugProtocolPart, WebDriverVirtualSensorPart, - WebDriverDevicePostureProtocolPart] + WebDriverDevicePostureProtocolPart, + AcaciaPlatformAccessibilityProtocolPart] def __init__(self, executor, browser, capabilities, **kwargs): super().__init__(executor, browser) + self.product_name = browser.product_name self.capabilities = capabilities if hasattr(browser, "capabilities"): if self.capabilities is None: diff --git a/tools/wptrunner/wptrunner/executors/protocol.py b/tools/wptrunner/wptrunner/executors/protocol.py index 3d588738b6e005..3d8aa2fc37b417 100644 --- a/tools/wptrunner/wptrunner/executors/protocol.py +++ b/tools/wptrunner/wptrunner/executors/protocol.py @@ -321,6 +321,20 @@ def get_computed_role(self, element): pass +class PlatformAccessibilityProtocolPart(ProtocolPart): + """Protocol part for platform accessibility introspection""" + __metaclass__ = ABCMeta + + name = "platform_accessibility" + + @abstractmethod + def get_accessibility_api_node(self, dom_id): + """Return the the platform accessibilty object. + + :param id: DOM ID.""" + pass + + class CookiesProtocolPart(ProtocolPart): """Protocol part for managing cookies""" __metaclass__ = ABCMeta diff --git a/tools/wptrunner/wptrunner/testdriver-extra.js b/tools/wptrunner/wptrunner/testdriver-extra.js index 87d3826bfceb6a..e80c3ecb3e31e8 100644 --- a/tools/wptrunner/wptrunner/testdriver-extra.js +++ b/tools/wptrunner/wptrunner/testdriver-extra.js @@ -335,4 +335,8 @@ window.test_driver_internal.clear_device_posture = function(context=null) { return create_action("clear_device_posture", {context}); }; + + window.test_driver_internal.get_accessibility_api_node = function(dom_id) { + return create_action("get_accessibility_api_node", {dom_id}); + }; })(); diff --git a/tools/wptrunner/wptrunner/testrunner.py b/tools/wptrunner/wptrunner/testrunner.py index 93e19fa47ba036..af99042bf1b6f5 100644 --- a/tools/wptrunner/wptrunner/testrunner.py +++ b/tools/wptrunner/wptrunner/testrunner.py @@ -313,7 +313,7 @@ def __init__(self, suite_name, index, test_queue, test_implementations, stop_flag, retry_index=0, rerun=1, pause_after_test=False, pause_on_unexpected=False, restart_on_unexpected=True, debug_info=None, - capture_stdio=True, restart_on_new_group=True, recording=None, max_restarts=5): + capture_stdio=True, restart_on_new_group=True, recording=None, max_restarts=5, product_name=None): """Thread that owns a single TestRunner process and any processes required by the TestRunner (e.g. the Firefox binary). @@ -332,6 +332,7 @@ def __init__(self, suite_name, index, test_queue, self.suite_name = suite_name self.manager_number = index self.test_implementation_key = None + self.product_name = product_name self.test_implementations = {} for key, test_implementation in test_implementations.items(): @@ -594,6 +595,7 @@ def start_test_runner(self): self.executor_kwargs["group_metadata"] = self.state.group_metadata self.executor_kwargs["browser_settings"] = self.browser.browser_settings executor_browser_cls, executor_browser_kwargs = self.browser.browser.executor_browser() + executor_browser_kwargs["product_name"] = self.product_name args = (self.remote_queue, self.command_queue, @@ -984,8 +986,10 @@ def __init__(self, suite_name, test_queue_builder, test_implementations, capture_stdio=True, restart_on_new_group=True, recording=None, - max_restarts=5): + max_restarts=5, + product_name=None): self.suite_name = suite_name + self.product_name = product_name self.test_queue_builder = test_queue_builder self.test_implementations = test_implementations self.pause_after_test = pause_after_test @@ -1031,7 +1035,8 @@ def run(self, tests): self.capture_stdio, self.restart_on_new_group, recording=self.recording, - max_restarts=self.max_restarts) + max_restarts=self.max_restarts, + product_name=self.product_name) manager.start() self.pool.add(manager) self.wait() diff --git a/tools/wptrunner/wptrunner/wptcommandline.py b/tools/wptrunner/wptrunner/wptcommandline.py index 87f51d6be7f49c..db9dca8b45e671 100644 --- a/tools/wptrunner/wptrunner/wptcommandline.py +++ b/tools/wptrunner/wptrunner/wptcommandline.py @@ -377,6 +377,8 @@ def create_parser(product_choices=None): chrome_group.add_argument("--no-enable-experimental", action="store_false", dest="enable_experimental", help="Do not enable --enable-experimental-web-platform-features flag " "on experimental channels") + chrome_group.add_argument( "--force-renderer-accessibility", action="store_true", + dest="force_renderer_accessibility",help="Turn on accessibility.") chrome_group.add_argument( "--enable-sanitizer", action="store_true", diff --git a/tools/wptrunner/wptrunner/wptrunner.py b/tools/wptrunner/wptrunner/wptrunner.py index d9d85de6a4d04b..9a45b72df328fb 100644 --- a/tools/wptrunner/wptrunner/wptrunner.py +++ b/tools/wptrunner/wptrunner/wptrunner.py @@ -307,6 +307,7 @@ def run_test_iteration(test_status, test_loader, test_queue_builder, kwargs["restart_on_new_group"], recording=recording, max_restarts=kwargs["max_restarts"], + product_name=product.name ) as manager_group: try: handle_interrupt_signals()