diff --git a/src/accessibility.ts b/src/accessibility.ts index f4ce8a4b9d..63324b46d1 100644 --- a/src/accessibility.ts +++ b/src/accessibility.ts @@ -55,13 +55,12 @@ export interface AXNode { isLeafNode(): boolean; isControl(): boolean; serialize(): SerializedAXNode; - findElement(element: dom.ElementHandle): Promise; children(): Iterable; } export class Accessibility { - private _getAXTree: () => Promise; - constructor(getAXTree: () => Promise) { + 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 = 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]; } } diff --git a/src/chromium/crAccessibility.ts b/src/chromium/crAccessibility.ts index c582946ee8..591dde0009 100644 --- a/src/chromium/crAccessibility.ts +++ b/src/chromium/crAccessibility.ts @@ -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 { +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 { + async _findElement(element: dom.ElementHandle): Promise { 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); diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index 955e35692b..776809c9cd 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -480,8 +480,8 @@ export class CRPage implements PageDelegate { return to._createHandle(result.object).asElement()!; } - async getAccessibilityTree(): Promise { - return getAccessibilityTree(this._client); + async getAccessibilityTree(needle?: dom.ElementHandle) { + return getAccessibilityTree(this._client, needle); } async pdf(options?: types.PDFOptions): Promise { diff --git a/src/firefox/ffAccessibility.ts b/src/firefox/ffAccessibility.ts index 8dd4f49b7d..a83b7bf5ce 100644 --- a/src/firefox/ffAccessibility.ts +++ b/src/firefox/ffAccessibility.ts @@ -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 { - 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 { - 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 = [ + const userStringProperties: Array = [ 'name', 'value', 'description', @@ -176,7 +216,7 @@ class FFAXNode implements accessibility.AXNode { continue; node[userStringProperty] = this._payload[userStringProperty]; } - const booleanProperties: Array = [ + const booleanProperties: Array = [ 'disabled', 'expanded', 'focused', @@ -195,7 +235,7 @@ class FFAXNode implements accessibility.AXNode { continue; node[booleanProperty] = value; } - const tristateProperties: Array = [ + const tristateProperties: Array = [ 'checked', 'pressed', ]; @@ -205,7 +245,7 @@ class FFAXNode implements accessibility.AXNode { const value = this._payload[tristateProperty]; node[tristateProperty] = value; } - const numericalProperties: Array = [ + const numericalProperties: Array = [ 'level' ]; for (const numericalProperty of numericalProperties) { @@ -213,7 +253,7 @@ class FFAXNode implements accessibility.AXNode { continue; node[numericalProperty] = this._payload[numericalProperty]; } - const tokenProperties: Array = [ + const tokenProperties: Array = [ 'autocomplete', 'haspopup', 'invalid', diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index ccf2d61be1..6b834d933f 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -355,8 +355,8 @@ export class FFPage implements PageDelegate { return handle; } - async getAccessibilityTree() : Promise { - return getAccessibilityTree(this._session); + async getAccessibilityTree(needle?: dom.ElementHandle) { + return getAccessibilityTree(this._session, needle); } coverage(): Coverage | undefined { diff --git a/src/page.ts b/src/page.ts index 47bf3304e0..7ba29e8d8c 100644 --- a/src/page.ts +++ b/src/page.ts @@ -68,7 +68,7 @@ export interface PageDelegate { setInputFiles(handle: dom.ElementHandle, files: types.FilePayload[]): Promise; getBoundingBox(handle: dom.ElementHandle): Promise; - getAccessibilityTree(): Promise; + getAccessibilityTree(needle?: dom.ElementHandle): Promise<{tree: accessibility.AXNode, needle: accessibility.AXNode | null}>; pdf?: (options?: types.PDFOptions) => Promise; coverage(): Coverage | undefined; } diff --git a/src/webkit/wkAccessibility.ts b/src/webkit/wkAccessibility.ts index d957f4b988..f0454737dd 100644 --- a/src/webkit/wkAccessibility.ts +++ b/src/webkit/wkAccessibility.ts @@ -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 = [ '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 = [ '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 = [ '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 = [ 'autocomplete', 'haspopup', 'invalid', diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index 6416b8a8f3..c3848adf3e 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -497,8 +497,8 @@ export class WKPage implements PageDelegate { return to._createHandle(result.object) as dom.ElementHandle; } - async getAccessibilityTree() : Promise { - 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 { diff --git a/test/accessibility.spec.js b/test/accessibility.spec.js index 7c13afd9c0..64ab278d4a 100644 --- a/test/accessibility.spec.js +++ b/test/accessibility.spec.js @@ -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(` Accessibility Test -
Hello World

Inputs

@@ -37,10 +36,6 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) - `); // 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(``); - // 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(`
Hello World
`); + 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('
Hi
'); @@ -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(''); + it('autocomplete', async({page}) => { + await page.setContent('
hi
'); 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})
Tab1
Tab2
`); - 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: my fake image `); 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}) yo `); 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 yo `); - 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}) yo `); 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(``); @@ -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(`
First Item
@@ -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(`
`); - 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(` +
+
+ hello +
+ world +
+
+
+ `); + + 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; - } }); };