forked from web-platform-tests/wpt
-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Experiments with extending testdriver.js for platform accessibility API testing #2
Draft
spectranaut
wants to merge
32
commits into
master
Choose a base branch
from
acacia-test-testdriver
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 14 commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
5fbd9d7
Extend testdriver to add accessibility API testing
spectranaut 21d2d49
Switch from acacia to gi.repository Atspi
spectranaut e95bc28
Wait for document:load-complete
spectranaut d9dc813
Very minimal first pass
alice c794ae7
Add executoratspi and executoraxapi
alice 1e9eee4
Update class name
spectranaut 0dd7518
Fixes for when browser is not found
spectranaut 8a14630
Clean up example
spectranaut c80d378
python black formatter
spectranaut de8626e
Merge pull request #5 from Igalia/cleanup-testdriver
alice 4bfc87c
Implement get_accessibility_api_node for AXAPI
alice 52b469c
Merge pull request #7 from Igalia/cleanup-testdriver
bkardell 9790540
Minor fixes
spectranaut 1422467
Remove the name acacia
spectranaut b061a95
just poll for the active tab
alice e6ad95b
Remove unnecessary function
spectranaut f31b0fb
Remove file
spectranaut 90e5d94
Fix polling to work for chrome
spectranaut b5c17b6
Remove extra logs
spectranaut b73654b
Update tools/wptrunner/wptrunner/executors/executoratspi.py
alice 41ad1e2
Merge pull request #9 from Igalia/poll-active-tab
alice eb3958c
Add IA2 testing to testdriver.js
spectranaut e177942
Update tools/wptrunner/wptrunner/executors/executorwindowsaccessibili…
spectranaut b43ba7d
Review from Alice"
spectranaut 5edd923
Use URL instead of title
spectranaut 78a91b2
Linux: Add support to run multiple tests
spectranaut c031240
Fix test
spectranaut 8b315ec
Update API name to include 'platform'
spectranaut d1cec03
Merge pull request #12 from Igalia/api-name
alice 6417c83
Updates to AXAPI implementation: Do not throw error in init, and poll…
spectranaut 217aeb9
remove accidental file
spectranaut 17470fe
Activate accessibility at the point where you get the AXUIElement for…
alice File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<!doctype html> | ||
<meta charset=utf-8> | ||
<title>core-aam: role button</title> | ||
<script src="/resources/testharness.js"></script> | ||
<script src="/resources/testharnessreport.js"></script> | ||
<script src="/resources/testdriver.js"></script> | ||
<script src="/resources/testdriver-vendor.js"></script> | ||
<script src="/resources/testdriver-actions.js"></script> | ||
|
||
<body> | ||
<div id=test role=button>click me</div> | ||
|
||
<script> | ||
promise_test(async t => { | ||
const node = await test_driver.get_accessibility_api_node('test'); | ||
|
||
if (node.API == 'atspi') { | ||
assert_equals(node.role, 'push button', 'Atspi role'); | ||
} | ||
else if (node.API == 'axapi') { | ||
assert_equals(node.role, 'AXButton', 'AX API role'); | ||
} | ||
else { | ||
assert_unreached(`Unknown API: ${node.API}`) | ||
} | ||
}, 'role button'); | ||
</script> | ||
</body> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import gi | ||
|
||
gi.require_version("Atspi", "2.0") | ||
from gi.repository import Atspi | ||
import json | ||
import threading | ||
|
||
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) | ||
continue | ||
|
||
for i in range(Atspi.Accessible.get_child_count(node)): | ||
child = Atspi.Accessible.get_child_at_index(node, i) | ||
stack.append(child) | ||
|
||
return None | ||
|
||
|
||
def 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) | ||
|
||
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 (None, None) | ||
|
||
|
||
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?" | ||
) | ||
|
||
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?" | ||
) | ||
|
||
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?" | ||
) | ||
|
||
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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
from ApplicationServices import ( | ||
AXUIElementCopyAttributeNames, | ||
AXUIElementCopyAttributeValue, | ||
AXUIElementCreateApplication, | ||
) | ||
|
||
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_accessibility_api_node(self, dom_id): | ||
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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
tools/wptrunner/wptrunner/executors/executorplatformaccessibility.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
from .protocol import ProtocolPart | ||
|
||
from abc import ABCMeta | ||
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 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 | ||
if linux: | ||
self.impl = AtspiExecutorImpl() | ||
self.impl.setup(self.product_name) | ||
if mac: | ||
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) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe there's an open question (still @cookiecrook?) with the WebDriver accessibility APIs about whether to have a way to get this kind of bag of properties, or to instead behave more like the rest of the WebDriver APIs and only support fetching single properties.
We should probably consider for ourselves which will work better for us as well - getting the bag of properties might be slow if any of the properties are slow to compute or cause exceptions, etc.; however, accessing individual properties instead would probably require a kind of "stringly typed" (i.e. passing a string representing the property to query) API rather than having every single property we might want to test on each operating system as its own method.
Still, it might be worth prototyping what it might look like to access single properties as well as this bag of properties API?