Skip to content

Commit 5fbd9d7

Browse files
committed
Extend testdriver to add accessibility API testing
1 parent 6198cae commit 5fbd9d7

File tree

14 files changed

+186
-8
lines changed

14 files changed

+186
-8
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!doctype html>
2+
<meta charset=utf-8>
3+
<title>core-aam: acacia test using testdriver</title>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<script src="/resources/testdriver.js"></script>
7+
<script src="/resources/testdriver-vendor.js"></script>
8+
<script src="/resources/testdriver-actions.js"></script>
9+
10+
<body>
11+
<div id=testtest role="button"></div>
12+
<script>
13+
promise_test(async t => {
14+
const node = await test_driver.get_accessibility_api_node('testtest');
15+
assert_equals(node.role, 'push button');
16+
}, 'An acacia test');
17+
</script>
18+
</body>

resources/testdriver.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,18 @@
10661066
*/
10671067
clear_device_posture: function(context=null) {
10681068
return window.test_driver_internal.clear_device_posture(context);
1069+
},
1070+
1071+
/**
1072+
* Get a serialized object representing the accessibility API's accessibility node.
1073+
*
1074+
* @param {id} id of element
1075+
* @returns {Promise} Fullfilled with object representing accessibilty node,
1076+
* rejected in the cases of failures.
1077+
*/
1078+
get_accessibility_api_node: async function(dom_id) {
1079+
let jsonresult = await window.test_driver_internal.get_accessibility_api_node(dom_id);
1080+
return JSON.parse(jsonresult);
10691081
}
10701082
};
10711083

@@ -1254,6 +1266,10 @@
12541266

12551267
async clear_device_posture(context=null) {
12561268
throw new Error("clear_device_posture() is not implemented by testdriver-vendor.js");
1269+
},
1270+
1271+
async get_accessibility_api_node(dom_id) {
1272+
throw new Error("not implemented, whoops!");
12571273
}
12581274
};
12591275
})();

tools/webdriver/webdriver/client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,10 @@ def get_computed_label(self):
879879
def get_computed_role(self):
880880
return self.send_element_command("GET", "computedrole")
881881

882+
@command
883+
def get_accessibility_api_node(self):
884+
return self.send_element_command("GET", "accessibilityapinode")
885+
882886
# This MUST come last because otherwise @property decorators above
883887
# will be overridden by this.
884888
@command

tools/wpt/run.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,8 @@ def setup_kwargs(self, kwargs):
519519
# We are on Taskcluster, where our Docker container does not have
520520
# enough capabilities to run Chrome with sandboxing. (gh-20133)
521521
kwargs["binary_args"].append("--no-sandbox")
522-
522+
if kwargs["force_renderer_accessibility"]:
523+
kwargs["binary_args"].append("--force-renderer-accessibility")
523524

524525
class ContentShell(BrowserSetup):
525526
name = "content_shell"

tools/wptrunner/wptrunner/browsers/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,8 @@ def executor_browser(self) -> Tuple[Type[ExecutorBrowser], Mapping[str, Any]]:
430430
"host": self.host,
431431
"port": self.port,
432432
"pac": self.pac,
433-
"env": self.env}
433+
"env": self.env,
434+
"pid": self.pid}
434435

435436
def settings(self, test: Test) -> BrowserSettings:
436437
self._pac = test.environment.get("pac", None) if self._supports_pac else None

tools/wptrunner/wptrunner/executors/actions.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,18 @@ def __init__(self, logger, protocol):
464464
def __call__(self, payload):
465465
return self.protocol.device_posture.clear_device_posture()
466466

467+
class GetAccessibilityAPINodeAction:
468+
name = "get_accessibility_api_node"
469+
470+
def __init__(self, logger, protocol):
471+
self.logger = logger
472+
self.protocol = protocol
473+
474+
def __call__(self, payload):
475+
dom_id = payload["dom_id"]
476+
return self.protocol.platform_accessibility.get_accessibility_api_node(dom_id)
477+
478+
467479
actions = [ClickAction,
468480
DeleteAllCookiesAction,
469481
GetAllCookiesAction,
@@ -499,4 +511,5 @@ def __call__(self, payload):
499511
RemoveVirtualSensorAction,
500512
GetVirtualSensorInformationAction,
501513
SetDevicePostureAction,
502-
ClearDevicePostureAction]
514+
ClearDevicePostureAction,
515+
GetAccessibilityAPINodeAction]
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import acacia_atspi
2+
import json
3+
from .protocol import (PlatformAccessibilityProtocolPart)
4+
5+
# When running against chrome family browser:
6+
# self.parent is WebDriverProtocol
7+
# self.parent.webdriver is webdriver
8+
9+
def findActiveTab(root):
10+
stack = [root]
11+
while stack:
12+
node = stack.pop()
13+
14+
if node.getRoleName() == 'frame':
15+
relations = node.getRelations()
16+
if 'ATSPI_RELATION_EMBEDS' in relations:
17+
index = relations.index('ATSPI_RELATION_EMBEDS')
18+
target = node.getTargetForRelationAtIndex(index)
19+
print(target.getRoleName())
20+
print(target.getName())
21+
return target
22+
continue
23+
24+
for i in range(node.getChildCount()):
25+
child = node.getChildAtIndex(i)
26+
stack.append(child)
27+
28+
return None
29+
30+
def serialize_node(node):
31+
node_dictionary = {}
32+
node_dictionary['role'] = node.getRoleName()
33+
node_dictionary['name'] = node.getName()
34+
node_dictionary['description'] = node.getDescription()
35+
node_dictionary['states'] = sorted(node.getStates())
36+
node_dictionary['interfaces'] = sorted(node.getInterfaces())
37+
node_dictionary['attributes'] = sorted(node.getAttributes())
38+
39+
# TODO: serialize other attributes
40+
41+
return node_dictionary
42+
43+
def find_node(root, dom_id):
44+
stack = [root]
45+
while stack:
46+
node = stack.pop()
47+
48+
attributes = node.getAttributes()
49+
for attribute_pair in attributes:
50+
[attribute, value] = attribute_pair.split(':', 1)
51+
if attribute == 'id':
52+
if value == dom_id:
53+
return node
54+
55+
for i in range(node.getChildCount()):
56+
child = node.getChildAtIndex(i)
57+
stack.append(child)
58+
59+
return None
60+
61+
class AcaciaPlatformAccessibilityProtocolPart(PlatformAccessibilityProtocolPart):
62+
def setup(self):
63+
self.product_name = self.parent.product_name
64+
self.root = None
65+
self.errormsg = None
66+
67+
self.root = acacia_atspi.findRootAtspiNodeForName(self.product_name);
68+
if self.root.isNull():
69+
error = f"Cannot find root accessibility node for {self.product_name} - did you turn on accessibility?"
70+
print(error)
71+
self.errormsg = error
72+
73+
74+
def get_accessibility_api_node(self, dom_id):
75+
if self.root.isNull():
76+
return json.dumps({"role": self.errormsg})
77+
78+
active_tab = findActiveTab(self.root)
79+
80+
# This will fail sometimes when accessibilty is off.
81+
if not active_tab or active_tab.isNull():
82+
return json.dumps({"role": "couldn't find active tab"})
83+
84+
# This fails sometimes for unknown reasons.
85+
node = find_node(active_tab, dom_id)
86+
if not node or node.isNull():
87+
return json.dumps({"role": "couldn't find the node with that ID"})
88+
89+
return json.dumps(serialize_node(node))
90+
91+

tools/wptrunner/wptrunner/executors/executormarionette.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
WdspecExecutor,
2525
get_pages,
2626
strip_server)
27+
2728
from .protocol import (AccessibilityProtocolPart,
2829
ActionSequenceProtocolPart,
2930
AssertsProtocolPart,
@@ -48,6 +49,7 @@
4849
DevicePostureProtocolPart,
4950
merge_dicts)
5051

52+
from .executoracacia import (AcaciaPlatformAccessibilityProtocolPart)
5153

5254
def do_delayed_imports():
5355
global errors, marionette, Addons, WebAuthn
@@ -782,12 +784,14 @@ class MarionetteProtocol(Protocol):
782784
MarionetteDebugProtocolPart,
783785
MarionetteAccessibilityProtocolPart,
784786
MarionetteVirtualSensorProtocolPart,
785-
MarionetteDevicePostureProtocolPart]
787+
MarionetteDevicePostureProtocolPart,
788+
AcaciaPlatformAccessibilityProtocolPart]
786789

787790
def __init__(self, executor, browser, capabilities=None, timeout_multiplier=1, e10s=True, ccov=False):
788791
do_delayed_imports()
789792

790793
super().__init__(executor, browser)
794+
self.product_name = browser.product_name
791795
self.marionette = None
792796
self.marionette_port = browser.marionette_port
793797
self.capabilities = capabilities

tools/wptrunner/wptrunner/executors/executorwebdriver.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
DevicePostureProtocolPart,
3939
merge_dicts)
4040

41+
from .executoracacia import (AcaciaPlatformAccessibilityProtocolPart)
42+
4143
from webdriver.client import Session
4244
from webdriver import error
4345

@@ -462,10 +464,12 @@ class WebDriverProtocol(Protocol):
462464
WebDriverFedCMProtocolPart,
463465
WebDriverDebugProtocolPart,
464466
WebDriverVirtualSensorPart,
465-
WebDriverDevicePostureProtocolPart]
467+
WebDriverDevicePostureProtocolPart,
468+
AcaciaPlatformAccessibilityProtocolPart]
466469

467470
def __init__(self, executor, browser, capabilities, **kwargs):
468471
super().__init__(executor, browser)
472+
self.product_name = browser.product_name
469473
self.capabilities = capabilities
470474
if hasattr(browser, "capabilities"):
471475
if self.capabilities is None:

tools/wptrunner/wptrunner/executors/protocol.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,20 @@ def get_computed_role(self, element):
321321
pass
322322

323323

324+
class PlatformAccessibilityProtocolPart(ProtocolPart):
325+
"""Protocol part for platform accessibility introspection"""
326+
__metaclass__ = ABCMeta
327+
328+
name = "platform_accessibility"
329+
330+
@abstractmethod
331+
def get_accessibility_api_node(self, dom_id):
332+
"""Return the the platform accessibilty object.
333+
334+
:param id: DOM ID."""
335+
pass
336+
337+
324338
class CookiesProtocolPart(ProtocolPart):
325339
"""Protocol part for managing cookies"""
326340
__metaclass__ = ABCMeta

0 commit comments

Comments
 (0)