feat: root option in page.accessibility.snapshot() (#4318)

Going from `AXNode` -> `ElementHandle` is turning out to be controversial.

This patch instead adds a way to go from `ElementHandle` -> `AXNode`. If the API looks good, I'll add it into Firefox as well.

References #3641
This commit is contained in:
Joel Einbinder 2019-05-10 02:39:42 -04:00 committed by Andrey Lushnikov
parent b3027a6e16
commit a3cb16308c
3 changed files with 95 additions and 6 deletions

View File

@ -2098,6 +2098,7 @@ Most of the accessibility tree gets filtered out when converting from Blink AX T
#### accessibility.snapshot([options])
- `options` <[Object]>
- `interestingOnly` <[boolean]> Prune uninteresting nodes from the tree. Defaults to `true`.
- `root` <[ElementHandle]> The root DOM element for the snapshot. Defaults to the whole page.
- returns: <[Promise]<[Object]>> An [AXNode] object with the following properties:
- `role` <[string]> The [role](https://www.w3.org/TR/wai-aria/#usage_intro).
- `name` <[string]> A human readable name for the node.

View File

@ -60,20 +60,36 @@ class Accessibility {
}
/**
* @param {{interestingOnly?: boolean}=} options
* @param {{interestingOnly?: boolean, root?: ?Puppeteer.ElementHandle}=} options
* @return {!Promise<!SerializedAXNode>}
*/
async snapshot(options = {}) {
const {interestingOnly = true} = options;
const {
interestingOnly = true,
root = null,
} = options;
const {nodes} = await this._client.send('Accessibility.getFullAXTree');
const root = AXNode.createTree(nodes);
let backendNodeId = null;
if (root) {
const {node} = await this._client.send('DOM.describeNode', {objectId: root._remoteObject.objectId});
backendNodeId = node.backendNodeId;
}
const defaultRoot = AXNode.createTree(nodes);
let needle = defaultRoot;
if (backendNodeId) {
needle = defaultRoot.find(node => node._payload.backendDOMNodeId === backendNodeId);
if (!needle)
return null;
}
if (!interestingOnly)
return serializeTree(root)[0];
return serializeTree(needle)[0];
/** @type {!Set<!AXNode>} */
const interestingNodes = new Set();
collectInterestingNodes(interestingNodes, root, false);
return serializeTree(root, interestingNodes)[0];
collectInterestingNodes(interestingNodes, defaultRoot, false);
if (!interestingNodes.has(needle))
return null;
return serializeTree(needle, interestingNodes)[0];
}
}
@ -179,6 +195,21 @@ class AXNode {
return this._cachedHasFocusableChild;
}
/**
* @param {function(AXNode):boolean} predicate
* @return {?AXNode}
*/
find(predicate) {
if (predicate(this))
return this;
for (const child of this._children) {
const result = child.find(predicate);
if (result)
return result;
}
return null;
}
/**
* @return {boolean}
*/

View File

@ -307,6 +307,63 @@ module.exports.addTests = function({testRunner, expect, FFOX}) {
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0]).toEqual(golden);
});
describe_fails_ffox('root option', function() {
it('should work a button', async({page}) => {
await page.setContent(`<button>My Button</button>`);
const button = await page.$('button');
expect(await page.accessibility.snapshot({root: button})).toEqual({
role: 'button',
name: 'My Button'
});
});
it('should work an input', async({page}) => {
await page.setContent(`<input title="My Input" value="My Value">`);
const input = await page.$('input');
expect(await page.accessibility.snapshot({root: input})).toEqual({
role: 'textbox',
name: 'My Input',
value: 'My Value'
});
});
it('should work a menu', async({page}) => {
await page.setContent(`
<div role="menu" title="My Menu">
<div role="menuitem">First Item</div>
<div role="menuitem">Second Item</div>
<div role="menuitem">Third Item</div>
</div>
`);
const menu = await page.$('div[role="menu"]');
expect(await page.accessibility.snapshot({root: menu})).toEqual({
role: 'menu',
name: 'My Menu',
children:
[ { role: 'menuitem', name: 'First Item' },
{ role: 'menuitem', name: 'Second Item' },
{ role: 'menuitem', name: 'Third Item' } ]
});
});
it('should return null when the element is no longer in DOM', async({page}) => {
await page.setContent(`<button>My Button</button>`);
const button = await page.$('button');
await page.$eval('button', button => button.remove());
expect(await page.accessibility.snapshot({root: button})).toEqual(null);
});
it('should support the interestingOnly option', async({page}) => {
await page.setContent(`<div><button>My Button</button></div>`);
const div = await page.$('div');
expect(await page.accessibility.snapshot({root: div})).toEqual(null);
expect(await page.accessibility.snapshot({root: div, interestingOnly: false})).toEqual({
role: 'GenericContainer',
name: '',
children: [ { role: 'button', name: 'My Button' } ] }
);
});
});
});
function findFocusedNode(node) {
if (node.focused)