mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
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:
parent
92bd854d8f
commit
aaa1c9203e
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user