normalize all the test output

This commit is contained in:
Joel Einbinder 2020-01-14 13:35:31 -08:00
parent f2d3ac68ae
commit 36b77efdc0
4 changed files with 103 additions and 128 deletions

View file

@ -73,14 +73,17 @@ export class Accessibility {
root = null, root = null,
} = options; } = options;
const {tree, needle} = await this._getAXTree(root); const {tree, needle} = await this._getAXTree(root);
if (!interestingOnly) if (!interestingOnly) {
return serializeTree(needle)[0]; if (root)
return needle && serializeTree(needle)[0];
return serializeTree(tree)[0];
}
const interestingNodes: Set<AXNode> = new Set(); const interestingNodes: Set<AXNode> = new Set();
collectInterestingNodes(interestingNodes, tree, false); collectInterestingNodes(interestingNodes, tree, false);
if (root && !interestingNodes.has(needle)) if (root && !interestingNodes.has(needle))
return null; return null;
return serializeTree(needle, interestingNodes)[0]; return serializeTree(needle || tree, interestingNodes)[0];
} }
} }

View file

@ -30,6 +30,33 @@ export async function getAccessibilityTree(session: FFSession, needle: dom.Eleme
}; };
} }
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 { class FFAXNode implements accessibility.AXNode {
_children: FFAXNode[]; _children: FFAXNode[];
private _payload: Protocol.AXTree; private _payload: Protocol.AXTree;
@ -173,7 +200,7 @@ class FFAXNode implements accessibility.AXNode {
serialize(): accessibility.SerializedAXNode { serialize(): accessibility.SerializedAXNode {
const node: {[x in keyof accessibility.SerializedAXNode]: any} = { const node: {[x in keyof accessibility.SerializedAXNode]: any} = {
role: this._role, role: FFRoleToARIARole.get(this._role) || this._role,
name: this._name || '' name: this._name || ''
}; };
const userStringProperties: Array<keyof accessibility.SerializedAXNode> = [ const userStringProperties: Array<keyof accessibility.SerializedAXNode> = [

View file

@ -28,6 +28,30 @@ export async function getAccessibilityTree(session: WKSession, needle?: dom.Elem
}; };
} }
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 { class WKAXNode implements accessibility.AXNode {
private _payload: Protocol.Page.AXNode; private _payload: Protocol.Page.AXNode;
private _children: WKAXNode[]; private _children: WKAXNode[];
@ -48,7 +72,7 @@ class WKAXNode implements accessibility.AXNode {
if (this._payload.found) if (this._payload.found)
return this; return this;
for (const child of this._children) { for (const child of this._children) {
const found = child._findNeedle() const found = child._findNeedle();
if (found) if (found)
return found; return found;
} }
@ -111,15 +135,27 @@ class WKAXNode implements accessibility.AXNode {
serialize(): accessibility.SerializedAXNode { serialize(): accessibility.SerializedAXNode {
const node : accessibility.SerializedAXNode = { const node : accessibility.SerializedAXNode = {
role: this._payload.role, role: WKRoleToARIARole.get(this._payload.role) || this._payload.role,
name: this._payload.name || '', name: this._payload.name || '',
}; };
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;
}
type AXPropertyOfType<Type> = {
[Key in keyof Protocol.Page.AXNode]:
Protocol.Page.AXNode[Key] extends Type ? Key : never
}[keyof Protocol.Page.AXNode];
const userStringProperties: string[] = [ const userStringProperties: string[] = [
'value', 'value',
'description',
'keyshortcuts', 'keyshortcuts',
'roledescription',
'valuetext' 'valuetext'
]; ];
for (const userStringProperty of userStringProperties) { for (const userStringProperty of userStringProperties) {

View file

@ -21,7 +21,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
describe('Accessibility', function() { describe('Accessibility', function() {
it.skip(WEBKIT)('should work', async function({page}) { it('should work', async function({page}) {
await page.setContent(` await page.setContent(`
<head> <head>
<title>Accessibility Test</title> <title>Accessibility Test</title>
@ -51,13 +51,13 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
children: [ children: [
{role: 'text leaf', name: 'Hello World'}, {role: 'text leaf', name: 'Hello World'},
{role: 'heading', name: 'Inputs', level: 1}, {role: 'heading', name: 'Inputs', level: 1},
{role: 'entry', name: 'Empty input', focused: true}, {role: 'textbox', name: 'Empty input', focused: true},
{role: 'entry', name: 'readonly input', readonly: true}, {role: 'textbox', name: 'readonly input', readonly: true},
{role: 'entry', name: 'disabled input', disabled: true}, {role: 'textbox', name: 'disabled input', disabled: true},
{role: 'entry', name: 'Input with whitespace', value: ' '}, {role: 'textbox', name: 'Input with whitespace', value: ' '},
{role: 'entry', name: '', value: 'value only'}, {role: 'textbox', name: '', value: 'value only'},
{role: 'entry', name: '', value: 'and a value'}, // firefox doesn't use aria-placeholder for the name {role: 'textbox', 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: 'textbox', name: '', value: 'and a value', description: 'This is a description!'}, // and here
{role: 'combobox', name: '', value: 'First Option', haspopup: true, children: [ {role: 'combobox', name: '', value: 'First Option', haspopup: true, children: [
{role: 'combobox option', name: 'First Option', selected: true}, {role: 'combobox option', name: 'First Option', selected: true},
{role: 'combobox option', name: 'Second Option'}] {role: 'combobox option', name: 'Second Option'}]
@ -83,14 +83,14 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
role: 'WebArea', role: 'WebArea',
name: 'Accessibility Test', name: 'Accessibility Test',
children: [ children: [
{role: 'heading', name: 'Inputs', level: 1 }, {role: 'heading', name: 'Inputs', level: 1},
{role: 'TextField', name: 'Empty input', focused: true, readonly: true}, {role: 'textbox', name: 'Empty input', focused: true},
{role: 'TextField', name: 'readonly input', readonly: true }, {role: 'textbox', name: 'readonly input', readonly: true},
{role: 'TextField', name: 'disabled input', disabled: true, readonly: true}, {role: 'textbox', name: 'disabled input', disabled: true},
{role: 'TextField', name: 'Input with whitespace', value: ' ', description: 'Input with whitespace', readonly: true}, {role: 'textbox', name: 'Input with whitespace', value: ' ' },
{role: 'TextField', name: '', value: 'value only', readonly: true }, {role: 'textbox', name: '', value: 'value only' },
{role: 'TextField', name: 'placeholder',value: 'and a value',readonly: true}, {role: 'textbox', name: 'placeholder',value: 'and a value'},
{role: 'TextField', name: 'This is a description!',value: 'and a value',readonly: true}, {role: 'textbox', name: 'This is a description!',value: 'and a value'}, // webkit uses the description over placeholder for the name
{role: 'button', name: '', value: 'First Option', children: [ {role: 'button', name: '', value: 'First Option', children: [
{ role: 'MenuListOption', name: '', value: 'First Option', selected: true }, { role: 'MenuListOption', name: '', value: 'First Option', selected: true },
{ role: 'MenuListOption', name: '', value: 'Second Option' }] { role: 'MenuListOption', name: '', value: 'Second Option' }]
@ -99,42 +99,6 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
}; };
expect(await page.accessibility.snapshot()).toEqual(golden); 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('roledescription', async({page}) => { it('roledescription', async({page}) => {
await page.setContent('<div tabIndex=-1 aria-roledescription="foo">Hi</div>'); await page.setContent('<div tabIndex=-1 aria-roledescription="foo">Hi</div>');
const snapshot = await page.accessibility.snapshot(); const snapshot = await page.accessibility.snapshot();
@ -145,8 +109,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
const snapshot = await page.accessibility.snapshot(); const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0].orientation).toEqual('vertical'); expect(snapshot.children[0].orientation).toEqual('vertical');
}); });
it.skip(FFOX || WEBKIT)('autocomplete', async({page}) => { it('autocomplete', async({page}) => {
await page.setContent('<input type="number" aria-autocomplete="list" />'); await page.setContent('<div role="textbox" aria-autocomplete="list">hi</div>');
const snapshot = await page.accessibility.snapshot(); const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0].autocomplete).toEqual('list'); expect(snapshot.children[0].autocomplete).toEqual('list');
}); });
@ -167,33 +131,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
<div role="tab" aria-selected="true"><b>Tab1</b></div> <div role="tab" aria-selected="true"><b>Tab1</b></div>
<div role="tab">Tab2</div> <div role="tab">Tab2</div>
</div>`); </div>`);
const golden = FFOX ? { const golden = {
role: 'document', role: FFOX ? 'document' : 'WebArea',
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',
name: '', name: '',
children: [{ children: [{
role: 'tab', role: 'tab',
@ -244,7 +183,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
Edit this image: <img src="fakeimage.png" alt="my fake image"> Edit this image: <img src="fakeimage.png" alt="my fake image">
</div>`); </div>`);
const golden = FFOX ? { const golden = FFOX ? {
role: 'entry', role: 'textbox',
name: '', name: '',
value: 'Edit this image: my fake image', value: 'Edit this image: my fake image',
children: [{ children: [{
@ -305,7 +244,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
<img alt="yo" src="fakeimg.png"> <img alt="yo" src="fakeimg.png">
</div>`); </div>`);
const golden = FFOX ? { const golden = FFOX ? {
role: 'entry', role: 'textbox',
name: 'my favorite textbox', name: 'my favorite textbox',
value: 'this is the inner content yo' value: 'this is the inner content yo'
} : CHROMIUM ? { } : CHROMIUM ? {
@ -313,10 +252,9 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
name: 'my favorite textbox', name: 'my favorite textbox',
value: 'this is the inner content ' value: 'this is the inner content '
} : { } : {
role: 'TextField', role: 'textbox',
name: 'my favorite textbox', name: 'my favorite textbox',
value: 'this is the inner content ', value: 'this is the inner content ',
description: 'my favorite textbox'
}; };
const snapshot = await page.accessibility.snapshot(); const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0]).toEqual(golden); expect(snapshot.children[0]).toEqual(golden);
@ -327,19 +265,10 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
this is the inner content this is the inner content
<img alt="yo" src="fakeimg.png"> <img alt="yo" src="fakeimg.png">
</div>`); </div>`);
const golden = FFOX ? { const golden = {
role: 'checkbutton',
name: 'my favorite checkbox',
checked: true
} : CHROMIUM ? {
role: 'checkbox', role: 'checkbox',
name: 'my favorite checkbox', name: 'my favorite checkbox',
checked: true checked: true
} : {
role: 'checkbox',
name: 'my favorite checkbox',
description: "my favorite checkbox",
checked: true
}; };
const snapshot = await page.accessibility.snapshot(); const snapshot = await page.accessibility.snapshot();
expect(snapshot.children[0]).toEqual(golden); expect(snapshot.children[0]).toEqual(golden);
@ -351,7 +280,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
<img alt="yo" src="fakeimg.png"> <img alt="yo" src="fakeimg.png">
</div>`); </div>`);
const golden = FFOX ? { const golden = FFOX ? {
role: 'checkbutton', role: 'checkbox',
name: 'this is the inner content yo', name: 'this is the inner content yo',
checked: true checked: true
} : { } : {
@ -364,7 +293,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
}); });
describe('root option', function() { describe('root option', function() {
fit('should work a button', async({page}) => { it('should work a button', async({page}) => {
await page.setContent(`<button>My Button</button>`); await page.setContent(`<button>My Button</button>`);
const button = await page.$('button'); const button = await page.$('button');
@ -383,7 +312,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
value: 'My Value' value: 'My Value'
}); });
}); });
it('should work a menu', async({page}) => { it('should work on a menu', async({page}) => {
await page.setContent(` await page.setContent(`
<div role="menu" title="My Menu"> <div role="menu" title="My Menu">
<div role="menuitem">First Item</div> <div role="menuitem">First Item</div>
@ -399,7 +328,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
children: children:
[ { role: 'menuitem', name: 'First Item' }, [ { role: 'menuitem', name: 'First Item' },
{ role: 'menuitem', name: 'Second 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}) => { it('should return null when the element is no longer in DOM', async({page}) => {
@ -408,27 +338,6 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
await page.$eval('button', button => button.remove()); await page.$eval('button', button => button.remove());
expect(await page.accessibility.snapshot({root: button})).toEqual(null); 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"
}
],
}
]
});
});
}); });
}); });
function findFocusedNode(node) { function findFocusedNode(node) {