Commit d557b1cd authored by Johan Bay's avatar Johan Bay Committed by Commit Bot

Add queryAXTree CDP command

This command takes a DOM node and a name and/or role to search for.
It searches the accessibility subtree of the given node for the name
and role. The command also includes ignored nodes in its result.

The command is the underlying primitive for a new aria-based
querying mechanism in Puppeteer.
This CL is a follow-up to crrev.com/c/2366896. The feedback given there
has been addressed in this CL.

Doc: https://docs.google.com/document/d/1-BUEUgqAZlh26fv9oLfy1QRr0MF3ifnxGh7-NDucg9s/
Change-Id: Ibdcdb230b5d1287ab009f2dd9d25a53932072d6b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2398547
Commit-Queue: Johan Bay <jobay@google.com>
Reviewed-by: default avatarSigurd Schneider <sigurds@chromium.org>
Reviewed-by: default avatarMathias Bynens <mathias@chromium.org>
Reviewed-by: default avatarChris Hall <chrishall@chromium.org>
Reviewed-by: default avatarAlice Boxhall <aboxhall@chromium.org>
Reviewed-by: default avatarAlex Rudenko <alexrudenko@chromium.org>
Reviewed-by: default avatarAndrey Kosyakov <caseq@chromium.org>
Cr-Commit-Position: refs/heads/master@{#809251}
parent 3a91483d
......@@ -207,6 +207,28 @@ experimental domain Accessibility
returns
array of AXNode nodes
# Query a DOM node's accessibility subtree for accessible name and role.
# This command computes the name and role for all nodes in the subtree, including those that are
# ignored for accessibility, and returns those that mactch the specified name and role. If no DOM
# node is specified, or the DOM node does not exist, the command returns an error. If neither
# `accessibleName` or `role` is specified, it returns all the accessibility nodes in the subtree.
experimental command queryAXTree
parameters
# Identifier of the node for the root to query.
optional DOM.NodeId nodeId
# Identifier of the backend node for the root to query.
optional DOM.BackendNodeId backendNodeId
# JavaScript object id of the node wrapper for the root to query.
optional Runtime.RemoteObjectId objectId
# Find nodes with this computed name.
optional string accessibleName
# Find nodes with this computed role.
optional string role
returns
# A list of `Accessibility.AXNode` matching the specified attributes,
# including nodes that are ignored for accessibility.
array of AXNode nodes
experimental domain Animation
depends on Runtime
depends on DOM
......
......@@ -839,6 +839,81 @@ void InspectorAccessibilityAgent::AddChildren(
}
}
namespace {
void setNameAndRole(const AXObject& ax_object, std::unique_ptr<AXNode>& node) {
ax::mojom::blink::Role role = ax_object.RoleValue();
node->setRole(CreateRoleNameValue(role));
AXObject::NameSources name_sources;
String computed_name = ax_object.GetName(&name_sources);
std::unique_ptr<AXValue> name =
CreateValue(computed_name, AXValueTypeEnum::ComputedString);
node->setName(std::move(name));
}
} // namespace
Response InspectorAccessibilityAgent::queryAXTree(
Maybe<int> dom_node_id,
Maybe<int> backend_node_id,
Maybe<String> object_id,
Maybe<String> accessible_name,
Maybe<String> role,
std::unique_ptr<protocol::Array<AXNode>>* nodes) {
Node* root_dom_node = nullptr;
Response response = dom_agent_->AssertNode(dom_node_id, backend_node_id,
object_id, root_dom_node);
if (!response.IsSuccess())
return response;
Document& document = root_dom_node->GetDocument();
document.UpdateStyleAndLayout(DocumentUpdateReason::kInspector);
DocumentLifecycle::DisallowTransitionScope disallow_transition(
document.Lifecycle());
AXContext ax_context(document);
*nodes = std::make_unique<protocol::Array<protocol::Accessibility::AXNode>>();
auto& cache = To<AXObjectCacheImpl>(ax_context.GetAXObjectCache());
AXObject* root_ax_node = cache.GetOrCreate(root_dom_node);
auto sought_role = ax::mojom::blink::Role::kUnknown;
if (role.isJust())
sought_role = AXObject::AriaRoleToWebCoreRole(role.fromJust());
const String sought_name = accessible_name.fromMaybe("");
HeapVector<Member<AXObject>> reachable;
reachable.push_back(root_ax_node);
while (!reachable.IsEmpty()) {
AXObject* ax_object = reachable.back();
reachable.pop_back();
const AXObject::AXObjectVector& children =
ax_object->ChildrenIncludingIgnored();
reachable.AppendRange(children.rbegin(), children.rend());
// if querying by name: skip if name of current object does not match.
if (accessible_name.isJust() && sought_name != ax_object->ComputedName())
continue;
// if querying by role: skip if role of current object does not match.
if (role.isJust() && sought_role != ax_object->RoleValue())
continue;
// both name and role are OK, so we can add current object to the result.
if (ax_object->AccessibilityIsIgnored()) {
Node* dom_node = ax_object->GetNode();
std::unique_ptr<AXNode> protocol_node =
BuildObjectForIgnoredNode(dom_node, ax_object, false, *nodes, cache);
setNameAndRole(*ax_object, protocol_node);
(*nodes)->push_back(std::move(protocol_node));
} else {
(*nodes)->push_back(
BuildProtocolAXObject(*ax_object, nullptr, false, *nodes, cache));
}
}
return Response::Success();
}
void InspectorAccessibilityAgent::EnableAndReset() {
enabled_.Set(true);
LocalFrame* frame = inspected_frames_->Root();
......
......@@ -47,6 +47,14 @@ class MODULES_EXPORT InspectorAccessibilityAgent
protocol::Response getFullAXTree(
std::unique_ptr<protocol::Array<protocol::Accessibility::AXNode>>*)
override;
protocol::Response queryAXTree(
protocol::Maybe<int> dom_node_id,
protocol::Maybe<int> backend_node_id,
protocol::Maybe<String> object_id,
protocol::Maybe<String> accessibleName,
protocol::Maybe<String> role,
std::unique_ptr<protocol::Array<protocol::Accessibility::AXNode>>*)
override;
private:
// Unconditionally enables the agent, even if |enabled_.Get()==true|.
......
Tests that we cannot find elements inside OOPIF by accessible name
Running test: testGetIdsForSubtreeByAccessibleNameOOPIF
node1
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(async function(testRunner) {
const {dp, session} = await testRunner.startHTML(
`
<h1 id="node1">accessible name</h1>
`,
'Tests that we cannot find elements inside OOPIF by accessible name');
session.evaluate(`
const frame = document.createElement('iframe');
frame.setAttribute('src', 'http://devtools.oopif.test:8000/inspector-protocol/resources/iframe-accessible-name.html');
document.body.appendChild(frame);
`);
const documentResp = await dp.DOM.getDocument();
const documentId = documentResp.result.root.nodeId;
const documentResp2 = await dp.DOM.resolveNode({nodeId: documentId});
const documentObjId = documentResp2.result.object.objectId;
async function testGetIdsForSubtreeByAccessibleNameOOPIF() {
const response = await dp.Accessibility.queryAXTree(
{objectId: documentObjId, accessibleName: 'accessible name'});
await logNodes(response.result.nodes);
}
// copied from third_party/blink/web_tests/inspector-protocol/accessibility/accessibility-query-axtree.js
async function logNodes(axNodes) {
for (const axNode of axNodes) {
const backendNodeId = axNode.backendDOMNodeId;
const response = await dp.DOM.describeNode({backendNodeId});
const node = response.result.node;
// we can only print ids for ELEMENT_NODEs, skip TEXT_NODEs
if (node.nodeType !== Node.ELEMENT_NODE) {
continue;
}
const nodeAttributes = node.attributes;
const idIndex = nodeAttributes.indexOf('id') + 1;
testRunner.log(nodeAttributes[idIndex]);
}
}
testRunner.runTestSuite([testGetIdsForSubtreeByAccessibleNameOOPIF]);
});
<!DOCTYPE html>
<title>Iframe with accessible name</title>
<h1 id="node2">accessible name</h1>
Tests errors when finding DOM nodes by accessible name.
{
error : {
code : -32000
message : Either nodeId, backendNodeId or objectId must be specified
}
id : <number>
sessionId : <string>
}
{
error : {
code : -32000
message : Could not find node with given id
}
id : <number>
sessionId : <string>
}
{
error : {
code : -32000
message : No node found for given backend id
}
id : <number>
sessionId : <string>
}
{
error : {
code : -32000
message : Invalid remote object id
}
id : <number>
sessionId : <string>
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(async function(testRunner) {
const {dp} = await testRunner.startBlank(
'Tests errors when finding DOM nodes by accessible name.');
const wrongObjectId = 'not-a-node';
const wrongNodeId = -1;
// Expected: error because no node is specified
testRunner.log(await dp.Accessibility.queryAXTree({
accessibleName: 'name',
}));
// Expected: error because nodeId is wrong.
testRunner.log(await dp.Accessibility.queryAXTree({
nodeId: wrongNodeId,
accessibleName: 'name',
}));
// Expected: error because backendNodeId is wrong.
testRunner.log(await dp.Accessibility.queryAXTree({
backendNodeId: wrongNodeId,
accessibleName: 'name',
}));
// Expected: error because object ID is wrong.
testRunner.log(await dp.Accessibility.queryAXTree({
objectId: wrongObjectId,
accessibleName: 'name',
}));
testRunner.completeTest();
});
Test finding DOM nodes by accessible name
Running test: dumpAXNodes
dump both an ignored and an unignored axnode
{
backendDOMNodeId : <number>
ignored : false
name : {
sources : [
[0] : {
attribute : aria-labelledby
type : relatedElement
}
[1] : {
attribute : aria-label
type : attribute
}
[2] : {
type : contents
value : {
type : computedString
value : title
}
}
[3] : {
attribute : title
superseded : true
type : attribute
}
]
type : computedString
value : title
}
nodeId : <string>
properties : [
[0] : {
name : level
value : {
type : integer
value : 2
}
}
]
role : {
type : role
value : heading
}
}
{
backendDOMNodeId : <number>
ignored : false
name : {
sources : [
[0] : {
type : contents
value : {
type : computedString
value : title
}
}
]
type : computedString
value : title
}
nodeId : <string>
properties : [
]
role : {
type : role
value : text
}
}
{
backendDOMNodeId : <number>
ignored : false
nodeId : <string>
properties : [
]
role : {
type : internalRole
value : InlineTextBox
}
}
{
backendDOMNodeId : <number>
ignored : true
ignoredReasons : [
[0] : {
name : ariaHiddenElement
value : {
type : boolean
value : true
}
}
]
name : {
type : computedString
value : title
}
nodeId : <string>
role : {
type : role
value : heading
}
}
{
backendDOMNodeId : <number>
ignored : true
ignoredReasons : [
[0] : {
name : ariaHiddenSubtree
value : {
relatedNodes : [
[0] : {
backendDOMNodeId : <number>
idref : hidden
}
]
type : idref
}
}
]
name : {
type : computedString
value : title
}
nodeId : <string>
role : {
type : role
value : text
}
}
{
backendDOMNodeId : <number>
ignored : true
ignoredReasons : [
[0] : {
name : notRendered
value : {
type : boolean
value : true
}
}
]
name : {
type : computedString
value : title
}
nodeId : <string>
role : {
type : role
value : heading
}
}
Running test: testGetNodesForSubtreeByAccessibleName
find all elements with accessible name "foo"
node3
node5
node6
node7
find all elements with accessible name "foo" inside container
node5
node6
node7
find all elements with accessible name "bar"
node1
node2
node8
find all elements with accessible name "text content"
node10
node11
node13
find all elements with accessible name "Accessible Name"
node20
node21
node23
node24
find all elements with accessible name "item1 item2 item3"
node30
Running test: testGetNodesForSubtreeByRole
find all elements with role "button"
node5
node6
node7
node8
node10
node21
find all elements with role "heading"
shown
hidden
unrendered
node11
node13
find all elements with role "treeitem"
node30
node31
node32
node33
node34
find all ignored nodes with role "presentation"
node12
Running test: testGetNodesForSubtreeByAccessibleNameAndRole
find all elements with accessible name "foo" and role "button"
node5
node6
node7
find all elements with accessible name "foo" and role "button" inside container
node5
node6
node7
find all elements with accessible name "text content" and role "heading"
node11
node13
find all elements with accessible name "text content" and role "button"
node10
find all elements with accessible name "Accessible Name" and role "textbox"
node23
find all elements with accessible name "Accessible Name" and role "button"
node21
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(async function(testRunner) {
const {dp} = await testRunner.startHTML(
`
<h2 id="shown">title</h2>
<h2 id="hidden" aria-hidden="true">title</h2>
<h2 id="unrendered" hidden>title</h2>
<div id="node1" aria-labeledby="node2"></div>
<div id="node2" aria-label="bar"></div>
<div id="node3" aria-label="foo" aria-hidden="true"></div>
<div id="node4" class="container">
<div id="node5" role="button" aria-label="foo"></div>
<div id="node6" role="button" aria-label="foo"></div>
<div id="node7" hidden role="button" aria-label="foo"></div>
<div id="node8" role="button" aria-label="bar"></div>
</div>
<button id="node10">text content</button>
<h1 id="node11">text content</h1>
<!-- Accessible name not available when role is "presentation" -->
<h1 id="node12" role="presentation">text content</h1>
<!-- Elements inside shadow dom should be found -->
<script>
const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'open'});
const h1 = document.createElement('h1');
h1.textContent = 'text content';
h1.id = 'node13';
shadowRoot.appendChild(h1);
document.documentElement.appendChild(div);
</script>
<img id="node20" src="" alt="Accessible Name">
<input id="node21" type="submit" value="Accessible Name">
<label id="node22" for="node23">Accessible Name</label>
<input id="node23">
<!-- Accessible name for the <input> is "Accessible Name" -->
<div id="node24" title="Accessible Name"></div>
<div role="treeitem" id="node30">
<div role="treeitem" id="node31">
<div role="treeitem" id="node32">item1</div>
<div role="treeitem" id="node33">item2</div>
</div>
<div role="treeitem" id="node34">item3</div>
</div>
<!-- Accessible name for the following <div> is "item1 item2 item3" -->
<div aria-describedby="node30"></div>
`,
'Test finding DOM nodes by accessible name');
const documentResp = await dp.DOM.getDocument();
const documentId = documentResp.result.root.nodeId;
const containerResp =
await dp.DOM.querySelector({nodeId: documentId, selector: '.container'});
const containerId = containerResp.result.nodeId;
// gymnastics to get remoteObjectIds from nodes
const documentResp2 = await dp.DOM.resolveNode({nodeId: documentId});
const documentObjId = documentResp2.result.object.objectId;
const containerResp2 = await dp.DOM.resolveNode({nodeId: containerId});
const containerObjId = containerResp2.result.object.objectId;
async function dumpAXNodes() {
testRunner.log('dump both an ignored and an unignored axnode');
const response = await dp.Accessibility.queryAXTree(
{objectId: documentObjId, accessibleName: 'title'});
for (const axnode of response.result.nodes) {
testRunner.log(axnode, null, ['nodeId', 'backendDOMNodeId']);
}
}
async function testGetNodesForSubtreeByAccessibleName() {
let response;
testRunner.log('find all elements with accessible name "foo"');
response = await dp.Accessibility.queryAXTree(
{objectId: documentObjId, accessibleName: 'foo'});
await logNodes(response.result.nodes);
testRunner.log(
'find all elements with accessible name "foo" inside container');
response = await dp.Accessibility.queryAXTree(
{objectId: containerObjId, accessibleName: 'foo'});
await logNodes(response.result.nodes);
testRunner.log('find all elements with accessible name "bar"');
response = await dp.Accessibility.queryAXTree(
{objectId: documentObjId, accessibleName: 'bar'});
await logNodes(response.result.nodes);
testRunner.log('find all elements with accessible name "text content"');
response = await dp.Accessibility.queryAXTree(
{objectId: documentObjId, accessibleName: 'text content'});
await logNodes(response.result.nodes);
testRunner.log('find all elements with accessible name "Accessible Name"');
response = await dp.Accessibility.queryAXTree(
{objectId: documentObjId, accessibleName: 'Accessible Name'});
await logNodes(response.result.nodes);
testRunner.log(
'find all elements with accessible name "item1 item2 item3"');
response = await dp.Accessibility.queryAXTree(
{objectId: documentObjId, accessibleName: 'item1 item2 item3'});
await logNodes(response.result.nodes);
}
async function testGetNodesForSubtreeByRole() {
let response;
testRunner.log('find all elements with role "button"');
response = await dp.Accessibility.queryAXTree(
{objectId: documentObjId, role: 'button'});
await logNodes(response.result.nodes);
testRunner.log('find all elements with role "heading"');
response = await dp.Accessibility.queryAXTree(
{objectId: documentObjId, role: 'heading'});
await logNodes(response.result.nodes);
testRunner.log('find all elements with role "treeitem"');
response = await dp.Accessibility.queryAXTree(
{objectId: documentObjId, role: 'treeitem'});
await logNodes(response.result.nodes);
testRunner.log('find all ignored nodes with role "presentation"');
response = await dp.Accessibility.queryAXTree(
{objectId: documentObjId, role: 'presentation'});
await logNodes(response.result.nodes);
}
async function testGetNodesForSubtreeByAccessibleNameAndRole() {
let response;
testRunner.log(
'find all elements with accessible name "foo" and role "button"');
response = await dp.Accessibility.queryAXTree(
{objectId: documentObjId, accessibleName: 'foo', role: 'button'});
await logNodes(response.result.nodes);
testRunner.log(
'find all elements with accessible name "foo" and role "button" inside container');
response = await dp.Accessibility.queryAXTree(
{objectId: containerObjId, accessibleName: 'foo', role: 'button'});
await logNodes(response.result.nodes);
testRunner.log(
'find all elements with accessible name "text content" and role "heading"');
response = await dp.Accessibility.queryAXTree({
objectId: documentObjId,
accessibleName: 'text content',
role: 'heading'
});
await logNodes(response.result.nodes);
testRunner.log(
'find all elements with accessible name "text content" and role "button"');
response = await dp.Accessibility.queryAXTree({
objectId: documentObjId,
accessibleName: 'text content',
role: 'button'
});
await logNodes(response.result.nodes);
testRunner.log(
'find all elements with accessible name "Accessible Name" and role "textbox"');
response = await dp.Accessibility.queryAXTree({
objectId: documentObjId,
accessibleName: 'Accessible Name',
role: 'textbox'
});
await logNodes(response.result.nodes);
testRunner.log(
'find all elements with accessible name "Accessible Name" and role "button"');
response = await dp.Accessibility.queryAXTree({
objectId: documentObjId,
accessibleName: 'Accessible Name',
role: 'button'
});
await logNodes(response.result.nodes);
}
async function logNodes(axNodes) {
for (const axNode of axNodes) {
const backendNodeId = axNode.backendDOMNodeId;
const response = await dp.DOM.describeNode({backendNodeId});
const node = response.result.node;
// we can only print ids for ELEMENT_NODEs, skip TEXT_NODEs
if (node.nodeType !== Node.ELEMENT_NODE) {
continue;
}
const nodeAttributes = node.attributes;
const idIndex = nodeAttributes.indexOf('id') + 1;
testRunner.log(nodeAttributes[idIndex]);
}
}
testRunner.runTestSuite([
dumpAXNodes,
testGetNodesForSubtreeByAccessibleName,
testGetNodesForSubtreeByRole,
testGetNodesForSubtreeByAccessibleNameAndRole,
]);
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment