Skip to content

Commit

Permalink
Add queryAXTree CDP command
Browse files Browse the repository at this point in the history
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: Sigurd Schneider <sigurds@chromium.org>
Reviewed-by: Mathias Bynens <mathias@chromium.org>
Reviewed-by: Chris Hall <chrishall@chromium.org>
Reviewed-by: Alice Boxhall <aboxhall@chromium.org>
Reviewed-by: Alex Rudenko <alexrudenko@chromium.org>
Reviewed-by: Andrey Kosyakov <caseq@chromium.org>
Cr-Commit-Position: refs/heads/master@{#809251}
  • Loading branch information
johanbay authored and Commit Bot committed Sep 22, 2020
1 parent 3a91483 commit d557b1c
Show file tree
Hide file tree
Showing 10 changed files with 663 additions and 0 deletions.
22 changes: 22 additions & 0 deletions third_party/blink/public/devtools_protocol/browser_protocol.pdl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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|.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Tests that we cannot find elements inside OOPIF by accessible name

Running test: testGetIdsForSubtreeByAccessibleNameOOPIF
node1

Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!DOCTYPE html>
<title>Iframe with accessible name</title>
<h1 id="node2">accessible name</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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>
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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();
});

0 comments on commit d557b1c

Please sign in to comment.