From 5fbd9d7635e52298962e1b938515d5a6d18d5346 Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Tue, 30 Apr 2024 17:18:04 -0700 Subject: [PATCH 01/28] 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() From 21d2d493b4defc5cac28f15e8d7bc2e928309ec7 Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Wed, 15 May 2024 10:34:53 -0700 Subject: [PATCH 02/28] Switch from acacia to gi.repository Atspi --- .../wptrunner/executors/executoracacia.py | 99 ++++++++++--------- 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/tools/wptrunner/wptrunner/executors/executoracacia.py b/tools/wptrunner/wptrunner/executors/executoracacia.py index b67180b956ede5..816dd47a594875 100644 --- a/tools/wptrunner/wptrunner/executors/executoracacia.py +++ b/tools/wptrunner/wptrunner/executors/executoracacia.py @@ -1,42 +1,36 @@ -import acacia_atspi +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import 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): +def find_active_tab(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) + if Atspi.Accessible.get_role_name(node) == 'frame': + ## Helper: list of string relations, get targets for relation? + relationset = Atspi.Accessible.get_relation_set(node) + for relation in relationset: + if relation.get_relation_type() == Atspi.RelationType.EMBEDS: + return relation.get_target(0) + contiue + + 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 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()) + node_dictionary['role'] = Atspi.Accessible.get_role_name(node) + node_dictionary['name'] = Atspi.Accessible.get_name(node) + node_dictionary['description'] = Atspi.Accessible.get_description(node) # TODO: serialize other attributes + # states, interfaces, attributes, etc. return node_dictionary @@ -45,45 +39,58 @@ def find_node(root, dom_id): 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 + attributes = Atspi.Accessible.get_attributes(node) + if 'id' in attributes and attributes['id'] == dom_id: + return node - for i in range(node.getChildCount()): - child = node.getChildAtIndex(i) + 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 find_browser(name): + 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 name in Atspi.Accessible.get_name(app).lower(): + return app + return + + + class AcaciaPlatformAccessibilityProtocolPart(PlatformAccessibilityProtocolPart): + def handle_event(self, e): + print(f"---------------- EVENT ----------------") + print(f"{e.type}") + def setup(self): self.product_name = self.parent.product_name self.root = None - self.errormsg = None + self.found_browser = False - 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 + print(f"---------------- LISTENING ----------------") + self._event_listener = Atspi.EventListener.new(self.handle_event) + self._event_listener.register("document:load-complete") + self.root = find_browser(self.product_name); + if self.root: + self.found_browser = True + else: + print(f"Cannot find root accessibility node for {self.product_name} - did you turn on accessibility?") def get_accessibility_api_node(self, dom_id): - if self.root.isNull(): - return json.dumps({"role": self.errormsg}) + if not self.found_browser: + return json.dumps({"role": "couldn't find browser"}) - active_tab = findActiveTab(self.root) - - # This will fail sometimes when accessibilty is off. - if not active_tab or active_tab.isNull(): + active_tab = find_active_tab(self.root) + if not active_tab: 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(): + if not node: return json.dumps({"role": "couldn't find the node with that ID"}) return json.dumps(serialize_node(node)) From e95bc28eb534da286c5b24779c9a9ab1ce99a943 Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Thu, 16 May 2024 14:29:53 -0700 Subject: [PATCH 03/28] Wait for document:load-complete --- core-aam/acacia/test-testdriver.html | 9 ++++- .../wptrunner/executors/executoracacia.py | 39 ++++++++++++++----- tools/wptrunner/wptrunner/testrunner.py | 2 +- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/core-aam/acacia/test-testdriver.html b/core-aam/acacia/test-testdriver.html index 33c0dec11677af..cffa0f8b48dbbf 100644 --- a/core-aam/acacia/test-testdriver.html +++ b/core-aam/acacia/test-testdriver.html @@ -9,10 +9,17 @@
+ diff --git a/tools/wptrunner/wptrunner/executors/executoracacia.py b/tools/wptrunner/wptrunner/executors/executoracacia.py index 816dd47a594875..19f696dd8b1655 100644 --- a/tools/wptrunner/wptrunner/executors/executoracacia.py +++ b/tools/wptrunner/wptrunner/executors/executoracacia.py @@ -2,8 +2,14 @@ gi.require_version("Atspi", "2.0") from gi.repository import Atspi import json +import threading from .protocol import (PlatformAccessibilityProtocolPart) + +import traceback +import time + + def find_active_tab(root): stack = [root] while stack: @@ -54,33 +60,47 @@ def find_browser(name): child_count = Atspi.Accessible.get_child_count(desktop) for i in range(child_count): app = Atspi.Accessible.get_child_at_index(desktop, i) - if name in Atspi.Accessible.get_name(app).lower(): - return app + full_app_name = Atspi.Accessible.get_name(app) + if name in full_app_name.lower(): + return (app, full_app_name) return - class AcaciaPlatformAccessibilityProtocolPart(PlatformAccessibilityProtocolPart): + def start_atspi_listener(self): + self._event_listener = Atspi.EventListener.new(self.handle_event) + self._event_listener.register("document:load-complete") + Atspi.event_main() + + def handle_event(self, e): - print(f"---------------- EVENT ----------------") - print(f"{e.type}") + app = Atspi.Accessible.get_application(e.source) + app_name = Atspi.Accessible.get_name(app) + if (self.full_app_name == app_name and e.any_data): + self.load_complete = True + self._event_listener.deregister("document:load-complete") + Atspi.event_quit() def setup(self): self.product_name = self.parent.product_name + self.full_app_name = '' self.root = None self.found_browser = False + self.load_complete = False - print(f"---------------- LISTENING ----------------") - self._event_listener = Atspi.EventListener.new(self.handle_event) - self._event_listener.register("document:load-complete") + self.atspi_listener_thread = threading.Thread(target=self.start_atspi_listener) - self.root = find_browser(self.product_name); + (self.root, self.full_app_name) = find_browser(self.product_name); if self.root: self.found_browser = True + self.atspi_listener_thread.start() else: print(f"Cannot find root accessibility node for {self.product_name} - did you turn on accessibility?") def get_accessibility_api_node(self, dom_id): + if not self.load_complete: + self.atspi_listener_thread.join() + if not self.found_browser: return json.dumps({"role": "couldn't find browser"}) @@ -88,7 +108,6 @@ def get_accessibility_api_node(self, dom_id): if not active_tab: return json.dumps({"role": "couldn't find active tab"}) - node = find_node(active_tab, dom_id) if not node: return json.dumps({"role": "couldn't find the node with that ID"}) diff --git a/tools/wptrunner/wptrunner/testrunner.py b/tools/wptrunner/wptrunner/testrunner.py index af99042bf1b6f5..950378ce77f225 100644 --- a/tools/wptrunner/wptrunner/testrunner.py +++ b/tools/wptrunner/wptrunner/testrunner.py @@ -494,7 +494,7 @@ def wait_event(self): } try: command, data = self.command_queue.get(True, 1) - self.logger.debug("Got command: %r" % command) + #self.logger.debug("Got command: %r" % command) except OSError: self.logger.error("Got IOError from poll") return RunnerManagerState.restarting(self.state.subsuite, From d9dc8138f4b9843647ea26282b2cedc1e0db5a98 Mon Sep 17 00:00:00 2001 From: Alice Boxhall <95208+alice@users.noreply.github.com> Date: Fri, 17 May 2024 17:36:30 +1000 Subject: [PATCH 04/28] Very minimal first pass --- .../wptrunner/executors/executoracacia.py | 124 +++--------------- 1 file changed, 17 insertions(+), 107 deletions(-) diff --git a/tools/wptrunner/wptrunner/executors/executoracacia.py b/tools/wptrunner/wptrunner/executors/executoracacia.py index 19f696dd8b1655..988230b05adca8 100644 --- a/tools/wptrunner/wptrunner/executors/executoracacia.py +++ b/tools/wptrunner/wptrunner/executors/executoracacia.py @@ -1,117 +1,27 @@ -import gi -gi.require_version("Atspi", "2.0") -from gi.repository import Atspi -import json -import threading from .protocol import (PlatformAccessibilityProtocolPart) - -import traceback -import time - - -def find_active_tab(root): - stack = [root] - while stack: - node = stack.pop() - - if Atspi.Accessible.get_role_name(node) == 'frame': - ## Helper: list of string relations, get targets for relation? - relationset = Atspi.Accessible.get_relation_set(node) - for relation in relationset: - if relation.get_relation_type() == Atspi.RelationType.EMBEDS: - return relation.get_target(0) - contiue - - 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 serialize_node(node): - node_dictionary = {} - node_dictionary['role'] = Atspi.Accessible.get_role_name(node) - node_dictionary['name'] = Atspi.Accessible.get_name(node) - node_dictionary['description'] = Atspi.Accessible.get_description(node) - - # TODO: serialize other attributes - # states, interfaces, attributes, etc. - - return node_dictionary - -def find_node(root, dom_id): - 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 - -def find_browser(name): - 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 name in full_app_name.lower(): - return (app, full_app_name) - return +from sys import platform +linux = False +mac = False +if platform == "linux": + linux = True + from .executoratspi import * +if platform == "darwin": + mac = True + from .executoraxapi import * class AcaciaPlatformAccessibilityProtocolPart(PlatformAccessibilityProtocolPart): - def start_atspi_listener(self): - self._event_listener = Atspi.EventListener.new(self.handle_event) - self._event_listener.register("document:load-complete") - Atspi.event_main() - - - def handle_event(self, e): - app = Atspi.Accessible.get_application(e.source) - app_name = Atspi.Accessible.get_name(app) - if (self.full_app_name == app_name and e.any_data): - self.load_complete = True - self._event_listener.deregister("document:load-complete") - Atspi.event_quit() - def setup(self): self.product_name = self.parent.product_name - self.full_app_name = '' - self.root = None - self.found_browser = False - self.load_complete = False - - self.atspi_listener_thread = threading.Thread(target=self.start_atspi_listener) + self.impl = None + if linux: + self.impl = AtspiExecutorImpl() + self.impl.setup(self.product_name) + if mac: + self.impl = AXAPIExecutorImpl() + self.impl.setup(self.product_name) - (self.root, self.full_app_name) = find_browser(self.product_name); - if self.root: - self.found_browser = True - self.atspi_listener_thread.start() - else: - print(f"Cannot find root accessibility node for {self.product_name} - did you turn on accessibility?") def get_accessibility_api_node(self, dom_id): - if not self.load_complete: - self.atspi_listener_thread.join() - - if not self.found_browser: - return json.dumps({"role": "couldn't find browser"}) - - active_tab = find_active_tab(self.root) - if not active_tab: - return json.dumps({"role": "couldn't find active tab"}) - - node = find_node(active_tab, dom_id) - if not node: - return json.dumps({"role": "couldn't find the node with that ID"}) - - return json.dumps(serialize_node(node)) - - + return self.impl.get_accessibility_api_node(dom_id) \ No newline at end of file From c794ae7ef6b406aa07588e25ec11de9bd67ad32c Mon Sep 17 00:00:00 2001 From: Alice Boxhall <95208+alice@users.noreply.github.com> Date: Mon, 20 May 2024 09:56:52 +1000 Subject: [PATCH 05/28] Add executoratspi and executoraxapi --- .../wptrunner/executors/executoratspi.py | 117 ++++++++++++++++++ .../wptrunner/executors/executoraxapi.py | 36 ++++++ 2 files changed, 153 insertions(+) create mode 100644 tools/wptrunner/wptrunner/executors/executoratspi.py create mode 100644 tools/wptrunner/wptrunner/executors/executoraxapi.py diff --git a/tools/wptrunner/wptrunner/executors/executoratspi.py b/tools/wptrunner/wptrunner/executors/executoratspi.py new file mode 100644 index 00000000000000..aa4f3d4e56acfe --- /dev/null +++ b/tools/wptrunner/wptrunner/executors/executoratspi.py @@ -0,0 +1,117 @@ +import gi +gi.require_version("Atspi", "2.0") +from gi.repository import Atspi +import json +import threading +from .protocol import (PlatformAccessibilityProtocolPart) + + +import traceback +import time + + +def find_active_tab(root): + stack = [root] + while stack: + node = stack.pop() + + if Atspi.Accessible.get_role_name(node) == 'frame': + ## Helper: list of string relations, get targets for relation? + relationset = Atspi.Accessible.get_relation_set(node) + for relation in relationset: + if relation.get_relation_type() == Atspi.RelationType.EMBEDS: + return relation.get_target(0) + contiue + + 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 serialize_node(node): + node_dictionary = {} + node_dictionary['role'] = Atspi.Accessible.get_role_name(node) + node_dictionary['name'] = Atspi.Accessible.get_name(node) + node_dictionary['description'] = Atspi.Accessible.get_description(node) + + # TODO: serialize other attributes + # states, interfaces, attributes, etc. + + return node_dictionary + +def find_node(root, dom_id): + 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 + +def find_browser(name): + 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 name in full_app_name.lower(): + return (app, full_app_name) + return + + +class AtspiSupport(): + def start_atspi_listener(self): + self._event_listener = Atspi.EventListener.new(self.handle_event) + self._event_listener.register("document:load-complete") + Atspi.event_main() + + + def handle_event(self, e): + app = Atspi.Accessible.get_application(e.source) + app_name = Atspi.Accessible.get_name(app) + if (self.full_app_name == app_name and e.any_data): + self.load_complete = True + self._event_listener.deregister("document:load-complete") + Atspi.event_quit() + + def setup(self, product_name): + self.product_name = product_name + self.full_app_name = '' + self.root = None + self.found_browser = False + self.load_complete = False + + self.atspi_listener_thread = threading.Thread(target=self.start_atspi_listener) + + (self.root, self.full_app_name) = find_browser(self.product_name); + if self.root: + self.found_browser = True + self.atspi_listener_thread.start() + else: + print(f"Cannot find root accessibility node for {self.product_name} - did you turn on accessibility?") + + def get_accessibility_api_node(self, dom_id): + if not self.load_complete: + self.atspi_listener_thread.join() + + if not self.found_browser: + return json.dumps({"role": "couldn't find browser"}) + + active_tab = find_active_tab(self.root) + if not active_tab: + return json.dumps({"role": "couldn't find active tab"}) + + node = find_node(active_tab, dom_id) + if not node: + 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/executoraxapi.py b/tools/wptrunner/wptrunner/executors/executoraxapi.py new file mode 100644 index 00000000000000..5663f116698a4f --- /dev/null +++ b/tools/wptrunner/wptrunner/executors/executoraxapi.py @@ -0,0 +1,36 @@ +from ApplicationServices import ( + AXUIElementRef, + AXUIElementCopyAttributeNames, + AXUIElementCopyAttributeValue, + AXUIElementCopyParameterizedAttributeValue, + AXUIElementCopyParameterizedAttributeNames, + AXUIElementIsAttributeSettable, + AXUIElementCopyActionNames, + AXUIElementSetAttributeValue, + AXUIElementCreateApplication, + AXUIElementCopyMultipleAttributeValues, + AXUIElementCopyActionDescription, + AXValueRef, + AXValueGetType, + kAXValueAXErrorType, +) + +class AXAPIExecutorImpl: + def setup(self, product_name): + self.product_name = product_name + + def get_application_by_name(self, name): + # TODO: copied directly from https://github.com/eeejay/pyax/blob/main/src/pyax/_uielement.py + wl = CGWindowListCopyWindowInfo( + kCGWindowListExcludeDesktopElements, kCGNullWindowID + ) + for w in wl: + n = w.valueForKey_("kCGWindowOwnerName") + if name == w.valueForKey_("kCGWindowOwnerName"): + return AXUIElementCreateApplication( + int((w.valueForKey_("kCGWindowOwnerPID"))) + ) + + def get_accessibility_api_node(self, dom_id): + app = self.get_application_by_name(self.product_name) + From 1e9eee44ba820cec8f187a65c682fdcf76ad4f6c Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Tue, 21 May 2024 09:25:59 -0700 Subject: [PATCH 06/28] Update class name --- tools/wptrunner/wptrunner/executors/executoratspi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/wptrunner/wptrunner/executors/executoratspi.py b/tools/wptrunner/wptrunner/executors/executoratspi.py index aa4f3d4e56acfe..71ece852247662 100644 --- a/tools/wptrunner/wptrunner/executors/executoratspi.py +++ b/tools/wptrunner/wptrunner/executors/executoratspi.py @@ -66,7 +66,7 @@ def find_browser(name): return -class AtspiSupport(): +class AtspiExecutorImpl(): def start_atspi_listener(self): self._event_listener = Atspi.EventListener.new(self.handle_event) self._event_listener.register("document:load-complete") From 0dd751892867cf2b9cf81eaf0ec4fb8645f0a921 Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Tue, 21 May 2024 09:29:40 -0700 Subject: [PATCH 07/28] Fixes for when browser is not found --- tools/wptrunner/wptrunner/executors/executoratspi.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/wptrunner/wptrunner/executors/executoratspi.py b/tools/wptrunner/wptrunner/executors/executoratspi.py index 71ece852247662..d0a2cdea210573 100644 --- a/tools/wptrunner/wptrunner/executors/executoratspi.py +++ b/tools/wptrunner/wptrunner/executors/executoratspi.py @@ -63,7 +63,7 @@ def find_browser(name): full_app_name = Atspi.Accessible.get_name(app) if name in full_app_name.lower(): return (app, full_app_name) - return + return (None, None) class AtspiExecutorImpl(): @@ -98,12 +98,12 @@ def setup(self, product_name): print(f"Cannot find root accessibility node for {self.product_name} - did you turn on accessibility?") def get_accessibility_api_node(self, dom_id): - if not self.load_complete: - self.atspi_listener_thread.join() - if not self.found_browser: return json.dumps({"role": "couldn't find browser"}) + if not self.load_complete: + self.atspi_listener_thread.join() + active_tab = find_active_tab(self.root) if not active_tab: return json.dumps({"role": "couldn't find active tab"}) From 8a14630839771cf613420070a8feb5bc78135359 Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Wed, 29 May 2024 12:15:34 -0700 Subject: [PATCH 08/28] Clean up example --- ...-testdriver.html => all_apis_example.html} | 23 +++++++++++-------- resources/testdriver.js | 8 ++++--- .../wptrunner/executors/executoratspi.py | 16 ++++--------- 3 files changed, 23 insertions(+), 24 deletions(-) rename core-aam/acacia/{test-testdriver.html => all_apis_example.html} (50%) diff --git a/core-aam/acacia/test-testdriver.html b/core-aam/acacia/all_apis_example.html similarity index 50% rename from core-aam/acacia/test-testdriver.html rename to core-aam/acacia/all_apis_example.html index cffa0f8b48dbbf..38e2e3cca5b031 100644 --- a/core-aam/acacia/test-testdriver.html +++ b/core-aam/acacia/all_apis_example.html @@ -1,6 +1,6 @@ -core-aam: acacia test using testdriver +core-aam: role button @@ -8,18 +8,21 @@ -
+
click me
diff --git a/resources/testdriver.js b/resources/testdriver.js index 9bc0db5e208c96..ec892af499a9b5 100644 --- a/resources/testdriver.js +++ b/resources/testdriver.js @@ -1076,8 +1076,10 @@ * 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); + return window.test_driver_internal.get_accessibility_api_node(dom_id) + .then((jsonresult) => { + return JSON.parse(jsonresult); + }); } }; @@ -1269,7 +1271,7 @@ }, async get_accessibility_api_node(dom_id) { - throw new Error("not implemented, whoops!"); + throw new Error("get_accessibility_api_node() is not available."); } }; })(); diff --git a/tools/wptrunner/wptrunner/executors/executoratspi.py b/tools/wptrunner/wptrunner/executors/executoratspi.py index d0a2cdea210573..7bda5a7457963b 100644 --- a/tools/wptrunner/wptrunner/executors/executoratspi.py +++ b/tools/wptrunner/wptrunner/executors/executoratspi.py @@ -5,11 +5,6 @@ import threading from .protocol import (PlatformAccessibilityProtocolPart) - -import traceback -import time - - def find_active_tab(root): stack = [root] while stack: @@ -31,13 +26,11 @@ def find_active_tab(root): def serialize_node(node): node_dictionary = {} + node_dictionary['API'] = 'atspi'; node_dictionary['role'] = Atspi.Accessible.get_role_name(node) node_dictionary['name'] = Atspi.Accessible.get_name(node) node_dictionary['description'] = Atspi.Accessible.get_description(node) - # TODO: serialize other attributes - # states, interfaces, attributes, etc. - return node_dictionary def find_node(root, dom_id): @@ -99,18 +92,19 @@ def setup(self, product_name): def get_accessibility_api_node(self, dom_id): if not self.found_browser: - return json.dumps({"role": "couldn't find browser"}) + raise Exception(f"Couldn't find browser {self.product_name}. Did you turn on accessibility?") if not self.load_complete: self.atspi_listener_thread.join() active_tab = find_active_tab(self.root) if not active_tab: - return json.dumps({"role": "couldn't find active tab"}) + raise Exception(f"Could not find the test page within the browser. Did you turn on accessiblity?") node = find_node(active_tab, dom_id) if not node: - return json.dumps({"role": "couldn't find the node with that ID"}) + raise Exception(f"Couldn't find node with id {dom_id}.") + return json.dumps(serialize_node(node)) From c80d37864f72945f54526950aa66ae17640b1a05 Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Wed, 29 May 2024 12:27:52 -0700 Subject: [PATCH 09/28] python black formatter --- .../wptrunner/executors/executoracacia.py | 14 +++--- .../wptrunner/executors/executoratspi.py | 49 +++++++++++-------- .../wptrunner/executors/executoraxapi.py | 2 +- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/tools/wptrunner/wptrunner/executors/executoracacia.py b/tools/wptrunner/wptrunner/executors/executoracacia.py index 988230b05adca8..acafbc3625ea69 100644 --- a/tools/wptrunner/wptrunner/executors/executoracacia.py +++ b/tools/wptrunner/wptrunner/executors/executoracacia.py @@ -1,14 +1,15 @@ -from .protocol import (PlatformAccessibilityProtocolPart) +from .protocol import PlatformAccessibilityProtocolPart from sys import platform + linux = False mac = False if platform == "linux": - linux = True - from .executoratspi import * + linux = True + from .executoratspi import * if platform == "darwin": - mac = True - from .executoraxapi import * + mac = True + from .executoraxapi import * class AcaciaPlatformAccessibilityProtocolPart(PlatformAccessibilityProtocolPart): @@ -22,6 +23,5 @@ def setup(self): self.impl = AXAPIExecutorImpl() self.impl.setup(self.product_name) - def get_accessibility_api_node(self, dom_id): - return self.impl.get_accessibility_api_node(dom_id) \ No newline at end of file + return self.impl.get_accessibility_api_node(dom_id) diff --git a/tools/wptrunner/wptrunner/executors/executoratspi.py b/tools/wptrunner/wptrunner/executors/executoratspi.py index 7bda5a7457963b..cd0cae5503b386 100644 --- a/tools/wptrunner/wptrunner/executors/executoratspi.py +++ b/tools/wptrunner/wptrunner/executors/executoratspi.py @@ -1,21 +1,23 @@ import gi + gi.require_version("Atspi", "2.0") from gi.repository import Atspi import json import threading -from .protocol import (PlatformAccessibilityProtocolPart) +from .protocol import PlatformAccessibilityProtocolPart + def find_active_tab(root): stack = [root] while stack: node = stack.pop() - if Atspi.Accessible.get_role_name(node) == 'frame': + if Atspi.Accessible.get_role_name(node) == "frame": ## Helper: list of string relations, get targets for relation? relationset = Atspi.Accessible.get_relation_set(node) for relation in relationset: - if relation.get_relation_type() == Atspi.RelationType.EMBEDS: - return relation.get_target(0) + if relation.get_relation_type() == Atspi.RelationType.EMBEDS: + return relation.get_target(0) contiue for i in range(Atspi.Accessible.get_child_count(node)): @@ -24,22 +26,24 @@ def find_active_tab(root): return None + def serialize_node(node): node_dictionary = {} - node_dictionary['API'] = 'atspi'; - node_dictionary['role'] = Atspi.Accessible.get_role_name(node) - node_dictionary['name'] = Atspi.Accessible.get_name(node) - node_dictionary['description'] = Atspi.Accessible.get_description(node) + node_dictionary["API"] = "atspi" + node_dictionary["role"] = Atspi.Accessible.get_role_name(node) + node_dictionary["name"] = Atspi.Accessible.get_name(node) + node_dictionary["description"] = Atspi.Accessible.get_description(node) return node_dictionary + def find_node(root, dom_id): stack = [root] while stack: node = stack.pop() attributes = Atspi.Accessible.get_attributes(node) - if 'id' in attributes and attributes['id'] == dom_id: + if "id" in attributes and attributes["id"] == dom_id: return node for i in range(Atspi.Accessible.get_child_count(node)): @@ -48,6 +52,7 @@ def find_node(root, dom_id): return None + def find_browser(name): desktop = Atspi.get_desktop(0) child_count = Atspi.Accessible.get_child_count(desktop) @@ -59,53 +64,55 @@ def find_browser(name): return (None, None) -class AtspiExecutorImpl(): +class AtspiExecutorImpl: def start_atspi_listener(self): self._event_listener = Atspi.EventListener.new(self.handle_event) self._event_listener.register("document:load-complete") Atspi.event_main() - def handle_event(self, e): app = Atspi.Accessible.get_application(e.source) app_name = Atspi.Accessible.get_name(app) - if (self.full_app_name == app_name and e.any_data): + if self.full_app_name == app_name and e.any_data: self.load_complete = True self._event_listener.deregister("document:load-complete") Atspi.event_quit() def setup(self, product_name): self.product_name = product_name - self.full_app_name = '' + self.full_app_name = "" self.root = None self.found_browser = False self.load_complete = False self.atspi_listener_thread = threading.Thread(target=self.start_atspi_listener) - (self.root, self.full_app_name) = find_browser(self.product_name); + (self.root, self.full_app_name) = find_browser(self.product_name) if self.root: self.found_browser = True self.atspi_listener_thread.start() else: - print(f"Cannot find root accessibility node for {self.product_name} - did you turn on accessibility?") + print( + f"Cannot find root accessibility node for {self.product_name} - did you turn on accessibility?" + ) def get_accessibility_api_node(self, dom_id): if not self.found_browser: - raise Exception(f"Couldn't find browser {self.product_name}. Did you turn on accessibility?") + raise Exception( + f"Couldn't find browser {self.product_name}. Did you turn on accessibility?" + ) if not self.load_complete: - self.atspi_listener_thread.join() + self.atspi_listener_thread.join() active_tab = find_active_tab(self.root) if not active_tab: - raise Exception(f"Could not find the test page within the browser. Did you turn on accessiblity?") + raise Exception( + f"Could not find the test page within the browser. Did you turn on accessiblity?" + ) node = find_node(active_tab, dom_id) if not node: raise Exception(f"Couldn't find node with id {dom_id}.") - return json.dumps(serialize_node(node)) - - diff --git a/tools/wptrunner/wptrunner/executors/executoraxapi.py b/tools/wptrunner/wptrunner/executors/executoraxapi.py index 5663f116698a4f..571aa525c36092 100644 --- a/tools/wptrunner/wptrunner/executors/executoraxapi.py +++ b/tools/wptrunner/wptrunner/executors/executoraxapi.py @@ -15,6 +15,7 @@ kAXValueAXErrorType, ) + class AXAPIExecutorImpl: def setup(self, product_name): self.product_name = product_name @@ -33,4 +34,3 @@ def get_application_by_name(self, name): def get_accessibility_api_node(self, dom_id): app = self.get_application_by_name(self.product_name) - From 4bfc87c7108f7063a8751a7a7aa25d9da4cb4b59 Mon Sep 17 00:00:00 2001 From: Alice Boxhall <95208+alice@users.noreply.github.com> Date: Thu, 30 May 2024 13:02:07 +0200 Subject: [PATCH 10/28] Implement get_accessibility_api_node for AXAPI --- .../wptrunner/executors/executoratspi.py | 2 +- .../wptrunner/executors/executoraxapi.py | 109 ++++++++++++++---- 2 files changed, 87 insertions(+), 24 deletions(-) diff --git a/tools/wptrunner/wptrunner/executors/executoratspi.py b/tools/wptrunner/wptrunner/executors/executoratspi.py index cd0cae5503b386..09259423592789 100644 --- a/tools/wptrunner/wptrunner/executors/executoratspi.py +++ b/tools/wptrunner/wptrunner/executors/executoratspi.py @@ -18,7 +18,7 @@ def find_active_tab(root): for relation in relationset: if relation.get_relation_type() == Atspi.RelationType.EMBEDS: return relation.get_target(0) - contiue + continue for i in range(Atspi.Accessible.get_child_count(node)): child = Atspi.Accessible.get_child_at_index(node, i) diff --git a/tools/wptrunner/wptrunner/executors/executoraxapi.py b/tools/wptrunner/wptrunner/executors/executoraxapi.py index 571aa525c36092..f864cbfebb1754 100644 --- a/tools/wptrunner/wptrunner/executors/executoraxapi.py +++ b/tools/wptrunner/wptrunner/executors/executoraxapi.py @@ -1,36 +1,99 @@ from ApplicationServices import ( - AXUIElementRef, AXUIElementCopyAttributeNames, AXUIElementCopyAttributeValue, - AXUIElementCopyParameterizedAttributeValue, - AXUIElementCopyParameterizedAttributeNames, - AXUIElementIsAttributeSettable, - AXUIElementCopyActionNames, - AXUIElementSetAttributeValue, AXUIElementCreateApplication, - AXUIElementCopyMultipleAttributeValues, - AXUIElementCopyActionDescription, - AXValueRef, - AXValueGetType, - kAXValueAXErrorType, ) +from Cocoa import ( + NSApplicationActivationPolicyRegular, + NSPredicate, + NSWorkspace, +) + +import json + +def find_browser(name): + 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] '{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_active_tab(browser): + stack = [browser] + tabs = [] + while stack: + node = stack.pop() + + (err, role) = AXUIElementCopyAttributeValue(node, "AXRole", None) + if err: + continue + if role == "AXWebArea": + return node + + (err, children) = AXUIElementCopyAttributeValue(node, "AXChildren", None) + if err: + continue + stack.extend(children) + + return None + + +def find_node(root, attribute, expected_value): + stack = [root] + while stack: + node = stack.pop() + + (err, attributes) = AXUIElementCopyAttributeNames(node, None) + if err: + continue + if attribute in attributes: + (err, value) = AXUIElementCopyAttributeValue(node, attribute, None) + if err: + continue + if value == expected_value: + return node + + (err, children) = AXUIElementCopyAttributeValue(node, "AXChildren", None) + if err: + continue + stack.extend(children) + return None + + +def serialize_node(node): + props = {} + props["API"] = "axapi" + (err, role) = AXUIElementCopyAttributeValue(node, "AXRole", None) + props["role"] = role + (err, name) = AXUIElementCopyAttributeValue(node, "AXTitle", None) + props["name"] = name + (err, description) = AXUIElementCopyAttributeValue(node, "AXDescription", None) + props["description"] = description + + return props + class AXAPIExecutorImpl: def setup(self, product_name): self.product_name = product_name + self.root = find_browser(self.product_name) + + if not self.root: + raise Exception(f"Couldn't find application: {product_name}") - def get_application_by_name(self, name): - # TODO: copied directly from https://github.com/eeejay/pyax/blob/main/src/pyax/_uielement.py - wl = CGWindowListCopyWindowInfo( - kCGWindowListExcludeDesktopElements, kCGNullWindowID - ) - for w in wl: - n = w.valueForKey_("kCGWindowOwnerName") - if name == w.valueForKey_("kCGWindowOwnerName"): - return AXUIElementCreateApplication( - int((w.valueForKey_("kCGWindowOwnerPID"))) - ) def get_accessibility_api_node(self, dom_id): - app = self.get_application_by_name(self.product_name) + tab = find_active_tab(self.root) + node = find_node(tab, "AXDOMIdentifier", dom_id) + if not node: + raise Exception(f"Couldn't find node with ID {dom_id}. Try passing --force-renderer-accessibility.") + return json.dumps(serialize_node(node)) From 9790540475afd61cdd6920a52a0142c253c833ad Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Wed, 12 Jun 2024 11:12:29 -0700 Subject: [PATCH 11/28] Minor fixes --- tools/wptrunner/wptrunner/browsers/base.py | 3 +-- tools/wptrunner/wptrunner/testrunner.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/wptrunner/wptrunner/browsers/base.py b/tools/wptrunner/wptrunner/browsers/base.py index f9e2661285d33d..6b1465cde8aca9 100644 --- a/tools/wptrunner/wptrunner/browsers/base.py +++ b/tools/wptrunner/wptrunner/browsers/base.py @@ -430,8 +430,7 @@ def executor_browser(self) -> Tuple[Type[ExecutorBrowser], Mapping[str, Any]]: "host": self.host, "port": self.port, "pac": self.pac, - "env": self.env, - "pid": self.pid} + "env": self.env} 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/testrunner.py b/tools/wptrunner/wptrunner/testrunner.py index 950378ce77f225..af99042bf1b6f5 100644 --- a/tools/wptrunner/wptrunner/testrunner.py +++ b/tools/wptrunner/wptrunner/testrunner.py @@ -494,7 +494,7 @@ def wait_event(self): } try: command, data = self.command_queue.get(True, 1) - #self.logger.debug("Got command: %r" % command) + self.logger.debug("Got command: %r" % command) except OSError: self.logger.error("Got IOError from poll") return RunnerManagerState.restarting(self.state.subsuite, From 1422467fbd0fe28b03bc60237cb32ed53fcba9fc Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Wed, 12 Jun 2024 11:23:37 -0700 Subject: [PATCH 12/28] Remove the name acacia --- .../wptrunner/wptrunner/executors/executoratspi.py | 2 -- .../wptrunner/executors/executormarionette.py | 4 ++-- ...racacia.py => executorplatformaccessibility.py} | 10 ++++++++-- .../wptrunner/executors/executorwebdriver.py | 4 ++-- tools/wptrunner/wptrunner/executors/protocol.py | 14 -------------- 5 files changed, 12 insertions(+), 22 deletions(-) rename tools/wptrunner/wptrunner/executors/{executoracacia.py => executorplatformaccessibility.py} (71%) diff --git a/tools/wptrunner/wptrunner/executors/executoratspi.py b/tools/wptrunner/wptrunner/executors/executoratspi.py index 09259423592789..9c2b2bb742b185 100644 --- a/tools/wptrunner/wptrunner/executors/executoratspi.py +++ b/tools/wptrunner/wptrunner/executors/executoratspi.py @@ -4,8 +4,6 @@ from gi.repository import Atspi import json import threading -from .protocol import PlatformAccessibilityProtocolPart - def find_active_tab(root): stack = [root] diff --git a/tools/wptrunner/wptrunner/executors/executormarionette.py b/tools/wptrunner/wptrunner/executors/executormarionette.py index e3f29959858a90..fae212645c2a49 100644 --- a/tools/wptrunner/wptrunner/executors/executormarionette.py +++ b/tools/wptrunner/wptrunner/executors/executormarionette.py @@ -49,7 +49,7 @@ DevicePostureProtocolPart, merge_dicts) -from .executoracacia import (AcaciaPlatformAccessibilityProtocolPart) +from .executorplatformaccessibility import (PlatformAccessibilityProtocolPart) def do_delayed_imports(): global errors, marionette, Addons, WebAuthn @@ -785,7 +785,7 @@ class MarionetteProtocol(Protocol): MarionetteAccessibilityProtocolPart, MarionetteVirtualSensorProtocolPart, MarionetteDevicePostureProtocolPart, - AcaciaPlatformAccessibilityProtocolPart] + PlatformAccessibilityProtocolPart] def __init__(self, executor, browser, capabilities=None, timeout_multiplier=1, e10s=True, ccov=False): do_delayed_imports() diff --git a/tools/wptrunner/wptrunner/executors/executoracacia.py b/tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py similarity index 71% rename from tools/wptrunner/wptrunner/executors/executoracacia.py rename to tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py index acafbc3625ea69..3b241569617f1a 100644 --- a/tools/wptrunner/wptrunner/executors/executoracacia.py +++ b/tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py @@ -1,5 +1,6 @@ -from .protocol import PlatformAccessibilityProtocolPart +from .protocol import ProtocolPart +from abc import ABCMeta from sys import platform linux = False @@ -12,7 +13,12 @@ from .executoraxapi import * -class AcaciaPlatformAccessibilityProtocolPart(PlatformAccessibilityProtocolPart): +class PlatformAccessibilityProtocolPart(ProtocolPart): + """Protocol part for platform accessibility introspection""" + __metaclass__ = ABCMeta + + name = "platform_accessibility" + def setup(self): self.product_name = self.parent.product_name self.impl = None diff --git a/tools/wptrunner/wptrunner/executors/executorwebdriver.py b/tools/wptrunner/wptrunner/executors/executorwebdriver.py index 7982ded4b42ca4..2664e3dc68f4da 100644 --- a/tools/wptrunner/wptrunner/executors/executorwebdriver.py +++ b/tools/wptrunner/wptrunner/executors/executorwebdriver.py @@ -38,7 +38,7 @@ DevicePostureProtocolPart, merge_dicts) -from .executoracacia import (AcaciaPlatformAccessibilityProtocolPart) +from .executorplatformaccessibility import (PlatformAccessibilityProtocolPart) from webdriver.client import Session from webdriver import error @@ -465,7 +465,7 @@ class WebDriverProtocol(Protocol): WebDriverDebugProtocolPart, WebDriverVirtualSensorPart, WebDriverDevicePostureProtocolPart, - AcaciaPlatformAccessibilityProtocolPart] + PlatformAccessibilityProtocolPart] def __init__(self, executor, browser, capabilities, **kwargs): super().__init__(executor, browser) diff --git a/tools/wptrunner/wptrunner/executors/protocol.py b/tools/wptrunner/wptrunner/executors/protocol.py index 3d8aa2fc37b417..3d588738b6e005 100644 --- a/tools/wptrunner/wptrunner/executors/protocol.py +++ b/tools/wptrunner/wptrunner/executors/protocol.py @@ -321,20 +321,6 @@ 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 From b061a9513340dca3ca9f838ce9dc5a806720bbf6 Mon Sep 17 00:00:00 2001 From: Alice Boxhall <95208+alice@users.noreply.github.com> Date: Wed, 10 Jul 2024 17:56:34 +0200 Subject: [PATCH 13/28] just poll for the active tab --- tools/wpt/virtualenv.py | 1 + tools/wpt/wpt.py | 1 + tools/wptrunner/wptrunner/executors/base.py | 2 +- .../wptrunner/executors/executoratspi.py | 60 ++++++++++--------- tools/wptrunner/wptrunner/testdriver-extra.js | 1 + tools/wptrunner/wptrunner/testrunner.py | 2 +- 6 files changed, 38 insertions(+), 29 deletions(-) diff --git a/tools/wpt/virtualenv.py b/tools/wpt/virtualenv.py index f1fad7327013d8..9bb9e65adc5199 100644 --- a/tools/wpt/virtualenv.py +++ b/tools/wpt/virtualenv.py @@ -58,6 +58,7 @@ def bin_path(self): @property def pip_path(self): + print(f"pip_path, self.bin_path is {self.bin_path}", file=sys.stderr) path = which("pip3", path=self.bin_path) if path is None: path = which("pip", path=self.bin_path) diff --git a/tools/wpt/wpt.py b/tools/wpt/wpt.py index e1df4ef42dcfb8..f7db50e98f02e4 100644 --- a/tools/wpt/wpt.py +++ b/tools/wpt/wpt.py @@ -127,6 +127,7 @@ def create_complete_parser(): # `wpt build-docs` command but we need to look up the environment to # find out where it's located. venv_path = os.environ["VIRTUAL_ENV"] + print(f"venv_path: {venv_path}", file=sys.stderr) venv = virtualenv.Virtualenv(venv_path, True) for command in commands: diff --git a/tools/wptrunner/wptrunner/executors/base.py b/tools/wptrunner/wptrunner/executors/base.py index 92a782e835c11b..936a0c09493b65 100644 --- a/tools/wptrunner/wptrunner/executors/base.py +++ b/tools/wptrunner/wptrunner/executors/base.py @@ -747,7 +747,7 @@ def process_complete(self, url, payload): def process_action(self, url, payload): action = payload["action"] cmd_id = payload["id"] - self.logger.debug(f"Got action: {action}") + self.logger.info(f"Got action: {action}") try: action_handler = self.actions[action] except KeyError as e: diff --git a/tools/wptrunner/wptrunner/executors/executoratspi.py b/tools/wptrunner/wptrunner/executors/executoratspi.py index 9c2b2bb742b185..0c5cae04bee2b6 100644 --- a/tools/wptrunner/wptrunner/executors/executoratspi.py +++ b/tools/wptrunner/wptrunner/executors/executoratspi.py @@ -4,13 +4,30 @@ from gi.repository import Atspi import json import threading +import time + +import sys + +def poll_for_active_tab(root): + active_tab = find_active_tab(root) + iterations = 0 + while not active_tab: + time.sleep(0.01) + active_tab = find_active_tab(root) + iterations += 1 + + print(f"found active tab in {iterations} iterations", file=sys.stderr) + return active_tab + def find_active_tab(root): stack = [root] + root_role = Atspi.Accessible.get_role_name(root) while stack: node = stack.pop() - + role= Atspi.Accessible.get_role_name(node) if Atspi.Accessible.get_role_name(node) == "frame": + attributes = Atspi.Accessible.get_attributes(node) ## Helper: list of string relations, get targets for relation? relationset = Atspi.Accessible.get_relation_set(node) for relation in relationset: @@ -63,32 +80,15 @@ def find_browser(name): class AtspiExecutorImpl: - def start_atspi_listener(self): - self._event_listener = Atspi.EventListener.new(self.handle_event) - self._event_listener.register("document:load-complete") - Atspi.event_main() - - def handle_event(self, e): - app = Atspi.Accessible.get_application(e.source) - app_name = Atspi.Accessible.get_name(app) - if self.full_app_name == app_name and e.any_data: - self.load_complete = True - self._event_listener.deregister("document:load-complete") - Atspi.event_quit() - def setup(self, product_name): self.product_name = product_name self.full_app_name = "" self.root = None self.found_browser = False - self.load_complete = False - - self.atspi_listener_thread = threading.Thread(target=self.start_atspi_listener) (self.root, self.full_app_name) = find_browser(self.product_name) if self.root: self.found_browser = True - self.atspi_listener_thread.start() else: print( f"Cannot find root accessibility node for {self.product_name} - did you turn on accessibility?" @@ -100,15 +100,21 @@ def get_accessibility_api_node(self, dom_id): f"Couldn't find browser {self.product_name}. Did you turn on accessibility?" ) - if not self.load_complete: - self.atspi_listener_thread.join() - - active_tab = find_active_tab(self.root) - if not active_tab: - raise Exception( - f"Could not find the test page within the browser. Did you turn on accessiblity?" - ) - + active_tab = poll_for_active_tab(self.root) + + state_set = Atspi.Accessible.get_state_set(active_tab) + iterations = 0 + while Atspi.StateSet.contains(state_set, Atspi.StateType.BUSY): + state_set = Atspi.Accessible.get_state_set(active_tab) + iterations += 1 + print(f"active tab no longer busy after {iterations} iterations", file=sys.stderr) + role = Atspi.Accessible.get_role_name(active_tab) + attributes = Atspi.Accessible.get_attributes(active_tab) + print(f"active tab role: {role}; attributes: {attributes}", file=sys.stderr) + document = Atspi.Accessible.get_document_iface(active_tab) + document_attributes = Atspi.Document.get_document_attributes(document) + url = document_attributes["DocURL"] + print(f"document: {document}; attributes: {document_attributes}, url: {url}", file=sys.stderr) node = find_node(active_tab, dom_id) if not node: raise Exception(f"Couldn't find node with id {dom_id}.") diff --git a/tools/wptrunner/wptrunner/testdriver-extra.js b/tools/wptrunner/wptrunner/testdriver-extra.js index e80c3ecb3e31e8..fc063c1964f729 100644 --- a/tools/wptrunner/wptrunner/testdriver-extra.js +++ b/tools/wptrunner/wptrunner/testdriver-extra.js @@ -125,6 +125,7 @@ return selector; }; + // NOTE(alice): this is where the action is :D const create_action = function(name, props) { let cmd_id; const action_msg = {type: "action", diff --git a/tools/wptrunner/wptrunner/testrunner.py b/tools/wptrunner/wptrunner/testrunner.py index af99042bf1b6f5..59d25371d0df45 100644 --- a/tools/wptrunner/wptrunner/testrunner.py +++ b/tools/wptrunner/wptrunner/testrunner.py @@ -494,7 +494,7 @@ def wait_event(self): } try: command, data = self.command_queue.get(True, 1) - self.logger.debug("Got command: %r" % command) + # self.logger.debug("Got command: %r" % command) except OSError: self.logger.error("Got IOError from poll") return RunnerManagerState.restarting(self.state.subsuite, From e6ad95b2f924a3aabf4764965d18457f42637353 Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Wed, 10 Jul 2024 10:28:47 -0700 Subject: [PATCH 14/28] Remove unnecessary function --- tools/webdriver/webdriver/client.py | 4 ---- tools/wptrunner/wptrunner/browsers/.#firefox.py | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) create mode 120000 tools/wptrunner/wptrunner/browsers/.#firefox.py diff --git a/tools/webdriver/webdriver/client.py b/tools/webdriver/webdriver/client.py index 1c9fd88965169a..e41df7f57640a6 100644 --- a/tools/webdriver/webdriver/client.py +++ b/tools/webdriver/webdriver/client.py @@ -879,10 +879,6 @@ 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/wptrunner/wptrunner/browsers/.#firefox.py b/tools/wptrunner/wptrunner/browsers/.#firefox.py new file mode 120000 index 00000000000000..8ec535f12d7f41 --- /dev/null +++ b/tools/wptrunner/wptrunner/browsers/.#firefox.py @@ -0,0 +1 @@ +spectranaut@thinkmoon.5003:1715889962 \ No newline at end of file From f31b0fbc9cb752f670ffeadf64f3539a899f098d Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Wed, 10 Jul 2024 10:30:53 -0700 Subject: [PATCH 15/28] Remove file --- tools/wptrunner/wptrunner/browsers/.#firefox.py | 1 - 1 file changed, 1 deletion(-) delete mode 120000 tools/wptrunner/wptrunner/browsers/.#firefox.py diff --git a/tools/wptrunner/wptrunner/browsers/.#firefox.py b/tools/wptrunner/wptrunner/browsers/.#firefox.py deleted file mode 120000 index 8ec535f12d7f41..00000000000000 --- a/tools/wptrunner/wptrunner/browsers/.#firefox.py +++ /dev/null @@ -1 +0,0 @@ -spectranaut@thinkmoon.5003:1715889962 \ No newline at end of file From 90e5d94baaf553959d19111ac4294fb8d0c39ede Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Wed, 17 Jul 2024 13:49:03 -0700 Subject: [PATCH 16/28] Fix polling to work for chrome --- .../wptrunner/executors/executoratspi.py | 73 ++++++++++--------- .../executorplatformaccessibility.py | 2 +- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/tools/wptrunner/wptrunner/executors/executoratspi.py b/tools/wptrunner/wptrunner/executors/executoratspi.py index 0c5cae04bee2b6..ea296d5e258db0 100644 --- a/tools/wptrunner/wptrunner/executors/executoratspi.py +++ b/tools/wptrunner/wptrunner/executors/executoratspi.py @@ -8,31 +8,32 @@ import sys -def poll_for_active_tab(root): - active_tab = find_active_tab(root) + +def poll_for_active_tab(root, logger, product): + active_tab = find_active_tab(root, product) iterations = 0 while not active_tab: time.sleep(0.01) - active_tab = find_active_tab(root) + active_tab = find_active_tab(root, product) iterations += 1 - print(f"found active tab in {iterations} iterations", file=sys.stderr) + logger.debug(f"Found ATSPI active tab in {iterations} iterations") return active_tab -def find_active_tab(root): +def find_active_tab(root, product): stack = [root] - root_role = Atspi.Accessible.get_role_name(root) while stack: node = stack.pop() - role= Atspi.Accessible.get_role_name(node) if Atspi.Accessible.get_role_name(node) == "frame": - attributes = Atspi.Accessible.get_attributes(node) - ## Helper: list of string relations, get targets for relation? relationset = Atspi.Accessible.get_relation_set(node) for relation in relationset: if relation.get_relation_type() == Atspi.RelationType.EMBEDS: - return relation.get_target(0) + active_tab = relation.get_target(0) + if is_ready(active_tab, product): + return active_tab + else: + return None continue for i in range(Atspi.Accessible.get_child_count(node)): @@ -42,6 +43,24 @@ def find_active_tab(root): return None +def is_ready(active_tab, product): + # Firefox uses the "BUSY" state to indicate the page is not ready. + if product == "firefox": + state_set = Atspi.Accessible.get_state_set(active_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 Title attribute. If the 'Title' + # attribute is not here, we need to query for a new accessible object. + # TODO: eventually we should test this against the actual title of the + # page being tested. + document = Atspi.Accessible.get_document_iface(active_tab) + document_attributes = Atspi.Document.get_document_attributes(document) + if "Title" in document_attributes and document_attributes["Title"]: + return True + return False + + def serialize_node(node): node_dictionary = {} node_dictionary["API"] = "atspi" @@ -80,43 +99,31 @@ def find_browser(name): class AtspiExecutorImpl: - def setup(self, product_name): + def setup(self, product_name, logger): + self.logger = logger self.product_name = product_name self.full_app_name = "" self.root = None self.found_browser = False (self.root, self.full_app_name) = find_browser(self.product_name) - if self.root: - self.found_browser = True - else: - print( - f"Cannot find root accessibility node for {self.product_name} - did you turn on accessibility?" + if not self.root: + self.logger.error( + f"Couldn't find browser {self.product_name} in accessibility API ATSPI. Accessibility API queries will not succeeded." ) def get_accessibility_api_node(self, dom_id): - if not self.found_browser: + if not self.root: raise Exception( - f"Couldn't find browser {self.product_name}. Did you turn on accessibility?" + f"Couldn't find browser {self.product_name} in accessibility API ATSPI. Did you turn on accessibility?" ) - active_tab = poll_for_active_tab(self.root) + active_tab = poll_for_active_tab(self.root, self.logger, self.product_name) - state_set = Atspi.Accessible.get_state_set(active_tab) - iterations = 0 - while Atspi.StateSet.contains(state_set, Atspi.StateType.BUSY): - state_set = Atspi.Accessible.get_state_set(active_tab) - iterations += 1 - print(f"active tab no longer busy after {iterations} iterations", file=sys.stderr) - role = Atspi.Accessible.get_role_name(active_tab) - attributes = Atspi.Accessible.get_attributes(active_tab) - print(f"active tab role: {role}; attributes: {attributes}", file=sys.stderr) - document = Atspi.Accessible.get_document_iface(active_tab) - document_attributes = Atspi.Document.get_document_attributes(document) - url = document_attributes["DocURL"] - print(f"document: {document}; attributes: {document_attributes}, url: {url}", file=sys.stderr) node = find_node(active_tab, dom_id) if not node: - raise Exception(f"Couldn't find node with id {dom_id}.") + raise Exception( + f"Couldn't find node with id={dom_id} in accessibility API ATSPI." + ) return json.dumps(serialize_node(node)) diff --git a/tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py b/tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py index 3b241569617f1a..b707352e7ab26d 100644 --- a/tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py +++ b/tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py @@ -24,7 +24,7 @@ def setup(self): self.impl = None if linux: self.impl = AtspiExecutorImpl() - self.impl.setup(self.product_name) + self.impl.setup(self.product_name, self.logger) if mac: self.impl = AXAPIExecutorImpl() self.impl.setup(self.product_name) From b5c17b6ee014321c59495f67bf36a5ab7d10329a Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Wed, 17 Jul 2024 13:54:06 -0700 Subject: [PATCH 17/28] Remove extra logs --- tools/wpt/virtualenv.py | 1 - tools/wpt/wpt.py | 1 - tools/wptrunner/wptrunner/executors/base.py | 2 +- tools/wptrunner/wptrunner/testdriver-extra.js | 1 - tools/wptrunner/wptrunner/testrunner.py | 2 +- 5 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tools/wpt/virtualenv.py b/tools/wpt/virtualenv.py index 9bb9e65adc5199..f1fad7327013d8 100644 --- a/tools/wpt/virtualenv.py +++ b/tools/wpt/virtualenv.py @@ -58,7 +58,6 @@ def bin_path(self): @property def pip_path(self): - print(f"pip_path, self.bin_path is {self.bin_path}", file=sys.stderr) path = which("pip3", path=self.bin_path) if path is None: path = which("pip", path=self.bin_path) diff --git a/tools/wpt/wpt.py b/tools/wpt/wpt.py index f7db50e98f02e4..e1df4ef42dcfb8 100644 --- a/tools/wpt/wpt.py +++ b/tools/wpt/wpt.py @@ -127,7 +127,6 @@ def create_complete_parser(): # `wpt build-docs` command but we need to look up the environment to # find out where it's located. venv_path = os.environ["VIRTUAL_ENV"] - print(f"venv_path: {venv_path}", file=sys.stderr) venv = virtualenv.Virtualenv(venv_path, True) for command in commands: diff --git a/tools/wptrunner/wptrunner/executors/base.py b/tools/wptrunner/wptrunner/executors/base.py index 936a0c09493b65..92a782e835c11b 100644 --- a/tools/wptrunner/wptrunner/executors/base.py +++ b/tools/wptrunner/wptrunner/executors/base.py @@ -747,7 +747,7 @@ def process_complete(self, url, payload): def process_action(self, url, payload): action = payload["action"] cmd_id = payload["id"] - self.logger.info(f"Got action: {action}") + self.logger.debug(f"Got action: {action}") try: action_handler = self.actions[action] except KeyError as e: diff --git a/tools/wptrunner/wptrunner/testdriver-extra.js b/tools/wptrunner/wptrunner/testdriver-extra.js index fc063c1964f729..e80c3ecb3e31e8 100644 --- a/tools/wptrunner/wptrunner/testdriver-extra.js +++ b/tools/wptrunner/wptrunner/testdriver-extra.js @@ -125,7 +125,6 @@ return selector; }; - // NOTE(alice): this is where the action is :D const create_action = function(name, props) { let cmd_id; const action_msg = {type: "action", diff --git a/tools/wptrunner/wptrunner/testrunner.py b/tools/wptrunner/wptrunner/testrunner.py index 59d25371d0df45..af99042bf1b6f5 100644 --- a/tools/wptrunner/wptrunner/testrunner.py +++ b/tools/wptrunner/wptrunner/testrunner.py @@ -494,7 +494,7 @@ def wait_event(self): } try: command, data = self.command_queue.get(True, 1) - # self.logger.debug("Got command: %r" % command) + self.logger.debug("Got command: %r" % command) except OSError: self.logger.error("Got IOError from poll") return RunnerManagerState.restarting(self.state.subsuite, From b73654bf6f6bfec87831a2320745729b5c90c0ab Mon Sep 17 00:00:00 2001 From: Alice <95208+alice@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:46:14 +0200 Subject: [PATCH 18/28] Update tools/wptrunner/wptrunner/executors/executoratspi.py --- tools/wptrunner/wptrunner/executors/executoratspi.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tools/wptrunner/wptrunner/executors/executoratspi.py b/tools/wptrunner/wptrunner/executors/executoratspi.py index ea296d5e258db0..d92af192aedf58 100644 --- a/tools/wptrunner/wptrunner/executors/executoratspi.py +++ b/tools/wptrunner/wptrunner/executors/executoratspi.py @@ -11,13 +11,10 @@ def poll_for_active_tab(root, logger, product): active_tab = find_active_tab(root, product) - iterations = 0 while not active_tab: time.sleep(0.01) active_tab = find_active_tab(root, product) - iterations += 1 - logger.debug(f"Found ATSPI active tab in {iterations} iterations") return active_tab From eb3958c1048143b048a963d190f8345ed2f05959 Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Wed, 19 Jun 2024 13:07:06 -0700 Subject: [PATCH 19/28] Add IA2 testing to testdriver.js --- core-aam/acacia/all_apis_example.html | 3 + resources/testdriver.js | 4 +- .../wptrunner/wptrunner/executors/actions.py | 3 +- .../wptrunner/executors/executoratspi.py | 3 +- .../wptrunner/executors/executoraxapi.py | 2 +- .../executorplatformaccessibility.py | 14 +- .../executors/executorwindowsaccessibility.py | 130 +++++++ .../wptrunner/executors/ia2/constants.py | 344 ++++++++++++++++++ .../wptrunner/executors/ia2/ia2_api_all.tlb | Bin 0 -> 29016 bytes tools/wptrunner/wptrunner/testdriver-extra.js | 4 +- 10 files changed, 495 insertions(+), 12 deletions(-) create mode 100644 tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py create mode 100644 tools/wptrunner/wptrunner/executors/ia2/constants.py create mode 100644 tools/wptrunner/wptrunner/executors/ia2/ia2_api_all.tlb diff --git a/core-aam/acacia/all_apis_example.html b/core-aam/acacia/all_apis_example.html index 38e2e3cca5b031..5dc231526f14c8 100644 --- a/core-aam/acacia/all_apis_example.html +++ b/core-aam/acacia/all_apis_example.html @@ -20,6 +20,9 @@ else if (node.API == 'axapi') { assert_equals(node.role, 'AXButton', 'AX API role'); } + else if (node.API == 'windows') { + assert_equals(node.msaa_role, 'ROLE_SYSTEM_PUSHBUTTON', 'MSAA Role'); + } else { assert_unreached(`Unknown API: ${node.API}`) } diff --git a/resources/testdriver.js b/resources/testdriver.js index ec892af499a9b5..6d77b9d0169f4e 100644 --- a/resources/testdriver.js +++ b/resources/testdriver.js @@ -1076,7 +1076,7 @@ * rejected in the cases of failures. */ get_accessibility_api_node: async function(dom_id) { - return window.test_driver_internal.get_accessibility_api_node(dom_id) + return window.test_driver_internal.get_accessibility_api_node(document.title, dom_id) .then((jsonresult) => { return JSON.parse(jsonresult); }); @@ -1270,7 +1270,7 @@ throw new Error("clear_device_posture() is not implemented by testdriver-vendor.js"); }, - async get_accessibility_api_node(dom_id) { + async get_accessibility_api_node(title, dom_id) { throw new Error("get_accessibility_api_node() is not available."); } }; diff --git a/tools/wptrunner/wptrunner/executors/actions.py b/tools/wptrunner/wptrunner/executors/actions.py index 90ae943ac25829..060d147a42d512 100644 --- a/tools/wptrunner/wptrunner/executors/actions.py +++ b/tools/wptrunner/wptrunner/executors/actions.py @@ -472,8 +472,9 @@ def __init__(self, logger, protocol): self.protocol = protocol def __call__(self, payload): + title = payload["title"] dom_id = payload["dom_id"] - return self.protocol.platform_accessibility.get_accessibility_api_node(dom_id) + return self.protocol.platform_accessibility.get_accessibility_api_node(title, dom_id) actions = [ClickAction, diff --git a/tools/wptrunner/wptrunner/executors/executoratspi.py b/tools/wptrunner/wptrunner/executors/executoratspi.py index d92af192aedf58..feb2fb114fe951 100644 --- a/tools/wptrunner/wptrunner/executors/executoratspi.py +++ b/tools/wptrunner/wptrunner/executors/executoratspi.py @@ -109,7 +109,8 @@ def setup(self, product_name, logger): f"Couldn't find browser {self.product_name} in accessibility API ATSPI. Accessibility API queries will not succeeded." ) - def get_accessibility_api_node(self, dom_id): + + def get_accessibility_api_node(self, dom_id, url): if not self.root: raise Exception( f"Couldn't find browser {self.product_name} in accessibility API ATSPI. Did you turn on accessibility?" diff --git a/tools/wptrunner/wptrunner/executors/executoraxapi.py b/tools/wptrunner/wptrunner/executors/executoraxapi.py index f864cbfebb1754..c9c52fdf3d79d3 100644 --- a/tools/wptrunner/wptrunner/executors/executoraxapi.py +++ b/tools/wptrunner/wptrunner/executors/executoraxapi.py @@ -91,7 +91,7 @@ def setup(self, product_name): raise Exception(f"Couldn't find application: {product_name}") - def get_accessibility_api_node(self, dom_id): + def get_accessibility_api_node(self, title, dom_id): tab = find_active_tab(self.root) node = find_node(tab, "AXDOMIdentifier", dom_id) if not node: diff --git a/tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py b/tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py index b707352e7ab26d..35fd6d42d91449 100644 --- a/tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py +++ b/tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py @@ -5,18 +5,19 @@ linux = False mac = False +windows = False if platform == "linux": linux = True from .executoratspi import * if platform == "darwin": mac = True from .executoraxapi import * - +if platform == "win32": + windows = True + from .executorwindowsaccessibility import WindowsAccessibilityExecutorImpl class PlatformAccessibilityProtocolPart(ProtocolPart): """Protocol part for platform accessibility introspection""" - __metaclass__ = ABCMeta - name = "platform_accessibility" def setup(self): @@ -28,6 +29,9 @@ def setup(self): if mac: self.impl = AXAPIExecutorImpl() self.impl.setup(self.product_name) + if windows: + self.impl = WindowsAccessibilityExecutorImpl() + self.impl.setup(self.product_name) - def get_accessibility_api_node(self, dom_id): - return self.impl.get_accessibility_api_node(dom_id) + def get_accessibility_api_node(self, title, dom_id): + return self.impl.get_accessibility_api_node(title, dom_id) diff --git a/tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py b/tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py new file mode 100644 index 00000000000000..0c5fe25ec8cf76 --- /dev/null +++ b/tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py @@ -0,0 +1,130 @@ +from ..executors.ia2.constants import * + +import json +import sys +import time + +import ctypes +from ctypes import POINTER, byref +from ctypes.wintypes import BOOL, HWND, LPARAM, POINT + +import comtypes.client +from comtypes import COMError, IServiceProvider + +CHILDID_SELF = 0 +OBJID_CLIENT = -4 + +user32 = ctypes.windll.user32 +oleacc = ctypes.oledll.oleacc +oleaccMod = comtypes.client.GetModule("oleacc.dll") +IAccessible = oleaccMod.IAccessible +del oleaccMod + +## CoCreateInstance of UIA also initialized IA2 +uiaMod = comtypes.client.GetModule("UIAutomationCore.dll") +uiaClient = comtypes.CoCreateInstance( + uiaMod.CUIAutomation._reg_clsid_, + interface=uiaMod.IUIAutomation, + clsctx=comtypes.CLSCTX_INPROC_SERVER, +) + +def accessible_object_from_window(hwnd, objectID=OBJID_CLIENT): + p = POINTER(IAccessible)() + oleacc.AccessibleObjectFromWindow( + hwnd, objectID, byref(IAccessible._iid_), byref(p) + ) + return p + +def name_from_hwnd(hwnd): + 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): + found = [] + + @ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) + def callback(hwnd, lParam): + name = name_from_hwnd(hwnd) + if product_name not in name.lower(): + return True + found.append(hwnd) + return False + + user32.EnumWindows(callback, LPARAM(0)) + if not found: + raise LookupError(f"Couldn't find {product_name} HWND") + return found[0] + +def to_ia2(node): + serv = node.QueryInterface(IServiceProvider) + return serv.QueryService(IAccessible2._iid_, IAccessible2) + +def find_browser(product_name): + hwnd = get_browser_hwnd(product_name) + root = accessible_object_from_window(hwnd) + return to_ia2(root) + +def poll_for_active_tab(title, root): + active_tab = find_active_tab(title, root) + iterations = 0 + while not active_tab: + time.sleep(0.01) + active_tab = find_active_tab(title, root) + iterations += 1 + + print(f"found active tab in {iterations} iterations", file=sys.stderr) + return active_tab + +def find_active_tab(title, root): + for i in range(1, root.accChildCount + 1): + child = to_ia2(root.accChild(i)) + if child.accRole(CHILDID_SELF) == ROLE_SYSTEM_DOCUMENT: + if child.accName(CHILDID_SELF) == title: + return child + # No need to search within documents. + return + descendant = find_active_tab(title, child) + if descendant: + return descendant + +def find_ia2_node(root, id): + search = f"id:{id};" + for i in range(1, root.accChildCount + 1): + child = to_ia2(root.accChild(i)) + if child.attributes and search in child.attributes: + return child + descendant = find_ia2_node(child, id) + if descendant: + return descendant + +def serialize_node(node): + node_dictionary = {} + node_dictionary["API"] = "windows" + + # MSAA properties + node_dictionary["name"] = node.accName(CHILDID_SELF) + node_dictionary["msaa_role"] = role_to_string[node.accRole(CHILDID_SELF)] + node_dictionary["msaa_states"] = get_msaa_state_list(node.accState(CHILDID_SELF)) + + # IAccessible2 properties + node_dictionary["ia2_role"] = role_to_string[node.role()] + node_dictionary["ia2_states"] = get_state_list(node.states) + + return node_dictionary + +class WindowsAccessibilityExecutorImpl: + def setup(self, product_name): + self.product_name = product_name + + def get_accessibility_api_node(self, title, dom_id): + self.root = find_browser(self.product_name) + if not self.root: + raise Exception(f"Couldn't find browser {self.product_name}.") + + active_tab = poll_for_active_tab(title, self.root) + node = find_ia2_node(active_tab, dom_id) + if not node: + raise Exception(f"Couldn't find node with ID {dom_id}.") + return json.dumps(serialize_node(node)) diff --git a/tools/wptrunner/wptrunner/executors/ia2/constants.py b/tools/wptrunner/wptrunner/executors/ia2/constants.py new file mode 100644 index 00000000000000..73b7ff2d3a7642 --- /dev/null +++ b/tools/wptrunner/wptrunner/executors/ia2/constants.py @@ -0,0 +1,344 @@ +import os +import comtypes.client + +# Get IAccessible2 constants for helper functions below +ia2Tlb = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "ia2_api_all.tlb", +) +ia2Mod = comtypes.client.GetModule(ia2Tlb) +# Add to globals the many IAccessible2 constants. These will also be imported +# when this file is imported. +globals().update((k, getattr(ia2Mod, k)) for k in ia2Mod.__all__) + +# Some constants are not provided via oleacc, specifically those for MSAA +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 + +role_to_string = { + ROLE_SYSTEM_TITLEBAR: "ROLE_SYSTEM_TITLEBAR", + ROLE_SYSTEM_MENUBAR: "ROLE_SYSTEM_MENUBAR", + ROLE_SYSTEM_SCROLLBAR: "ROLE_SYSTEM_SCROLLBAR", + ROLE_SYSTEM_GRIP: "ROLE_SYSTEM_GRIP", + ROLE_SYSTEM_SOUND: "ROLE_SYSTEM_SOUND", + ROLE_SYSTEM_CURSOR: "ROLE_SYSTEM_CURSOR", + ROLE_SYSTEM_CARET: "ROLE_SYSTEM_CARET", + ROLE_SYSTEM_ALERT: "ROLE_SYSTEM_ALERT", + ROLE_SYSTEM_WINDOW: "ROLE_SYSTEM_WINDOW", + ROLE_SYSTEM_CLIENT: "ROLE_SYSTEM_CLIENT", + ROLE_SYSTEM_MENUPOPUP: "ROLE_SYSTEM_MENUPOPUP", + ROLE_SYSTEM_MENUITEM: "ROLE_SYSTEM_MENUITEM", + ROLE_SYSTEM_TOOLTIP: "ROLE_SYSTEM_TOOLTIP", + ROLE_SYSTEM_APPLICATION: "ROLE_SYSTEM_APPLICATION", + ROLE_SYSTEM_DOCUMENT: "ROLE_SYSTEM_DOCUMENT", + ROLE_SYSTEM_PANE: "ROLE_SYSTEM_PANE", + ROLE_SYSTEM_CHART: "ROLE_SYSTEM_CHART", + ROLE_SYSTEM_DIALOG: "ROLE_SYSTEM_DIALOG", + ROLE_SYSTEM_BORDER: "ROLE_SYSTEM_BORDER", + ROLE_SYSTEM_GROUPING: "ROLE_SYSTEM_GROUPING", + ROLE_SYSTEM_SEPARATOR: "ROLE_SYSTEM_SEPARATOR", + ROLE_SYSTEM_TOOLBAR: "ROLE_SYSTEM_TOOLBAR", + ROLE_SYSTEM_STATUSBAR: "ROLE_SYSTEM_STATUSBAR", + ROLE_SYSTEM_TABLE: "ROLE_SYSTEM_TABLE", + ROLE_SYSTEM_COLUMNHEADER: "ROLE_SYSTEM_COLUMNHEADER", + ROLE_SYSTEM_ROWHEADER: "ROLE_SYSTEM_ROWHEADER", + ROLE_SYSTEM_COLUMN: "ROLE_SYSTEM_COLUMN", + ROLE_SYSTEM_ROW: "ROLE_SYSTEM_ROW", + ROLE_SYSTEM_CELL: "ROLE_SYSTEM_CEL", + ROLE_SYSTEM_LINK: "ROLE_SYSTEM_LINK", + ROLE_SYSTEM_HELPBALLOON: "ROLE_SYSTEM_HELPBALLOON", + ROLE_SYSTEM_CHARACTER: "ROLE_SYSTEM_CHARACTER", + ROLE_SYSTEM_LIST: "ROLE_SYSTEM_LIST", + ROLE_SYSTEM_LISTITEM: "ROLE_SYSTEM_LISTITEM", + ROLE_SYSTEM_OUTLINE: "ROLE_SYSTEM_OUTLINE", + ROLE_SYSTEM_OUTLINEITEM: "ROLE_SYSTEM_OUTLINEITEM", + ROLE_SYSTEM_PAGETAB: "ROLE_SYSTEM_PAGETAB", + ROLE_SYSTEM_PROPERTYPAGE: "ROLE_SYSTEM_PROPERTYPAGE", + ROLE_SYSTEM_INDICATOR: "ROLE_SYSTEM_INDICATOR", + ROLE_SYSTEM_GRAPHIC: "ROLE_SYSTEM_GRAPHIC", + ROLE_SYSTEM_STATICTEXT: "ROLE_SYSTEM_STATICTEXT", + ROLE_SYSTEM_TEXT: "ROLE_SYSTEM_TEXT", + ROLE_SYSTEM_PUSHBUTTON: "ROLE_SYSTEM_PUSHBUTTON", + ROLE_SYSTEM_CHECKBUTTON: "ROLE_SYSTEM_CHECKBUTTON", + ROLE_SYSTEM_RADIOBUTTON: "ROLE_SYSTEM_RADIOBUTTON", + ROLE_SYSTEM_COMBOBOX: "ROLE_SYSTEM_COMBOBOX", + ROLE_SYSTEM_DROPLIST: "ROLE_SYSTEM_DROPLIST", + ROLE_SYSTEM_PROGRESSBAR: "ROLE_SYSTEM_PROGRESSBAR", + ROLE_SYSTEM_DIAL: "ROLE_SYSTEM_DIAL", + ROLE_SYSTEM_HOTKEYFIELD: "ROLE_SYSTEM_HOTKEYFIELD", + ROLE_SYSTEM_SLIDER: "ROLE_SYSTEM_SLIDER", + ROLE_SYSTEM_SPINBUTTON: "ROLE_SYSTEM_SPINBUTTON", + ROLE_SYSTEM_DIAGRAM: "ROLE_SYSTEM_DIAGRAM", + ROLE_SYSTEM_ANIMATION: "ROLE_SYSTEM_ANIMATION", + ROLE_SYSTEM_EQUATION: "ROLE_SYSTEM_EQUATION", + ROLE_SYSTEM_BUTTONDROPDOWN: "ROLE_SYSTEM_BUTTONDROPDOWN", + ROLE_SYSTEM_BUTTONMENU: "ROLE_SYSTEM_BUTTONMENU", + ROLE_SYSTEM_BUTTONDROPDOWNGRID: "ROLE_SYSTEM_BUTTONDROPDOWNGRID", + ROLE_SYSTEM_WHITESPACE: "ROLE_SYSTEM_WHITESPACE", + ROLE_SYSTEM_PAGETABLIST: "ROLE_SYSTEM_PAGETABLIST", + ROLE_SYSTEM_CLOCK: "ROLE_SYSTEM_CLOCK", + ROLE_SYSTEM_SPLITBUTTON: "ROLE_SYSTEM_SPLITBUTTON", + ROLE_SYSTEM_IPADDRESS: "ROLE_SYSTEM_IPADDRESS", + ROLE_SYSTEM_OUTLINEBUTTON: "ROLE_SYSTEM_OUTLINEBUTTON", + IA2_ROLE_CANVAS: "IA2_ROLE_CANVAS", + IA2_ROLE_CAPTION: "IA2_ROLE_CAPTION", + IA2_ROLE_CHECK_MENU_ITEM: "IA2_ROLE_CHECK_MENU_ITEM", + IA2_ROLE_COLOR_CHOOSER: "IA2_ROLE_COLOR_CHOOSER", + IA2_ROLE_DATE_EDITOR: "IA2_ROLE_DATE_EDITOR", + IA2_ROLE_DESKTOP_ICON: "IA2_ROLE_DESKTOP_ICON", + IA2_ROLE_DESKTOP_PANE: "IA2_ROLE_DESKTOP_PANE", + IA2_ROLE_DIRECTORY_PANE: "IA2_ROLE_DIRECTORY_PANE", + IA2_ROLE_EDITBAR: "IA2_ROLE_EDITBAR", + IA2_ROLE_EMBEDDED_OBJECT: "IA2_ROLE_EMBEDDED_OBJECT", + IA2_ROLE_ENDNOTE: "IA2_ROLE_ENDNOTE", + IA2_ROLE_FILE_CHOOSER: "IA2_ROLE_FILE_CHOOSER", + IA2_ROLE_FONT_CHOOSER: "IA2_ROLE_FONT_CHOOSER", + IA2_ROLE_FOOTER: "IA2_ROLE_FOOTER", + IA2_ROLE_FOOTNOTE: "IA2_ROLE_FOOTNOTE", + IA2_ROLE_FORM: "IA2_ROLE_FORM", + IA2_ROLE_FRAME: "IA2_ROLE_FRAME", + IA2_ROLE_GLASS_PANE: "IA2_ROLE_GLASS_PANE", + IA2_ROLE_HEADER: "IA2_ROLE_HEADER", + IA2_ROLE_HEADING: "IA2_ROLE_HEADING", + IA2_ROLE_ICON: "IA2_ROLE_ICON", + IA2_ROLE_IMAGE_MAP: "IA2_ROLE_IMAGE_MAP", + IA2_ROLE_INPUT_METHOD_WINDOW: "IA2_ROLE_INPUT_METHOD_WINDOW", + IA2_ROLE_INTERNAL_FRAME: "IA2_ROLE_INTERNAL_FRAME", + IA2_ROLE_LABEL: "IA2_ROLE_LABEL", + IA2_ROLE_LAYERED_PANE: "IA2_ROLE_LAYERED_PANE", + IA2_ROLE_NOTE: "IA2_ROLE_NOTE", + IA2_ROLE_OPTION_PANE: "IA2_ROLE_OPTION_PANE", + IA2_ROLE_PAGE: "IA2_ROLE_PAGE", + IA2_ROLE_PARAGRAPH: "IA2_ROLE_PARAGRAPH", + IA2_ROLE_RADIO_MENU_ITEM: "IA2_ROLE_RADIO_MENU_ITEM", + IA2_ROLE_REDUNDANT_OBJECT: "IA2_ROLE_REDUNDANT_OBJECT", + IA2_ROLE_ROOT_PANE: "IA2_ROLE_ROOT_PANE", + IA2_ROLE_RULER: "IA2_ROLE_RULER", + IA2_ROLE_SCROLL_PANE: "IA2_ROLE_SCROLL_PANE", + IA2_ROLE_SECTION: "IA2_ROLE_SECTION", + IA2_ROLE_SHAPE: "IA2_ROLE_SHAPE", + IA2_ROLE_SPLIT_PANE: "IA2_ROLE_SPLIT_PANE", + IA2_ROLE_TEAR_OFF_MENU: "IA2_ROLE_TEAR_OFF_MENU", + IA2_ROLE_TERMINAL: "IA2_ROLE_TERMINAL", + IA2_ROLE_TEXT_FRAME: "IA2_ROLE_TEXT_FRAME", + IA2_ROLE_TOGGLE_BUTTON: "IA2_ROLE_TOGGLE_BUTTON", + IA2_ROLE_UNKNOWN: "IA2_ROLE_UNKNOWN", + IA2_ROLE_VIEW_PORT: "IA2_ROLE_VIEW_PORT", + IA2_ROLE_COMPLEMENTARY_CONTENT: "IA2_ROLE_COMPLEMENTARY_CONTENT", + IA2_ROLE_LANDMARK: "IA2_ROLE_LANDMARK", + IA2_ROLE_LEVEL_BAR: "IA2_ROLE_LEVEL_BAR", + IA2_ROLE_CONTENT_DELETION: "IA2_ROLE_CONTENT_DELETION", + IA2_ROLE_CONTENT_INSERTION: "IA2_ROLE_CONTENT_INSERTION", + IA2_ROLE_BLOCK_QUOTE: "IA2_ROLE_BLOCK_QUOTE", + IA2_ROLE_MARK: "IA2_ROLE_MARK", + IA2_ROLE_SUGGESTION: "IA2_ROLE_SUGGESTION", + IA2_ROLE_COMMENT: "IA2_ROLE_COMMENT" +} + +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_INDETERMINATE = STATE_SYSTEM_MIXED +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 +STATE_SYSTEM_VALID = 0x3FFFFFFF + +def get_msaa_state_list(states): + state_strings = [] + if states & STATE_SYSTEM_ALERT_HIGH: + state_strings.append("ALERT_HIGH") + if states & STATE_SYSTEM_ALERT_MEDIUM: + state_strings.append("ALERT_MEDIUM") + if states & STATE_SYSTEM_ALERT_LOW: + state_strings.append("ALERT_LOW") + if states & STATE_SYSTEM_ANIMATED: + state_strings.append("ANIMATED") + if states & STATE_SYSTEM_BUSY: + state_strings.append("BUSY") + if states & STATE_SYSTEM_CHECKED: + state_strings.append("CHECKED") + if states & STATE_SYSTEM_COLLAPSED: + state_strings.append("COLLAPSED") + if states & STATE_SYSTEM_DEFAULT: + state_strings.append("DEFAULT") + if states & STATE_SYSTEM_EXPANDED: + state_strings.append("EXPANDED") + if states & STATE_SYSTEM_EXTSELECTABLE: + state_strings.append("EXTSELECTABLE") + if states & STATE_SYSTEM_FLOATING: + state_strings.append("FLOATING") + if states & STATE_SYSTEM_FOCUSABLE: + state_strings.append("FOCUSABLE") + if states & STATE_SYSTEM_FOCUSED: + state_strings.append("FOCUSED") + if states & STATE_SYSTEM_HASPOPUP: + state_strings.append("HASPOPUP") + if states & STATE_SYSTEM_HOTTRACKED: + state_strings.append("HOTTRACKED") + if states & STATE_SYSTEM_INVISIBLE: + state_strings.append("INVISIBLE") + if states & STATE_SYSTEM_LINKED: + state_strings.append("LINKED") + if states & STATE_SYSTEM_MARQUEED: + state_strings.append("MARQUEED") + if states & STATE_SYSTEM_MIXED: + state_strings.append("MIXED") + if states & STATE_SYSTEM_MOVEABLE: + state_strings.append("MOVEABLE") + if states & STATE_SYSTEM_MULTISELECTABLE: + state_strings.append("MULTISELECTABLE") + if states & STATE_SYSTEM_OFFSCREEN: + state_strings.append("OFFSCREEN") + if states & STATE_SYSTEM_PRESSED: + state_strings.append("PRESSED") + if states & STATE_SYSTEM_PROTECTED: + state_strings.append("PROTECTED") + if states & STATE_SYSTEM_READONLY: + state_strings.append("READONLY") + if states & STATE_SYSTEM_SELECTABLE: + state_strings.append("SELECTABLE") + if states & STATE_SYSTEM_SELECTED: + state_strings.append("SELECTED") + if states & STATE_SYSTEM_SELFVOICING: + state_strings.append("SELFVOICING") + if states & STATE_SYSTEM_SIZEABLE: + state_strings.append("SIZEABLE") + if states & STATE_SYSTEM_TRAVERSED: + state_strings.append("TRAVERSED") + if states & STATE_SYSTEM_UNAVAILABLE: + state_strings.append("UNAVAILABLE") + + return state_strings + +def get_state_list(states): + state_strings = [] + if states & IA2_STATE_ACTIVE: + state_strings.append("ACTIVE") + if states & IA2_STATE_ARMED: + state_strings.append("ARMED") + if states & IA2_STATE_CHECKABLE: + state_strings.append("CHECKABLE") + if states & IA2_STATE_DEFUNCT: + state_strings.append("DEFUNCT") + if states & IA2_STATE_EDITABLE: + state_strings.append("EDITABLE") + if states & IA2_STATE_HORIZONTAL: + state_strings.append("HORIZONTAL") + if states & IA2_STATE_ICONIFIED: + state_strings.append("ICONIFIED") + if states & IA2_STATE_INVALID_ENTRY: + state_strings.append("INVALID_ENTRY") + if states & IA2_STATE_MANAGES_DESCENDANTS: + state_strings.append("MANAGES_DESCENDANTS") + if states & IA2_STATE_MODAL: + state_strings.append("MODAL") + if states & IA2_STATE_MULTI_LINE: + state_strings.append("MULTI_LINE") + if states & IA2_STATE_OPAQUE: + state_strings.append("OPAQUE") + if states & IA2_STATE_PINNED: + state_strings.append("PINNED") + if states & IA2_STATE_REQUIRED: + state_strings.append("REQUIRED") + if states & IA2_STATE_SELECTABLE_TEXT: + state_strings.append("SELECTABLE_TEXT") + if states & IA2_STATE_SINGLE_LINE: + state_strings.append("SINGLE_LINE") + if states & IA2_STATE_STALE: + state_strings.append("STALE") + if states & IA2_STATE_SUPPORTS_AUTOCOMPLETION: + state_strings.append("SUPPORTS_AUTOCOMPLETION") + if states & IA2_STATE_TRANSIENT: + state_strings.append("TRANSIENT") + if states & IA2_STATE_VERTICAL: + state_strings.append("VERTICAL") + + return state_strings diff --git a/tools/wptrunner/wptrunner/executors/ia2/ia2_api_all.tlb b/tools/wptrunner/wptrunner/executors/ia2/ia2_api_all.tlb new file mode 100644 index 0000000000000000000000000000000000000000..449c10b258dd7bcab244cd7279af1886da146da9 GIT binary patch literal 29016 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%0DP@Pz0KN= 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 BD0Tn< literal 0 HcmV?d00001 diff --git a/tools/wptrunner/wptrunner/testdriver-extra.js b/tools/wptrunner/wptrunner/testdriver-extra.js index e80c3ecb3e31e8..9f8427e8907364 100644 --- a/tools/wptrunner/wptrunner/testdriver-extra.js +++ b/tools/wptrunner/wptrunner/testdriver-extra.js @@ -336,7 +336,7 @@ 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}); + window.test_driver_internal.get_accessibility_api_node = function(title, dom_id) { + return create_action("get_accessibility_api_node", {title, dom_id}); }; })(); From e1779422df4bf4b13310db36ac29aca873858d76 Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Wed, 17 Jul 2024 11:00:11 -0700 Subject: [PATCH 20/28] Update tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py --- .../wptrunner/executors/executorwindowsaccessibility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py b/tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py index 0c5fe25ec8cf76..90620002da7528 100644 --- a/tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py +++ b/tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py @@ -20,7 +20,7 @@ IAccessible = oleaccMod.IAccessible del oleaccMod -## CoCreateInstance of UIA also initialized IA2 +# CoCreateInstance of UIA also initializes IA2 uiaMod = comtypes.client.GetModule("UIAutomationCore.dll") uiaClient = comtypes.CoCreateInstance( uiaMod.CUIAutomation._reg_clsid_, From b43ba7d7d89aca8d78ecbeb45a58db89c601dbd6 Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Fri, 19 Jul 2024 11:51:16 -0700 Subject: [PATCH 21/28] Review from Alice" --- .../executors/executorwindowsaccessibility.py | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py b/tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py index 90620002da7528..d8777409a626e1 100644 --- a/tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py +++ b/tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py @@ -18,7 +18,6 @@ oleacc = ctypes.oledll.oleacc oleaccMod = comtypes.client.GetModule("oleacc.dll") IAccessible = oleaccMod.IAccessible -del oleaccMod # CoCreateInstance of UIA also initializes IA2 uiaMod = comtypes.client.GetModule("UIAutomationCore.dll") @@ -28,10 +27,10 @@ clsctx=comtypes.CLSCTX_INPROC_SERVER, ) -def accessible_object_from_window(hwnd, objectID=OBJID_CLIENT): +def accessible_object_from_window(hwnd): p = POINTER(IAccessible)() oleacc.AccessibleObjectFromWindow( - hwnd, objectID, byref(IAccessible._iid_), byref(p) + hwnd, OBJID_CLIENT, byref(IAccessible._iid_), byref(p) ) return p @@ -45,39 +44,37 @@ def get_browser_hwnd(product_name): found = [] @ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) - def callback(hwnd, lParam): - name = name_from_hwnd(hwnd) - if product_name not in name.lower(): + def check_window_name(hwnd, lParam): + 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(callback, LPARAM(0)) + 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): - serv = node.QueryInterface(IServiceProvider) - return serv.QueryService(IAccessible2._iid_, IAccessible2) + service = node.QueryInterface(IServiceProvider) + return service.QueryService(IAccessible2._iid_, IAccessible2) def find_browser(product_name): hwnd = get_browser_hwnd(product_name) root = accessible_object_from_window(hwnd) return to_ia2(root) -def poll_for_active_tab(title, root): - active_tab = find_active_tab(title, root) - iterations = 0 - while not active_tab: +def poll_for_tab(title, root): + tab = find_tab(title, root) + while not tab: time.sleep(0.01) - active_tab = find_active_tab(title, root) - iterations += 1 + tab = find_tab(title, root) + return tab - print(f"found active tab in {iterations} iterations", file=sys.stderr) - return active_tab - -def find_active_tab(title, root): +def find_tab(title, root): for i in range(1, root.accChildCount + 1): child = to_ia2(root.accChild(i)) if child.accRole(CHILDID_SELF) == ROLE_SYSTEM_DOCUMENT: @@ -85,15 +82,15 @@ def find_active_tab(title, root): return child # No need to search within documents. return - descendant = find_active_tab(title, child) + descendant = find_tab(title, child) if descendant: return descendant def find_ia2_node(root, id): - search = f"id:{id};" + id_attribute = f"id:{id};" for i in range(1, root.accChildCount + 1): child = to_ia2(root.accChild(i)) - if child.attributes and search in child.attributes: + if child.attributes and id_attribute in child.attributes: return child descendant = find_ia2_node(child, id) if descendant: @@ -123,8 +120,8 @@ def get_accessibility_api_node(self, title, dom_id): if not self.root: raise Exception(f"Couldn't find browser {self.product_name}.") - active_tab = poll_for_active_tab(title, self.root) - node = find_ia2_node(active_tab, dom_id) + tab = poll_for_tab(title, self.root) + node = find_ia2_node(tab, dom_id) if not node: raise Exception(f"Couldn't find node with ID {dom_id}.") return json.dumps(serialize_node(node)) From 5edd9231f9303dc0703b709c943a2da90864bf7f Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Fri, 19 Jul 2024 12:13:52 -0700 Subject: [PATCH 22/28] Use URL instead of title --- resources/testdriver.js | 4 ++-- tools/wptrunner/wptrunner/executors/actions.py | 4 ++-- .../wptrunner/executors/executoraxapi.py | 2 +- .../executors/executorplatformaccessibility.py | 4 ++-- .../executors/executorwindowsaccessibility.py | 16 ++++++++-------- tools/wptrunner/wptrunner/testdriver-extra.js | 4 ++-- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/resources/testdriver.js b/resources/testdriver.js index 6d77b9d0169f4e..ac3ed65bd039f3 100644 --- a/resources/testdriver.js +++ b/resources/testdriver.js @@ -1076,7 +1076,7 @@ * rejected in the cases of failures. */ get_accessibility_api_node: async function(dom_id) { - return window.test_driver_internal.get_accessibility_api_node(document.title, dom_id) + return window.test_driver_internal.get_accessibility_api_node(dom_id, location.href) .then((jsonresult) => { return JSON.parse(jsonresult); }); @@ -1270,7 +1270,7 @@ throw new Error("clear_device_posture() is not implemented by testdriver-vendor.js"); }, - async get_accessibility_api_node(title, dom_id) { + async get_accessibility_api_node(dom_id, url) { throw new Error("get_accessibility_api_node() is not available."); } }; diff --git a/tools/wptrunner/wptrunner/executors/actions.py b/tools/wptrunner/wptrunner/executors/actions.py index 060d147a42d512..41c56585c70ce7 100644 --- a/tools/wptrunner/wptrunner/executors/actions.py +++ b/tools/wptrunner/wptrunner/executors/actions.py @@ -472,9 +472,9 @@ def __init__(self, logger, protocol): self.protocol = protocol def __call__(self, payload): - title = payload["title"] dom_id = payload["dom_id"] - return self.protocol.platform_accessibility.get_accessibility_api_node(title, dom_id) + url = payload["url"] + return self.protocol.platform_accessibility.get_accessibility_api_node(dom_id, url) actions = [ClickAction, diff --git a/tools/wptrunner/wptrunner/executors/executoraxapi.py b/tools/wptrunner/wptrunner/executors/executoraxapi.py index c9c52fdf3d79d3..54c4fea472b390 100644 --- a/tools/wptrunner/wptrunner/executors/executoraxapi.py +++ b/tools/wptrunner/wptrunner/executors/executoraxapi.py @@ -91,7 +91,7 @@ def setup(self, product_name): raise Exception(f"Couldn't find application: {product_name}") - def get_accessibility_api_node(self, title, dom_id): + def get_accessibility_api_node(self, dom_id, url): tab = find_active_tab(self.root) node = find_node(tab, "AXDOMIdentifier", dom_id) if not node: diff --git a/tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py b/tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py index 35fd6d42d91449..f7868dc4c16a92 100644 --- a/tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py +++ b/tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py @@ -33,5 +33,5 @@ def setup(self): self.impl = WindowsAccessibilityExecutorImpl() self.impl.setup(self.product_name) - def get_accessibility_api_node(self, title, dom_id): - return self.impl.get_accessibility_api_node(title, dom_id) + def get_accessibility_api_node(self, dom_id, url): + return self.impl.get_accessibility_api_node(dom_id, url) diff --git a/tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py b/tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py index d8777409a626e1..bca19b40d82636 100644 --- a/tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py +++ b/tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py @@ -67,22 +67,22 @@ def find_browser(product_name): root = accessible_object_from_window(hwnd) return to_ia2(root) -def poll_for_tab(title, root): - tab = find_tab(title, root) +def poll_for_tab(url, root): + tab = find_tab(url, root) while not tab: time.sleep(0.01) - tab = find_tab(title, root) + tab = find_tab(url, root) return tab -def find_tab(title, root): +def find_tab(url, root): for i in range(1, root.accChildCount + 1): child = to_ia2(root.accChild(i)) if child.accRole(CHILDID_SELF) == ROLE_SYSTEM_DOCUMENT: - if child.accName(CHILDID_SELF) == title: + if child.accValue(CHILDID_SELF) == url: return child # No need to search within documents. return - descendant = find_tab(title, child) + descendant = find_tab(url, child) if descendant: return descendant @@ -115,12 +115,12 @@ class WindowsAccessibilityExecutorImpl: def setup(self, product_name): self.product_name = product_name - def get_accessibility_api_node(self, title, dom_id): + def get_accessibility_api_node(self, dom_id, url): self.root = find_browser(self.product_name) if not self.root: raise Exception(f"Couldn't find browser {self.product_name}.") - tab = poll_for_tab(title, self.root) + tab = poll_for_tab(url, self.root) node = find_ia2_node(tab, dom_id) if not node: raise Exception(f"Couldn't find node with ID {dom_id}.") diff --git a/tools/wptrunner/wptrunner/testdriver-extra.js b/tools/wptrunner/wptrunner/testdriver-extra.js index 9f8427e8907364..9a5880b17a4713 100644 --- a/tools/wptrunner/wptrunner/testdriver-extra.js +++ b/tools/wptrunner/wptrunner/testdriver-extra.js @@ -336,7 +336,7 @@ return create_action("clear_device_posture", {context}); }; - window.test_driver_internal.get_accessibility_api_node = function(title, dom_id) { - return create_action("get_accessibility_api_node", {title, dom_id}); + window.test_driver_internal.get_accessibility_api_node = function(dom_id, url) { + return create_action("get_accessibility_api_node", {dom_id, url}); }; })(); From 78a91b2f9c7c2642ea5088f0bf0f20d7749c7bbd Mon Sep 17 00:00:00 2001 From: valerie young Date: Fri, 19 Jul 2024 13:47:53 -0700 Subject: [PATCH 23/28] Linux: Add support to run multiple tests --- core-aam/acacia/blockquote.html | 32 +++++++++++++++ .../{all_apis_example.html => button.html} | 0 .../wptrunner/executors/executoratspi.py | 40 ++++++++++--------- 3 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 core-aam/acacia/blockquote.html rename core-aam/acacia/{all_apis_example.html => button.html} (100%) diff --git a/core-aam/acacia/blockquote.html b/core-aam/acacia/blockquote.html new file mode 100644 index 00000000000000..cf51e4945445a0 --- /dev/null +++ b/core-aam/acacia/blockquote.html @@ -0,0 +1,32 @@ + + +core-aam: blockquote role + + + + + + + +
quote
+ + + diff --git a/core-aam/acacia/all_apis_example.html b/core-aam/acacia/button.html similarity index 100% rename from core-aam/acacia/all_apis_example.html rename to core-aam/acacia/button.html diff --git a/tools/wptrunner/wptrunner/executors/executoratspi.py b/tools/wptrunner/wptrunner/executors/executoratspi.py index feb2fb114fe951..ff04b191ced224 100644 --- a/tools/wptrunner/wptrunner/executors/executoratspi.py +++ b/tools/wptrunner/wptrunner/executors/executoratspi.py @@ -9,16 +9,16 @@ import sys -def poll_for_active_tab(root, logger, product): - active_tab = find_active_tab(root, product) - while not active_tab: +def poll_for_tab(root, product, url): + tab = find_tab(root, product, url) + while not tab: time.sleep(0.01) - active_tab = find_active_tab(root, product) + tab = find_tab(root, product, url) - return active_tab + return tab -def find_active_tab(root, product): +def find_tab(root, product, url): stack = [root] while stack: node = stack.pop() @@ -26,9 +26,9 @@ def find_active_tab(root, product): relationset = Atspi.Accessible.get_relation_set(node) for relation in relationset: if relation.get_relation_type() == Atspi.RelationType.EMBEDS: - active_tab = relation.get_target(0) - if is_ready(active_tab, product): - return active_tab + tab = relation.get_target(0) + if is_ready(tab, product, url): + return tab else: return None continue @@ -40,20 +40,18 @@ def find_active_tab(root, product): return None -def is_ready(active_tab, product): +def is_ready(tab, product, url): # Firefox uses the "BUSY" state to indicate the page is not ready. if product == "firefox": - state_set = Atspi.Accessible.get_state_set(active_tab) + 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 Title attribute. If the 'Title' + # tell if the document can be queried by URL attribute. If the 'URL' # attribute is not here, we need to query for a new accessible object. - # TODO: eventually we should test this against the actual title of the - # page being tested. - document = Atspi.Accessible.get_document_iface(active_tab) + document = Atspi.Accessible.get_document_iface(tab) document_attributes = Atspi.Document.get_document_attributes(document) - if "Title" in document_attributes and document_attributes["Title"]: + if "URI" in document_attributes and document_attributes["URI"] == url: return True return False @@ -101,7 +99,9 @@ def setup(self, product_name, logger): self.product_name = product_name self.full_app_name = "" self.root = None - self.found_browser = False + self.document = None + self.test_url = None + (self.root, self.full_app_name) = find_browser(self.product_name) if not self.root: @@ -116,9 +116,11 @@ def get_accessibility_api_node(self, dom_id, url): f"Couldn't find browser {self.product_name} in accessibility API ATSPI. Did you turn on accessibility?" ) - active_tab = poll_for_active_tab(self.root, self.logger, self.product_name) + if self.test_url != url or not self.document: + self.test_url = url + self.document = poll_for_tab(self.root, self.product_name, url) - node = find_node(active_tab, dom_id) + node = find_node(self.document, dom_id) if not node: raise Exception( f"Couldn't find node with id={dom_id} in accessibility API ATSPI." From c0312401b3655fca500c977e584fd359a3c3b80a Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Fri, 19 Jul 2024 14:01:17 -0700 Subject: [PATCH 24/28] Fix test --- core-aam/acacia/blockquote.html | 2 +- .../wptrunner/executors/.#executorwindowsaccessibility.py | 1 + tools/wptrunner/wptrunner/executors/executoratspi.py | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 tools/wptrunner/wptrunner/executors/.#executorwindowsaccessibility.py diff --git a/core-aam/acacia/blockquote.html b/core-aam/acacia/blockquote.html index cf51e4945445a0..6857c7987d4aaa 100644 --- a/core-aam/acacia/blockquote.html +++ b/core-aam/acacia/blockquote.html @@ -22,7 +22,7 @@ } else if (node.API == 'windows') { assert_equals(node.msaa_role, 'ROLE_SYSTEM_GROUPING', 'MSAA Role'); - assert_equals(node.msaa_role, 'IA2_ROLE_BLOCK_QUOTE', 'IA2 Role'); + assert_equals(node.ia2_role, 'IA2_ROLE_BLOCK_QUOTE', 'IA2 Role'); } else { assert_unreached(`Unknown API: ${node.API}`) diff --git a/tools/wptrunner/wptrunner/executors/.#executorwindowsaccessibility.py b/tools/wptrunner/wptrunner/executors/.#executorwindowsaccessibility.py new file mode 100644 index 00000000000000..ee175b8872fee4 --- /dev/null +++ b/tools/wptrunner/wptrunner/executors/.#executorwindowsaccessibility.py @@ -0,0 +1 @@ +spectranaut@LicensedWindows.4440:1721410383 \ No newline at end of file diff --git a/tools/wptrunner/wptrunner/executors/executoratspi.py b/tools/wptrunner/wptrunner/executors/executoratspi.py index ff04b191ced224..c71caecb128d0f 100644 --- a/tools/wptrunner/wptrunner/executors/executoratspi.py +++ b/tools/wptrunner/wptrunner/executors/executoratspi.py @@ -102,7 +102,6 @@ def setup(self, product_name, logger): self.document = None self.test_url = None - (self.root, self.full_app_name) = find_browser(self.product_name) if not self.root: self.logger.error( From 8b315ecc8332cc3dbcdb21694f046c8f5e97a597 Mon Sep 17 00:00:00 2001 From: Valerie Young Date: Wed, 24 Jul 2024 17:07:03 -0700 Subject: [PATCH 25/28] Update API name to include 'platform' --- core-aam/acacia/blockquote.html | 2 +- core-aam/acacia/button.html | 2 +- resources/testdriver.js | 8 ++++---- tools/wptrunner/wptrunner/executors/actions.py | 4 ++-- tools/wptrunner/wptrunner/executors/executoratspi.py | 2 +- tools/wptrunner/wptrunner/executors/executoraxapi.py | 2 +- .../wptrunner/executors/executorplatformaccessibility.py | 4 ++-- .../wptrunner/executors/executorwindowsaccessibility.py | 2 +- tools/wptrunner/wptrunner/testdriver-extra.js | 4 ++-- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/core-aam/acacia/blockquote.html b/core-aam/acacia/blockquote.html index 6857c7987d4aaa..77bd526822f11d 100644 --- a/core-aam/acacia/blockquote.html +++ b/core-aam/acacia/blockquote.html @@ -12,7 +12,7 @@