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:
parent
b3027a6e16
commit
a3cb16308c
@ -2098,6 +2098,7 @@ Most of the accessibility tree gets filtered out when converting from Blink AX T
|
|||||||
#### accessibility.snapshot([options])
|
#### accessibility.snapshot([options])
|
||||||
- `options` <[Object]>
|
- `options` <[Object]>
|
||||||
- `interestingOnly` <[boolean]> Prune uninteresting nodes from the tree. Defaults to `true`.
|
- `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:
|
- returns: <[Promise]<[Object]>> An [AXNode] object with the following properties:
|
||||||
- `role` <[string]> The [role](https://www.w3.org/TR/wai-aria/#usage_intro).
|
- `role` <[string]> The [role](https://www.w3.org/TR/wai-aria/#usage_intro).
|
||||||
- `name` <[string]> A human readable name for the node.
|
- `name` <[string]> A human readable name for the node.
|
||||||
|
@ -60,20 +60,36 @@ class Accessibility {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{interestingOnly?: boolean}=} options
|
* @param {{interestingOnly?: boolean, root?: ?Puppeteer.ElementHandle}=} options
|
||||||
* @return {!Promise<!SerializedAXNode>}
|
* @return {!Promise<!SerializedAXNode>}
|
||||||
*/
|
*/
|
||||||
async snapshot(options = {}) {
|
async snapshot(options = {}) {
|
||||||
const {interestingOnly = true} = options;
|
const {
|
||||||
|
interestingOnly = true,
|
||||||
|
root = null,
|
||||||
|
} = options;
|
||||||
const {nodes} = await this._client.send('Accessibility.getFullAXTree');
|
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)
|
if (!interestingOnly)
|
||||||
return serializeTree(root)[0];
|
return serializeTree(needle)[0];
|
||||||
|
|
||||||
/** @type {!Set<!AXNode>} */
|
/** @type {!Set<!AXNode>} */
|
||||||
const interestingNodes = new Set();
|
const interestingNodes = new Set();
|
||||||
collectInterestingNodes(interestingNodes, root, false);
|
collectInterestingNodes(interestingNodes, defaultRoot, false);
|
||||||
return serializeTree(root, interestingNodes)[0];
|
if (!interestingNodes.has(needle))
|
||||||
|
return null;
|
||||||
|
return serializeTree(needle, interestingNodes)[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,6 +195,21 @@ class AXNode {
|
|||||||
return this._cachedHasFocusableChild;
|
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}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
|
@ -307,6 +307,63 @@ module.exports.addTests = function({testRunner, expect, FFOX}) {
|
|||||||
const snapshot = await page.accessibility.snapshot();
|
const snapshot = await page.accessibility.snapshot();
|
||||||
expect(snapshot.children[0]).toEqual(golden);
|
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) {
|
function findFocusedNode(node) {
|
||||||
if (node.focused)
|
if (node.focused)
|
||||||
|
Loading…
Reference in New Issue
Block a user