diff options
Diffstat (limited to 'remote/test/puppeteer/test/src/accessibility.spec.ts')
-rw-r--r-- | remote/test/puppeteer/test/src/accessibility.spec.ts | 582 |
1 files changed, 582 insertions, 0 deletions
diff --git a/remote/test/puppeteer/test/src/accessibility.spec.ts b/remote/test/puppeteer/test/src/accessibility.spec.ts new file mode 100644 index 0000000000..3ab518e703 --- /dev/null +++ b/remote/test/puppeteer/test/src/accessibility.spec.ts @@ -0,0 +1,582 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import assert from 'assert'; + +import expect from 'expect'; +import {SerializedAXNode} from 'puppeteer-core/internal/common/Accessibility.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('Accessibility', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + it('should work', async () => { + const {page, isFirefox} = getTestState(); + + 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 /> + <input placeholder="disabled input" disabled /> + <input aria-label="Input with whitespace" value=" " /> + <input value="value only" /> + <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>`); + + await page.focus('[placeholder="Empty input"]'); + const golden = isFirefox + ? { + 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: 'RootWebArea', + name: 'Accessibility Test', + children: [ + {role: 'StaticText', name: 'Hello World'}, + {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: 'placeholder', + value: 'and a value', + description: 'This is a description!', + }, + { + role: 'combobox', + name: '', + value: 'First Option', + haspopup: 'menu', + children: [ + {role: 'menuitem', name: 'First Option', selected: true}, + {role: 'menuitem', name: 'Second Option'}, + ], + }, + ], + }; + expect(await page.accessibility.snapshot()).toEqual(golden); + }); + it('should report uninteresting nodes', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(`<textarea>hi</textarea>`); + await page.focus('textarea'); + const golden = isFirefox + ? { + role: 'entry', + name: '', + value: 'hi', + focused: true, + multiline: true, + children: [ + { + role: 'text leaf', + name: 'hi', + }, + ], + } + : { + role: 'textbox', + name: '', + value: 'hi', + focused: true, + multiline: true, + children: [ + { + role: 'generic', + name: '', + children: [ + { + role: 'StaticText', + name: 'hi', + }, + ], + }, + ], + }; + expect( + findFocusedNode( + await page.accessibility.snapshot({interestingOnly: false}) + ) + ).toEqual(golden); + }); + it('get snapshots while the tree is re-calculated', async () => { + // see https://github.com/puppeteer/puppeteer/issues/9404 + const {page} = getTestState(); + + await page.setContent( + `<!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Accessible name + aria-expanded puppeteer bug</title> + <style> + [aria-expanded="false"] + * { + display: none; + } + </style> + </head> + <body> + <button hidden>Show</button> + <p>Some content</p> + <script> + const button = document.querySelector('button'); + button.removeAttribute('hidden') + button.setAttribute('aria-expanded', 'false'); + button.addEventListener('click', function() { + button.setAttribute('aria-expanded', button.getAttribute('aria-expanded') !== 'true') + if (button.getAttribute('aria-expanded') == 'true') { + button.textContent = 'Hide' + } else { + button.textContent = 'Show' + } + }) + </script> + </body> + </html>` + ); + async function getAccessibleName(page: any, element: any) { + return (await page.accessibility.snapshot({root: element})).name; + } + const button = await page.$('button'); + expect(await getAccessibleName(page, button)).toEqual('Show'); + await button?.click(); + await page.waitForSelector('aria/Hide'); + }); + it('roledescription', async () => { + const {page} = getTestState(); + + await page.setContent( + '<div tabIndex=-1 aria-roledescription="foo">Hi</div>' + ); + const snapshot = await page.accessibility.snapshot(); + // See https://chromium-review.googlesource.com/c/chromium/src/+/3088862 + assert(snapshot); + assert(snapshot.children); + assert(snapshot.children[0]); + expect(snapshot.children[0]!.roledescription).toBeUndefined(); + }); + it('orientation', async () => { + const {page} = getTestState(); + + await page.setContent( + '<a href="" role="slider" aria-orientation="vertical">11</a>' + ); + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + assert(snapshot.children[0]); + expect(snapshot.children[0]!.orientation).toEqual('vertical'); + }); + it('autocomplete', async () => { + const {page} = getTestState(); + + await page.setContent('<input type="number" aria-autocomplete="list" />'); + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + assert(snapshot.children[0]); + expect(snapshot.children[0]!.autocomplete).toEqual('list'); + }); + it('multiselectable', async () => { + const {page} = getTestState(); + + await page.setContent( + '<div role="grid" tabIndex=-1 aria-multiselectable=true>hey</div>' + ); + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + assert(snapshot.children[0]); + expect(snapshot.children[0]!.multiselectable).toEqual(true); + }); + it('keyshortcuts', async () => { + const {page} = getTestState(); + + await page.setContent( + '<div role="grid" tabIndex=-1 aria-keyshortcuts="foo">hey</div>' + ); + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + assert(snapshot.children[0]); + expect(snapshot.children[0]!.keyshortcuts).toEqual('foo'); + }); + describe('filtering children of leaf nodes', function () { + it('should not report text nodes inside controls', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(` + <div role="tablist"> + <div role="tab" aria-selected="true"><b>Tab1</b></div> + <div role="tab">Tab2</div> + </div>`); + const golden = isFirefox + ? { + role: 'document', + name: '', + children: [ + { + role: 'pagetab', + name: 'Tab1', + selected: true, + }, + { + role: 'pagetab', + name: 'Tab2', + }, + ], + } + : { + role: 'RootWebArea', + name: '', + children: [ + { + role: 'tab', + name: 'Tab1', + selected: true, + }, + { + role: 'tab', + name: 'Tab2', + }, + ], + }; + expect(await page.accessibility.snapshot()).toEqual(golden); + }); + it('rich text editable fields should have children', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(` + <div contenteditable="true"> + Edit this image: <img src="fakeimage.png" alt="my fake image"> + </div>`); + const golden = isFirefox + ? { + role: 'section', + name: '', + children: [ + { + role: 'text leaf', + name: 'Edit this image:', + }, + { + role: 'StaticText', + name: 'my fake image', + }, + ], + } + : { + role: 'generic', + name: '', + value: 'Edit this image: ', + children: [ + { + role: 'StaticText', + name: 'Edit this image: ', + }, + { + role: 'img', + name: 'my fake image', + }, + ], + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual(golden); + }); + it('rich text editable fields with role should have children', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(` + <div contenteditable="true" role='textbox'> + Edit this image: <img src="fakeimage.png" alt="my fake image"> + </div>`); + // Image node should not be exposed in contenteditable elements. See https://crbug.com/1324392. + const golden = isFirefox + ? { + role: 'entry', + name: '', + value: 'Edit this image: my fake image', + children: [ + { + role: 'StaticText', + name: 'my fake image', + }, + ], + } + : { + role: 'textbox', + name: '', + value: 'Edit this image: ', + multiline: true, + children: [ + { + role: 'StaticText', + name: 'Edit this image: ', + }, + ], + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual(golden); + }); + + // Firefox does not support contenteditable="plaintext-only". + describe('plaintext contenteditable', function () { + it('plain text field with role should not have children', async () => { + const {page} = getTestState(); + + await page.setContent(` + <div contenteditable="plaintext-only" role='textbox'>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`); + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual({ + role: 'textbox', + name: '', + value: 'Edit this image:', + multiline: true, + }); + }); + }); + it('non editable textbox with role and tabIndex and label should not have children', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(` + <div role="textbox" tabIndex=0 aria-checked="true" aria-label="my favorite textbox"> + this is the inner content + <img alt="yo" src="fakeimg.png"> + </div>`); + const golden = isFirefox + ? { + role: 'entry', + name: 'my favorite textbox', + value: 'this is the inner content yo', + } + : { + role: 'textbox', + name: 'my favorite textbox', + value: 'this is the inner content ', + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual(golden); + }); + it('checkbox with and tabIndex and label should not have children', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(` + <div role="checkbox" tabIndex=0 aria-checked="true" aria-label="my favorite checkbox"> + this is the inner content + <img alt="yo" src="fakeimg.png"> + </div>`); + const golden = isFirefox + ? { + role: 'checkbutton', + name: 'my favorite checkbox', + checked: true, + } + : { + role: 'checkbox', + name: 'my favorite checkbox', + checked: true, + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual(golden); + }); + it('checkbox without label should not have children', async () => { + const {page, isFirefox} = getTestState(); + + await page.setContent(` + <div role="checkbox" aria-checked="true"> + this is the inner content + <img alt="yo" src="fakeimg.png"> + </div>`); + const golden = isFirefox + ? { + role: 'checkbutton', + name: 'this is the inner content yo', + checked: true, + } + : { + role: 'checkbox', + name: 'this is the inner content yo', + checked: true, + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual(golden); + }); + + describe('root option', function () { + it('should work a button', async () => { + const {page} = getTestState(); + + await page.setContent(`<button>My Button</button>`); + + const button = (await page.$('button'))!; + expect(await page.accessibility.snapshot({root: button})).toEqual({ + role: 'button', + name: 'My Button', + }); + }); + it('should work an input', async () => { + const {page} = getTestState(); + + await page.setContent(`<input title="My Input" value="My Value">`); + + const input = (await page.$('input'))!; + expect(await page.accessibility.snapshot({root: input})).toEqual({ + role: 'textbox', + name: 'My Input', + value: 'My Value', + }); + }); + it('should work a menu', async () => { + const {page} = getTestState(); + + await page.setContent(` + <div role="menu" title="My Menu"> + <div role="menuitem">First Item</div> + <div role="menuitem">Second Item</div> + <div role="menuitem">Third Item</div> + </div> + `); + + const menu = (await page.$('div[role="menu"]'))!; + expect(await page.accessibility.snapshot({root: menu})).toEqual({ + role: 'menu', + name: 'My Menu', + children: [ + {role: 'menuitem', name: 'First Item'}, + {role: 'menuitem', name: 'Second Item'}, + {role: 'menuitem', name: 'Third Item'}, + ], + orientation: 'vertical', + }); + }); + it('should return null when the element is no longer in DOM', async () => { + const {page} = getTestState(); + + await page.setContent(`<button>My Button</button>`); + const button = (await page.$('button'))!; + await page.$eval('button', button => { + return button.remove(); + }); + expect(await page.accessibility.snapshot({root: button})).toEqual(null); + }); + it('should support the interestingOnly option', async () => { + const {page} = getTestState(); + + 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: 'StaticText', name: 'My Button'}], + }, + ], + }); + }); + }); + }); + + function findFocusedNode( + node: SerializedAXNode | null + ): SerializedAXNode | null { + if (node?.focused) { + return node; + } + for (const child of node?.children || []) { + const focusedChild = findFocusedNode(child); + if (focusedChild) { + return focusedChild; + } + } + return null; + } +}); |