feat(firefox&webkit): support root in accessibility.snapshot (#495)

This adds support for `root` in accessibility.snapshot
firefox role names are now normalized to aria roles where they match
webkit roledescriptions are less noisey on mac
webkit mac/linux results are further defined
interestingOnly tests are replaced by one that doesn't rely on undefined behavior
the main accessibility test was split up a bit for more refined testing.
This commit is contained in:
Joel Einbinder 2020-01-14 16:54:50 -08:00 committed by GitHub
parent 92bd854d8f
commit aaa1c9203e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 229 additions and 201 deletions

View File

@ -55,13 +55,12 @@ export interface AXNode {
isLeafNode(): boolean;
isControl(): boolean;
serialize(): SerializedAXNode;
findElement(element: dom.ElementHandle): Promise<AXNode|null>;
children(): Iterable<AXNode>;
}
export class Accessibility {
private _getAXTree: () => Promise<AXNode>;
constructor(getAXTree: () => Promise<AXNode>) {
private _getAXTree: (needle?: dom.ElementHandle) => Promise<{tree: AXNode, needle: AXNode | null}>;
constructor(getAXTree: (needle?: dom.ElementHandle) => Promise<{tree: AXNode, needle: AXNode | null}>) {
this._getAXTree = getAXTree;
}
@ -73,21 +72,18 @@ export class Accessibility {
interestingOnly = true,
root = null,
} = options;
const defaultRoot = await this._getAXTree();
let needle: AXNode | null = defaultRoot;
if (root) {
needle = await defaultRoot.findElement(root);
if (!needle)
return null;
const {tree, needle} = await this._getAXTree(root || undefined);
if (!interestingOnly) {
if (root)
return needle && serializeTree(needle)[0];
return serializeTree(tree)[0];
}
if (!interestingOnly)
return serializeTree(needle)[0];
const interestingNodes: Set<AXNode> = new Set();
collectInterestingNodes(interestingNodes, defaultRoot, false);
if (root && !interestingNodes.has(needle))
collectInterestingNodes(interestingNodes, tree, false);
if (root && (!needle || !interestingNodes.has(needle)))
return null;
return serializeTree(needle, interestingNodes)[0];
return serializeTree(needle || tree, interestingNodes)[0];
}
}

View File

@ -20,9 +20,13 @@ import { Protocol } from './protocol';
import * as dom from '../dom';
import * as accessibility from '../accessibility';
export async function getAccessibilityTree(client: CRSession) : Promise<accessibility.AXNode> {
export async function getAccessibilityTree(client: CRSession, needle?: dom.ElementHandle) : Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> {
const {nodes} = await client.send('Accessibility.getFullAXTree');
return CRAXNode.createTree(client, nodes);
const tree = CRAXNode.createTree(client, nodes);
return {
tree,
needle: needle ? await tree._findElement(needle) : null
};
}
class CRAXNode implements accessibility.AXNode {
@ -90,7 +94,7 @@ class CRAXNode implements accessibility.AXNode {
return this._children;
}
async findElement(element: dom.ElementHandle): Promise<CRAXNode | null> {
async _findElement(element: dom.ElementHandle): Promise<CRAXNode | null> {
const remoteObject = element._remoteObject as Protocol.Runtime.RemoteObject;
const {node: {backendNodeId}} = await this._client.send('DOM.describeNode', {objectId: remoteObject.objectId});
const needle = this.find(node => node._payload.backendDOMNodeId === backendNodeId);

View File

@ -480,8 +480,8 @@ export class CRPage implements PageDelegate {
return to._createHandle(result.object).asElement()!;
}
async getAccessibilityTree(): Promise<accessibility.AXNode> {
return getAccessibilityTree(this._client);
async getAccessibilityTree(needle?: dom.ElementHandle) {
return getAccessibilityTree(this._client, needle);
}
async pdf(options?: types.PDFOptions): Promise<platform.BufferType> {

View File

@ -18,15 +18,48 @@
import * as accessibility from '../accessibility';
import { FFSession } from './ffConnection';
import { Protocol } from './protocol';
import * as dom from '../dom';
export async function getAccessibilityTree(session: FFSession) : Promise<accessibility.AXNode> {
const { tree } = await session.send('Accessibility.getFullAXTree');
return new FFAXNode(tree);
export async function getAccessibilityTree(session: FFSession, needle?: dom.ElementHandle) : Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> {
const objectId = needle ? needle._remoteObject.objectId : undefined;
const { tree } = await session.send('Accessibility.getFullAXTree', { objectId });
const axNode = new FFAXNode(tree);
return {
tree: axNode,
needle: needle ? axNode._findNeedle() : null
};
}
const FFRoleToARIARole = new Map(Object.entries({
'pushbutton': 'button',
'checkbutton': 'checkbox',
'editcombobox': 'combobox',
'content deletion': 'deletion',
'footnote': 'doc-footnote',
'non-native document': 'document',
'grouping': 'group',
'graphic': 'img',
'content insertion': 'insertion',
'animation': 'marquee',
'flat equation': 'math',
'menupopup': 'menu',
'check menu item': 'menuitemcheckbox',
'radio menu item': 'menuitemradio',
'listbox option': 'option',
'radiobutton': 'radio',
'statusbar': 'status',
'pagetab': 'tab',
'pagetablist': 'tablist',
'propertypage': 'tabpanel',
'entry': 'textbox',
'outline': 'tree',
'tree table': 'treegrid',
'outlineitem': 'treeitem',
}));
class FFAXNode implements accessibility.AXNode {
_children: FFAXNode[];
private _payload: any;
private _payload: Protocol.Accessibility.AXTree;
private _editable: boolean;
private _richlyEditable: boolean;
private _focusable: boolean;
@ -77,8 +110,15 @@ class FFAXNode implements accessibility.AXNode {
return this._children;
}
async findElement(): Promise<FFAXNode | null> {
throw new Error('Not implimented');
_findNeedle(): FFAXNode | null {
if (this._payload.foundObject)
return this;
for (const child of this._children) {
const found = child._findNeedle();
if (found)
return found;
}
return null;
}
isLeafNode(): boolean {
@ -160,10 +200,10 @@ class FFAXNode implements accessibility.AXNode {
serialize(): accessibility.SerializedAXNode {
const node: {[x in keyof accessibility.SerializedAXNode]: any} = {
role: this._role,
role: FFRoleToARIARole.get(this._role) || this._role,
name: this._name || ''
};
const userStringProperties: Array<keyof accessibility.SerializedAXNode> = [
const userStringProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [
'name',
'value',
'description',
@ -176,7 +216,7 @@ class FFAXNode implements accessibility.AXNode {
continue;
node[userStringProperty] = this._payload[userStringProperty];
}
const booleanProperties: Array<keyof accessibility.SerializedAXNode> = [
const booleanProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [
'disabled',
'expanded',
'focused',
@ -195,7 +235,7 @@ class FFAXNode implements accessibility.AXNode {
continue;
node[booleanProperty] = value;
}
const tristateProperties: Array<keyof accessibility.SerializedAXNode> = [
const tristateProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [
'checked',
'pressed',
];
@ -205,7 +245,7 @@ class FFAXNode implements accessibility.AXNode {
const value = this._payload[tristateProperty];
node[tristateProperty] = value;
}
const numericalProperties: Array<keyof accessibility.SerializedAXNode> = [
const numericalProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [
'level'
];
for (const numericalProperty of numericalProperties) {
@ -213,7 +253,7 @@ class FFAXNode implements accessibility.AXNode {
continue;
node[numericalProperty] = this._payload[numericalProperty];
}
const tokenProperties: Array<keyof accessibility.SerializedAXNode> = [
const tokenProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Accessibility.AXTree> = [
'autocomplete',
'haspopup',
'invalid',

View File

@ -355,8 +355,8 @@ export class FFPage implements PageDelegate {
return handle;
}
async getAccessibilityTree() : Promise<accessibility.AXNode> {
return getAccessibilityTree(this._session);
async getAccessibilityTree(needle?: dom.ElementHandle) {
return getAccessibilityTree(this._session, needle);
}
coverage(): Coverage | undefined {

View File

@ -68,7 +68,7 @@ export interface PageDelegate {
setInputFiles(handle: dom.ElementHandle<HTMLInputElement>, files: types.FilePayload[]): Promise<void>;
getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null>;
getAccessibilityTree(): Promise<accessibility.AXNode>;
getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}>;
pdf?: (options?: types.PDFOptions) => Promise<platform.BufferType>;
coverage(): Coverage | undefined;
}

View File

@ -16,12 +16,42 @@
import * as accessibility from '../accessibility';
import { WKSession } from './wkConnection';
import { Protocol } from './protocol';
import * as dom from '../dom';
export async function getAccessibilityTree(session: WKSession) {
const {axNode} = await session.send('Page.accessibilitySnapshot');
return new WKAXNode(axNode);
export async function getAccessibilityTree(session: WKSession, needle?: dom.ElementHandle) {
const objectId = needle ? needle._remoteObject.objectId : undefined;
const {axNode} = await session.send('Page.accessibilitySnapshot', { objectId });
const tree = new WKAXNode(axNode);
return {
tree,
needle: needle ? tree._findNeedle() : null
};
}
const WKRoleToARIARole = new Map(Object.entries({
'TextField': 'textbox',
}));
// WebKit localizes role descriptions on mac, but the english versions only add noise.
const WKUnhelpfulRoleDescriptions = new Map(Object.entries({
'WebArea': 'HTML content',
'Summary': 'summary',
'DescriptionList': 'description list',
'ImageMap': 'image map',
'ListMarker': 'list marker',
'Video': 'video playback',
'Mark': 'highlighted',
'contentinfo': 'content information',
'Details': 'details',
'DescriptionListDetail': 'description',
'DescriptionListTerm': 'term',
'alertdialog': 'web alert dialog',
'dialog': 'web dialog',
'status': 'application status',
'tabpanel': 'tab panel',
'application': 'web application',
}));
class WKAXNode implements accessibility.AXNode {
private _payload: Protocol.Page.AXNode;
private _children: WKAXNode[];
@ -38,7 +68,14 @@ class WKAXNode implements accessibility.AXNode {
return this._children;
}
async findElement() {
_findNeedle() : WKAXNode | null {
if (this._payload.found)
return this;
for (const child of this._children) {
const found = child._findNeedle();
if (found)
return found;
}
return null;
}
@ -71,8 +108,26 @@ class WKAXNode implements accessibility.AXNode {
}
}
_isTextControl() : boolean {
switch (this._payload.role) {
case 'combobox':
case 'searchfield':
case 'textbox':
case 'TextField':
return true;
}
return false;
}
_name() : string {
if (this._payload.role === 'text')
return this._payload.value || '';
return this._payload.name || '';
}
isInteresting(insideControl: boolean) : boolean {
const {role, focusable, name} = this._payload;
const {role, focusable} = this._payload;
const name = this._name();
if (role === 'ScrollArea')
return false;
if (role === 'WebArea')
@ -92,30 +147,54 @@ class WKAXNode implements accessibility.AXNode {
return this.isLeafNode() && !!name;
}
_hasRendundantTextChild() {
if (this._children.length !== 1)
return false;
const child = this._children[0];
return child._payload.role === 'text' && this._payload.name === child._payload.value;
}
isLeafNode() : boolean {
return !this._children.length;
if (!this._children.length)
return true;
// WebKit on Linux ignores everything inside text controls, normalize this behavior
if (this._isTextControl())
return true;
// WebKit for mac has text nodes inside heading, li, menuitem, a, and p nodes
if (this._hasRendundantTextChild())
return true;
return false;
}
serialize(): accessibility.SerializedAXNode {
const node : accessibility.SerializedAXNode = {
role: this._payload.role,
name: this._payload.name || '',
role: WKRoleToARIARole.get(this._payload.role) || this._payload.role,
name: this._name(),
};
const userStringProperties: string[] = [
'value',
'description',
if ('description' in this._payload && this._payload.description !== node.name)
node.description = this._payload.description;
if ('roledescription' in this._payload) {
const roledescription = this._payload.roledescription;
if (roledescription !== this._payload.role && WKUnhelpfulRoleDescriptions.get(this._payload.role) !== roledescription)
node.roledescription = roledescription;
}
if ('value' in this._payload && this._payload.role !== 'text')
node.value = this._payload.value;
const userStringProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Page.AXNode> = [
'keyshortcuts',
'roledescription',
'valuetext'
];
for (const userStringProperty of userStringProperties) {
if (!(userStringProperty in this._payload))
continue;
(node as any)[userStringProperty] = (this._payload as any)[userStringProperty];
(node as any)[userStringProperty] = this._payload[userStringProperty];
}
const booleanProperties: string[] = [
const booleanProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Page.AXNode> = [
'disabled',
'expanded',
'focused',
@ -131,7 +210,7 @@ class WKAXNode implements accessibility.AXNode {
// not whether focus is specifically on the root node.
if (booleanProperty === 'focused' && (this._payload.role === 'WebArea' || this._payload.role === 'ScrollArea'))
continue;
const value = (this._payload as any)[booleanProperty];
const value = this._payload[booleanProperty];
if (!value)
continue;
(node as any)[booleanProperty] = value;
@ -147,7 +226,7 @@ class WKAXNode implements accessibility.AXNode {
const value = this._payload[tristateProperty];
node[tristateProperty] = value === 'mixed' ? 'mixed' : value === 'true' ? true : false;
}
const numericalProperties: string[] = [
const numericalProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Page.AXNode> = [
'level',
'valuemax',
'valuemin',
@ -157,7 +236,7 @@ class WKAXNode implements accessibility.AXNode {
continue;
(node as any)[numericalProperty] = (this._payload as any)[numericalProperty];
}
const tokenProperties: string[] = [
const tokenProperties: Array<keyof accessibility.SerializedAXNode & keyof Protocol.Page.AXNode> = [
'autocomplete',
'haspopup',
'invalid',

View File

@ -497,8 +497,8 @@ export class WKPage implements PageDelegate {
return to._createHandle(result.object) as dom.ElementHandle<T>;
}
async getAccessibilityTree() : Promise<accessibility.AXNode> {
return getAccessibilityTree(this._session);
async getAccessibilityTree(needle?: dom.ElementHandle) : Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}> {
return getAccessibilityTree(this._session, needle);
}
coverage(): Coverage | undefined {

View File

@ -15,19 +15,18 @@
* limitations under the License.
*/
module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) {
module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT, MAC}) {
const {describe, xdescribe, fdescribe} = testRunner;
const {it, fit, xit, dit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
describe('Accessibility', function() {
it.skip(WEBKIT)('should work', async function({page}) {
it('should work', async function({page}) {
await page.setContent(`
<head>
<title>Accessibility Test</title>
</head>
<body>
<div>Hello World</div>
<h1>Inputs</h1>
<input placeholder="Empty input" autofocus />
<input placeholder="readonly input" readonly />
@ -37,10 +36,6 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
<input aria-placeholder="placeholder" value="and a value" />
<div aria-hidden="true" id="desc">This is a description!</div>
<input aria-placeholder="placeholder" value="and a value" aria-describedby="desc" />
<select>
<option>First Option</option>
<option>Second Option</option>
</select>
</body>`);
// autofocus happens after a delay in chrome these days
await page.waitForFunction(() => document.activeElement.hasAttribute('autofocus'));
@ -49,24 +44,19 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
role: 'document',
name: 'Accessibility Test',
children: [
{role: 'text leaf', name: 'Hello World'},
{role: 'heading', name: 'Inputs', level: 1},
{role: 'entry', name: 'Empty input', focused: true},
{role: 'entry', name: 'readonly input', readonly: true},
{role: 'entry', name: 'disabled input', disabled: true},
{role: 'entry', name: 'Input with whitespace', value: ' '},
{role: 'entry', name: '', value: 'value only'},
{role: 'entry', name: '', value: 'and a value'}, // firefox doesn't use aria-placeholder for the name
{role: 'entry', name: '', value: 'and a value', description: 'This is a description!'}, // and here
{role: 'combobox', name: '', value: 'First Option', haspopup: true, children: [
{role: 'combobox option', name: 'First Option', selected: true},
{role: 'combobox option', name: 'Second Option'}]
}]
{role: 'textbox', name: 'Empty input', focused: true},
{role: 'textbox', name: 'readonly input', readonly: true},
{role: 'textbox', name: 'disabled input', disabled: true},
{role: 'textbox', name: 'Input with whitespace', value: ' '},
{role: 'textbox', name: '', value: 'value only'},
{role: 'textbox', name: '', value: 'and a value'}, // firefox doesn't use aria-placeholder for the name
{role: 'textbox', name: '', value: 'and a value', description: 'This is a description!'}, // and here
]
} : CHROMIUM ? {
role: 'WebArea',
name: 'Accessibility Test',
children: [
{role: 'text', name: 'Hello World'},
{role: 'heading', name: 'Inputs', level: 1},
{role: 'textbox', name: 'Empty input', focused: true},
{role: 'textbox', name: 'readonly input', readonly: true},
@ -75,65 +65,30 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
{role: 'textbox', name: '', value: 'value only'},
{role: 'textbox', name: 'placeholder', value: 'and a value'},
{role: 'textbox', name: 'placeholder', value: 'and a value', description: 'This is a description!'},
{role: 'combobox', name: '', value: 'First Option', children: [
{role: 'menuitem', name: 'First Option', selected: true},
{role: 'menuitem', name: 'Second Option'}]
}]
]
} : {
role: 'WebArea',
name: 'Accessibility Test',
children: [
{role: 'heading', name: 'Inputs', level: 1 },
{role: 'TextField', name: 'Empty input', focused: true, readonly: true},
{role: 'TextField', name: 'readonly input', readonly: true },
{role: 'TextField', name: 'disabled input', disabled: true, readonly: true},
{role: 'TextField', name: 'Input with whitespace', value: ' ', description: 'Input with whitespace', readonly: true},
{role: 'TextField', name: '', value: 'value only', readonly: true },
{role: 'TextField', name: 'placeholder',value: 'and a value',readonly: true},
{role: 'TextField', name: 'This is a description!',value: 'and a value',readonly: true},
{role: 'button', name: '', value: 'First Option', children: [
{ role: 'MenuListOption', name: '', value: 'First Option', selected: true },
{ role: 'MenuListOption', name: '', value: 'Second Option' }]
}
{role: 'heading', name: 'Inputs', level: 1},
{role: 'textbox', name: 'Empty input', focused: true},
{role: 'textbox', name: 'readonly input', readonly: true},
{role: 'textbox', name: 'disabled input', disabled: true},
{role: 'textbox', name: 'Input with whitespace', value: ' ' },
{role: 'textbox', name: '', value: 'value only' },
{role: 'textbox', name: 'placeholder', value: 'and a value'},
{role: 'textbox', name: 'This is a description!',value: 'and a value'}, // webkit uses the description over placeholder for the name
]
};
expect(await page.accessibility.snapshot()).toEqual(golden);
});
it.skip(WEBKIT)('should report uninteresting nodes', async function({page}) {
await page.setContent(`<textarea autofocus>hi</textarea>`);
// autofocus happens after a delay in chrome these days
await page.waitForFunction(() => document.activeElement.hasAttribute('autofocus'));
const golden = FFOX ? {
role: 'entry',
name: '',
value: 'hi',
focused: true,
multiline: true,
children: [{
role: 'text leaf',
name: 'hi'
}]
} : CHROMIUM ? {
role: 'textbox',
name: '',
value: 'hi',
focused: true,
multiline: true,
children: [{
role: 'generic',
name: '',
children: [{
role: 'text', name: 'hi'
}]
}]
} : {
role: 'textbox',
name: '',
value: 'hi',
focused: true,
multiline: true
};
expect(findFocusedNode(await page.accessibility.snapshot({interestingOnly: false}))).toEqual(golden);
it.skip(WEBKIT && !MAC)('should work with regular text', async({page}) => {
await page.setContent(`<div>Hello World</div>`);
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0]).toEqual({
role: FFOX ? 'text leaf' : 'text',
name: 'Hello World',
});
});
it('roledescription', async({page}) => {
await page.setContent('<div tabIndex=-1 aria-roledescription="foo">Hi</div>');
@ -145,8 +100,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0].orientation).toEqual('vertical');
});
it.skip(FFOX || WEBKIT)('autocomplete', async({page}) => {
await page.setContent('<input type="number" aria-autocomplete="list" />');
it('autocomplete', async({page}) => {
await page.setContent('<div role="textbox" aria-autocomplete="list">hi</div>');
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0].autocomplete).toEqual('list');
});
@ -167,33 +122,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
<div role="tab" aria-selected="true"><b>Tab1</b></div>
<div role="tab">Tab2</div>
</div>`);
const golden = FFOX ? {
role: 'document',
name: '',
children: [{
role: 'pagetab',
name: 'Tab1',
selected: true
}, {
role: 'pagetab',
name: 'Tab2'
}]
} : WEBKIT ? {
role: 'WebArea',
name: '',
roledescription: 'HTML content',
children: [{
role: 'tab',
name: 'Tab1',
roledescription: 'tab',
selected: true
}, {
role: 'tab',
name: 'Tab2',
roledescription: 'tab',
}]
} : {
role: 'WebArea',
const golden = {
role: FFOX ? 'document' : 'WebArea',
name: '',
children: [{
role: 'tab',
@ -244,7 +174,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
Edit this image: <img src="fakeimage.png" alt="my fake image">
</div>`);
const golden = FFOX ? {
role: 'entry',
role: 'textbox',
name: '',
value: 'Edit this image: my fake image',
children: [{
@ -305,7 +235,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
<img alt="yo" src="fakeimg.png">
</div>`);
const golden = FFOX ? {
role: 'entry',
role: 'textbox',
name: 'my favorite textbox',
value: 'this is the inner content yo'
} : CHROMIUM ? {
@ -313,10 +243,9 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
name: 'my favorite textbox',
value: 'this is the inner content '
} : {
role: 'TextField',
role: 'textbox',
name: 'my favorite textbox',
value: 'this is the inner content ',
description: 'my favorite textbox'
};
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0]).toEqual(golden);
@ -327,19 +256,10 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
this is the inner content
<img alt="yo" src="fakeimg.png">
</div>`);
const golden = FFOX ? {
role: 'checkbutton',
name: 'my favorite checkbox',
checked: true
} : CHROMIUM ? {
const golden = {
role: 'checkbox',
name: 'my favorite checkbox',
checked: true
} : {
role: 'checkbox',
name: 'my favorite checkbox',
description: "my favorite checkbox",
checked: true
};
const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0]).toEqual(golden);
@ -351,7 +271,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
<img alt="yo" src="fakeimg.png">
</div>`);
const golden = FFOX ? {
role: 'checkbutton',
role: 'checkbox',
name: 'this is the inner content yo',
checked: true
} : {
@ -363,7 +283,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
expect(snapshot.children[0]).toEqual(golden);
});
describe.skip(FFOX || WEBKIT)('root option', function() {
describe('root option', function() {
it('should work a button', async({page}) => {
await page.setContent(`<button>My Button</button>`);
@ -383,7 +303,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
value: 'My Value'
});
});
it('should work a menu', async({page}) => {
it('should work on a menu', async({page}) => {
await page.setContent(`
<div role="menu" title="My Menu">
<div role="menuitem">First Item</div>
@ -399,7 +319,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
children:
[ { role: 'menuitem', name: 'First Item' },
{ role: 'menuitem', name: 'Second Item' },
{ role: 'menuitem', name: 'Third Item' } ]
{ role: 'menuitem', name: 'Third Item' } ],
orientation: WEBKIT ? 'vertical' : undefined
});
});
it('should return null when the element is no longer in DOM', async({page}) => {
@ -408,38 +329,26 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
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: 'generic',
name: '',
children: [
{
role: 'button',
name: 'My Button',
children: [
{
role: "text",
name: "My Button"
}
],
}
]
});
it('should show uninteresting nodes', async({page}) => {
await page.setContent(`
<div id="root" role="textbox">
<div>
hello
<div>
world
</div>
</div>
</div>
`);
const root = await page.$('#root');
const snapshot = await page.accessibility.snapshot({root, interestingOnly: false});
expect(snapshot.role).toBe('textbox');
expect(snapshot.value).toContain('hello');
expect(snapshot.value).toContain('world');
expect(!!snapshot.children).toBe(true);
});
});
});
function findFocusedNode(node) {
if (node.focused)
return node;
for (const child of node.children || []) {
const focusedChild = findFocusedNode(child);
if (focusedChild)
return focusedChild;
}
return null;
}
});
};