Skip to content

Commit

Permalink
Add getAXNodeAndAncestors CDP command
Browse files Browse the repository at this point in the history
This command takes a DOM node reference and retrieves the corresponding
AXNode and its ancestors up to the root. We use this command in the
The DevTools frontend lazily builds the accessibility tree. We use this
command to map DOM nodes to AXNodes that lives in parts of the tree that
has not been previously requested. Using the ancestor chain up to the
root we can then stitch the returned subtree together with the already
known part.

Bug: 1226486
Change-Id: I374765908358a88aaf2d21bc17acf814207dfc9b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3253351
Commit-Queue: Johan Bay <jobay@chromium.org>
Reviewed-by: Alex Rudenko <alexrudenko@chromium.org>
Reviewed-by: Nektarios Paisios <nektar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#939724}
  • Loading branch information
johanbay authored and Chromium LUCI CQ committed Nov 9, 2021
1 parent ed0e6f1 commit cf39dcf
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 0 deletions.
13 changes: 13 additions & 0 deletions third_party/blink/public/devtools_protocol/browser_protocol.pdl
Expand Up @@ -232,6 +232,19 @@ experimental domain Accessibility
returns
AXNode node

# Fetches a node and all ancestors up to and including the root.
# Requires `enable()` to have been called previously.
experimental command getAXNodeAndAncestors
parameters
# Identifier of the node to get.
optional DOM.NodeId nodeId
# Identifier of the backend node to get.
optional DOM.BackendNodeId backendNodeId
# JavaScript object id of the node wrapper to get.
optional Runtime.RemoteObjectId objectId
returns
array of AXNode nodes

# Fetches a particular accessibility node by AXNodeId.
# Requires `enable()` to have been called previously.
experimental command getChildAXNodes
Expand Down
Expand Up @@ -837,6 +837,56 @@ Response InspectorAccessibilityAgent::getRootAXNode(
return Response::Success();
}

protocol::Response InspectorAccessibilityAgent::getAXNodeAndAncestors(
Maybe<int> dom_node_id,
Maybe<int> backend_node_id,
Maybe<String> object_id,
std::unique_ptr<protocol::Array<protocol::Accessibility::AXNode>>*
out_nodes) {
if (!enabled_.Get())
return Response::ServerError("Accessibility has not been enabled.");

Node* dom_node = nullptr;
Response response =
dom_agent_->AssertNode(dom_node_id, backend_node_id, object_id, dom_node);
if (!response.IsSuccess())
return response;

Document& document = dom_node->GetDocument();
document.UpdateStyleAndLayout(DocumentUpdateReason::kInspector);
DocumentLifecycle::DisallowTransitionScope disallow_transition(
document.Lifecycle());
LocalFrame* local_frame = document.GetFrame();
if (!local_frame)
return Response::ServerError("Frame is detached.");

RetainAXContextForDocument(&document);

AXContext ax_context(document, ui::kAXModeComplete);
auto& cache = To<AXObjectCacheImpl>(ax_context.GetAXObjectCache());

AXObject* ax_object = cache.GetOrCreate(dom_node);

*out_nodes =
std::make_unique<protocol::Array<protocol::Accessibility::AXNode>>();

if (!ax_object) {
(*out_nodes)
->emplace_back(BuildProtocolAXNodeForDOMNodeWithNoAXNode(
IdentifiersFactory::IntIdForNode(dom_node)));
return Response::Success();
}

do {
std::unique_ptr<AXNode> ancestor =
BuildProtocolAXNodeForAXObject(*ax_object);
(*out_nodes)->emplace_back(std::move(ancestor));
ax_object = ax_object->ParentObjectIncludedInTree();
} while (ax_object);

return Response::Success();
}

protocol::Response InspectorAccessibilityAgent::getChildAXNodes(
const String& in_id,
Maybe<String> frame_id,
Expand Down
Expand Up @@ -56,6 +56,12 @@ class MODULES_EXPORT InspectorAccessibilityAgent
protocol::Response getRootAXNode(
protocol::Maybe<String> frame_id,
std::unique_ptr<protocol::Accessibility::AXNode>* node) override;
protocol::Response getAXNodeAndAncestors(
protocol::Maybe<int> dom_node_id,
protocol::Maybe<int> backend_node_id,
protocol::Maybe<String> object_id,
std::unique_ptr<protocol::Array<protocol::Accessibility::AXNode>>*
out_nodes) override;
protocol::Response getChildAXNodes(
const String& in_id,
protocol::Maybe<String> frame_id,
Expand Down
@@ -0,0 +1,176 @@
Tests Accessibility.getAXNodeAndAncestors
{
backendDOMNodeId : <number>
childIds : <object>
ignored : false
name : {
sources : [
[0] : {
attribute : aria-labelledby
type : relatedElement
}
[1] : {
attribute : aria-label
type : attribute
}
[2] : {
attribute : title
type : attribute
}
]
type : computedString
value :
}
nodeId : <string>
parentId : <string>
properties : [
]
role : {
type : role
value : paragraph
}
}
{
backendDOMNodeId : <number>
childIds : <object>
ignored : false
name : {
sources : [
[0] : {
attribute : aria-labelledby
type : relatedElement
}
[1] : {
attribute : aria-label
type : attribute
}
[2] : {
attribute : title
type : attribute
}
]
type : computedString
value :
}
nodeId : <string>
parentId : <string>
properties : [
]
role : {
type : role
value : article
}
}
{
backendDOMNodeId : <number>
childIds : <object>
ignored : false
name : {
sources : [
[0] : {
attribute : aria-labelledby
type : relatedElement
}
[1] : {
attribute : aria-label
type : attribute
}
[2] : {
attribute : title
type : attribute
}
]
type : computedString
value :
}
nodeId : <string>
parentId : <string>
properties : [
]
role : {
type : role
value : main
}
}
{
backendDOMNodeId : <number>
childIds : <object>
ignored : true
ignoredReasons : [
[0] : {
name : uninteresting
value : {
type : boolean
value : true
}
}
]
nodeId : <string>
parentId : <string>
role : {
type : role
value : none
}
}
{
backendDOMNodeId : <number>
childIds : <object>
ignored : true
ignoredReasons : [
]
nodeId : <string>
parentId : <string>
role : {
type : role
value : none
}
}
{
backendDOMNodeId : <number>
childIds : <object>
frameId : <string>
ignored : false
name : {
sources : [
[0] : {
attribute : aria-labelledby
type : relatedElement
}
[1] : {
attribute : aria-label
type : attribute
}
[2] : {
attribute : aria-label
superseded : true
type : attribute
}
[3] : {
nativeSource : title
type : relatedElement
}
[4] : {
attribute : title
superseded : true
type : attribute
}
]
type : computedString
value :
}
nodeId : <string>
properties : [
[0] : {
name : focusable
value : {
type : booleanOrUndefined
value : true
}
}
]
role : {
type : internalRole
value : RootWebArea
}
}

@@ -0,0 +1,28 @@
(async function(testRunner) {
var {page, session, dp} = await testRunner.startHTML(`
<main>
<article>
<h1>Article</h1>
<p>First paragraph</p>
</article>
</main>
`, 'Tests Accessibility.getAXNodeAndAncestors');
await dp.Accessibility.enable();

function logNode(axnode) {
testRunner.log(axnode, null, ['nodeId', 'backendDOMNodeId', 'childIds', 'frameId', 'parentId']);
}

const documentResp = await dp.DOM.getDocument();
const documentId = documentResp.result.root.nodeId;
const paragraphResp =
await dp.DOM.querySelector({nodeId: documentId, selector: 'p'});
const paragraphId = paragraphResp.result.nodeId;

let {result} = await dp.Accessibility.getAXNodeAndAncestors({nodeId: paragraphId});
for (const node of result.nodes) {
logNode(node);
}

testRunner.completeTest();
});

0 comments on commit cf39dcf

Please sign in to comment.