diff --git a/third_party/blink/public/devtools_protocol/browser_protocol.pdl b/third_party/blink/public/devtools_protocol/browser_protocol.pdl index 7de309308d7926..1800949a4b617b 100644 --- a/third_party/blink/public/devtools_protocol/browser_protocol.pdl +++ b/third_party/blink/public/devtools_protocol/browser_protocol.pdl @@ -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 diff --git a/third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.cc b/third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.cc index 15f3abc7d46fcc..f3358f5a7fc626 100644 --- a/third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.cc +++ b/third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.cc @@ -839,6 +839,81 @@ void InspectorAccessibilityAgent::AddChildren( } } +namespace { + +void setNameAndRole(const AXObject& ax_object, std::unique_ptr& 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 name = + CreateValue(computed_name, AXValueTypeEnum::ComputedString); + node->setName(std::move(name)); +} + +} // namespace + +Response InspectorAccessibilityAgent::queryAXTree( + Maybe dom_node_id, + Maybe backend_node_id, + Maybe object_id, + Maybe accessible_name, + Maybe role, + std::unique_ptr>* 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>(); + auto& cache = To(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> 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 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(); diff --git a/third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.h b/third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.h index 9a09e8770e55c2..ab973dad4567c6 100644 --- a/third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.h +++ b/third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.h @@ -47,6 +47,14 @@ class MODULES_EXPORT InspectorAccessibilityAgent protocol::Response getFullAXTree( std::unique_ptr>*) override; + protocol::Response queryAXTree( + protocol::Maybe dom_node_id, + protocol::Maybe backend_node_id, + protocol::Maybe object_id, + protocol::Maybe accessibleName, + protocol::Maybe role, + std::unique_ptr>*) + override; private: // Unconditionally enables the agent, even if |enabled_.Get()==true|. diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/query-axtree-oopif-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/query-axtree-oopif-expected.txt new file mode 100644 index 00000000000000..4adf0cb64e3e2e --- /dev/null +++ b/third_party/blink/web_tests/http/tests/inspector-protocol/query-axtree-oopif-expected.txt @@ -0,0 +1,5 @@ +Tests that we cannot find elements inside OOPIF by accessible name + +Running test: testGetIdsForSubtreeByAccessibleNameOOPIF +node1 + diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/query-axtree-oopif.js b/third_party/blink/web_tests/http/tests/inspector-protocol/query-axtree-oopif.js new file mode 100644 index 00000000000000..4ad6cc47440b5f --- /dev/null +++ b/third_party/blink/web_tests/http/tests/inspector-protocol/query-axtree-oopif.js @@ -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( + ` +

accessible name

+ `, + '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]); +}); diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/resources/iframe-accessible-name.html b/third_party/blink/web_tests/http/tests/inspector-protocol/resources/iframe-accessible-name.html new file mode 100644 index 00000000000000..0474b74068107d --- /dev/null +++ b/third_party/blink/web_tests/http/tests/inspector-protocol/resources/iframe-accessible-name.html @@ -0,0 +1,3 @@ + +Iframe with accessible name +

accessible name

diff --git a/third_party/blink/web_tests/inspector-protocol/accessibility/accessibility-query-axtree-errors-expected.txt b/third_party/blink/web_tests/inspector-protocol/accessibility/accessibility-query-axtree-errors-expected.txt new file mode 100644 index 00000000000000..c97aa192ba7d8d --- /dev/null +++ b/third_party/blink/web_tests/inspector-protocol/accessibility/accessibility-query-axtree-errors-expected.txt @@ -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 : + sessionId : +} +{ + error : { + code : -32000 + message : Could not find node with given id + } + id : + sessionId : +} +{ + error : { + code : -32000 + message : No node found for given backend id + } + id : + sessionId : +} +{ + error : { + code : -32000 + message : Invalid remote object id + } + id : + sessionId : +} + diff --git a/third_party/blink/web_tests/inspector-protocol/accessibility/accessibility-query-axtree-errors.js b/third_party/blink/web_tests/inspector-protocol/accessibility/accessibility-query-axtree-errors.js new file mode 100644 index 00000000000000..319841798a84ef --- /dev/null +++ b/third_party/blink/web_tests/inspector-protocol/accessibility/accessibility-query-axtree-errors.js @@ -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(); +}); diff --git a/third_party/blink/web_tests/inspector-protocol/accessibility/accessibility-query-axtree-expected.txt b/third_party/blink/web_tests/inspector-protocol/accessibility/accessibility-query-axtree-expected.txt new file mode 100644 index 00000000000000..f86438e38c4d11 --- /dev/null +++ b/third_party/blink/web_tests/inspector-protocol/accessibility/accessibility-query-axtree-expected.txt @@ -0,0 +1,223 @@ +Test finding DOM nodes by accessible name + +Running test: dumpAXNodes +dump both an ignored and an unignored axnode +{ + backendDOMNodeId : + 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 : + properties : [ + [0] : { + name : level + value : { + type : integer + value : 2 + } + } + ] + role : { + type : role + value : heading + } +} +{ + backendDOMNodeId : + ignored : false + name : { + sources : [ + [0] : { + type : contents + value : { + type : computedString + value : title + } + } + ] + type : computedString + value : title + } + nodeId : + properties : [ + ] + role : { + type : role + value : text + } +} +{ + backendDOMNodeId : + ignored : false + nodeId : + properties : [ + ] + role : { + type : internalRole + value : InlineTextBox + } +} +{ + backendDOMNodeId : + ignored : true + ignoredReasons : [ + [0] : { + name : ariaHiddenElement + value : { + type : boolean + value : true + } + } + ] + name : { + type : computedString + value : title + } + nodeId : + role : { + type : role + value : heading + } +} +{ + backendDOMNodeId : + ignored : true + ignoredReasons : [ + [0] : { + name : ariaHiddenSubtree + value : { + relatedNodes : [ + [0] : { + backendDOMNodeId : + idref : hidden + } + ] + type : idref + } + } + ] + name : { + type : computedString + value : title + } + nodeId : + role : { + type : role + value : text + } +} +{ + backendDOMNodeId : + ignored : true + ignoredReasons : [ + [0] : { + name : notRendered + value : { + type : boolean + value : true + } + } + ] + name : { + type : computedString + value : title + } + nodeId : + 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 + diff --git a/third_party/blink/web_tests/inspector-protocol/accessibility/accessibility-query-axtree.js b/third_party/blink/web_tests/inspector-protocol/accessibility/accessibility-query-axtree.js new file mode 100644 index 00000000000000..33bfe18839333c --- /dev/null +++ b/third_party/blink/web_tests/inspector-protocol/accessibility/accessibility-query-axtree.js @@ -0,0 +1,212 @@ +// 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( + ` +

title

+ + + +
+
+ +
+
+
+ +
+
+ + +

text content

+ +

text content

+ + + + Accessible Name + + + + +
+ +
+
+
item1
+
item2
+
+
item3
+
+ +
+ `, + '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, + ]); +});