1/** 2 * Copyright 2018 Google Inc. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import expect from 'expect'; 18import { 19 getTestState, 20 setupTestBrowserHooks, 21 setupTestPageAndContextHooks, 22 describeFailsFirefox, 23} from './mocha-utils'; // eslint-disable-line import/extensions 24 25describeFailsFirefox('Accessibility', function () { 26 setupTestBrowserHooks(); 27 setupTestPageAndContextHooks(); 28 29 it('should work', async () => { 30 const { page, isFirefox } = getTestState(); 31 32 await page.setContent(` 33 <head> 34 <title>Accessibility Test</title> 35 </head> 36 <body> 37 <div>Hello World</div> 38 <h1>Inputs</h1> 39 <input placeholder="Empty input" autofocus /> 40 <input placeholder="readonly input" readonly /> 41 <input placeholder="disabled input" disabled /> 42 <input aria-label="Input with whitespace" value=" " /> 43 <input value="value only" /> 44 <input aria-placeholder="placeholder" value="and a value" /> 45 <div aria-hidden="true" id="desc">This is a description!</div> 46 <input aria-placeholder="placeholder" value="and a value" aria-describedby="desc" /> 47 <select> 48 <option>First Option</option> 49 <option>Second Option</option> 50 </select> 51 </body>`); 52 53 await page.focus('[placeholder="Empty input"]'); 54 const golden = isFirefox 55 ? { 56 role: 'document', 57 name: 'Accessibility Test', 58 children: [ 59 { role: 'text leaf', name: 'Hello World' }, 60 { role: 'heading', name: 'Inputs', level: 1 }, 61 { role: 'entry', name: 'Empty input', focused: true }, 62 { role: 'entry', name: 'readonly input', readonly: true }, 63 { role: 'entry', name: 'disabled input', disabled: true }, 64 { role: 'entry', name: 'Input with whitespace', value: ' ' }, 65 { role: 'entry', name: '', value: 'value only' }, 66 { role: 'entry', name: '', value: 'and a value' }, // firefox doesn't use aria-placeholder for the name 67 { 68 role: 'entry', 69 name: '', 70 value: 'and a value', 71 description: 'This is a description!', 72 }, // and here 73 { 74 role: 'combobox', 75 name: '', 76 value: 'First Option', 77 haspopup: true, 78 children: [ 79 { 80 role: 'combobox option', 81 name: 'First Option', 82 selected: true, 83 }, 84 { role: 'combobox option', name: 'Second Option' }, 85 ], 86 }, 87 ], 88 } 89 : { 90 role: 'RootWebArea', 91 name: 'Accessibility Test', 92 children: [ 93 { role: 'StaticText', name: 'Hello World' }, 94 { role: 'heading', name: 'Inputs', level: 1 }, 95 { role: 'textbox', name: 'Empty input', focused: true }, 96 { role: 'textbox', name: 'readonly input', readonly: true }, 97 { role: 'textbox', name: 'disabled input', disabled: true }, 98 { role: 'textbox', name: 'Input with whitespace', value: ' ' }, 99 { role: 'textbox', name: '', value: 'value only' }, 100 { role: 'textbox', name: 'placeholder', value: 'and a value' }, 101 { 102 role: 'textbox', 103 name: 'placeholder', 104 value: 'and a value', 105 description: 'This is a description!', 106 }, 107 { 108 role: 'combobox', 109 name: '', 110 value: 'First Option', 111 children: [ 112 { role: 'menuitem', name: 'First Option', selected: true }, 113 { role: 'menuitem', name: 'Second Option' }, 114 ], 115 }, 116 ], 117 }; 118 expect(await page.accessibility.snapshot()).toEqual(golden); 119 }); 120 it('should report uninteresting nodes', async () => { 121 const { page, isFirefox } = getTestState(); 122 123 await page.setContent(`<textarea>hi</textarea>`); 124 await page.focus('textarea'); 125 const golden = isFirefox 126 ? { 127 role: 'entry', 128 name: '', 129 value: 'hi', 130 focused: true, 131 multiline: true, 132 children: [ 133 { 134 role: 'text leaf', 135 name: 'hi', 136 }, 137 ], 138 } 139 : { 140 role: 'textbox', 141 name: '', 142 value: 'hi', 143 focused: true, 144 multiline: true, 145 children: [ 146 { 147 role: 'generic', 148 name: '', 149 children: [ 150 { 151 role: 'StaticText', 152 name: 'hi', 153 }, 154 ], 155 }, 156 ], 157 }; 158 expect( 159 findFocusedNode( 160 await page.accessibility.snapshot({ interestingOnly: false }) 161 ) 162 ).toEqual(golden); 163 }); 164 it('roledescription', async () => { 165 const { page } = getTestState(); 166 167 await page.setContent( 168 '<div tabIndex=-1 aria-roledescription="foo">Hi</div>' 169 ); 170 const snapshot = await page.accessibility.snapshot(); 171 // See https://chromium-review.googlesource.com/c/chromium/src/+/3088862 172 expect(snapshot.children[0].roledescription).toEqual(undefined); 173 }); 174 it('orientation', async () => { 175 const { page } = getTestState(); 176 177 await page.setContent( 178 '<a href="" role="slider" aria-orientation="vertical">11</a>' 179 ); 180 const snapshot = await page.accessibility.snapshot(); 181 expect(snapshot.children[0].orientation).toEqual('vertical'); 182 }); 183 it('autocomplete', async () => { 184 const { page } = getTestState(); 185 186 await page.setContent('<input type="number" aria-autocomplete="list" />'); 187 const snapshot = await page.accessibility.snapshot(); 188 expect(snapshot.children[0].autocomplete).toEqual('list'); 189 }); 190 it('multiselectable', async () => { 191 const { page } = getTestState(); 192 193 await page.setContent( 194 '<div role="grid" tabIndex=-1 aria-multiselectable=true>hey</div>' 195 ); 196 const snapshot = await page.accessibility.snapshot(); 197 expect(snapshot.children[0].multiselectable).toEqual(true); 198 }); 199 it('keyshortcuts', async () => { 200 const { page } = getTestState(); 201 202 await page.setContent( 203 '<div role="grid" tabIndex=-1 aria-keyshortcuts="foo">hey</div>' 204 ); 205 const snapshot = await page.accessibility.snapshot(); 206 expect(snapshot.children[0].keyshortcuts).toEqual('foo'); 207 }); 208 describe('filtering children of leaf nodes', function () { 209 it('should not report text nodes inside controls', async () => { 210 const { page, isFirefox } = getTestState(); 211 212 await page.setContent(` 213 <div role="tablist"> 214 <div role="tab" aria-selected="true"><b>Tab1</b></div> 215 <div role="tab">Tab2</div> 216 </div>`); 217 const golden = isFirefox 218 ? { 219 role: 'document', 220 name: '', 221 children: [ 222 { 223 role: 'pagetab', 224 name: 'Tab1', 225 selected: true, 226 }, 227 { 228 role: 'pagetab', 229 name: 'Tab2', 230 }, 231 ], 232 } 233 : { 234 role: 'RootWebArea', 235 name: '', 236 children: [ 237 { 238 role: 'tab', 239 name: 'Tab1', 240 selected: true, 241 }, 242 { 243 role: 'tab', 244 name: 'Tab2', 245 }, 246 ], 247 }; 248 expect(await page.accessibility.snapshot()).toEqual(golden); 249 }); 250 it('rich text editable fields should have children', async () => { 251 const { page, isFirefox } = getTestState(); 252 253 await page.setContent(` 254 <div contenteditable="true"> 255 Edit this image: <img src="fakeimage.png" alt="my fake image"> 256 </div>`); 257 const golden = isFirefox 258 ? { 259 role: 'section', 260 name: '', 261 children: [ 262 { 263 role: 'text leaf', 264 name: 'Edit this image: ', 265 }, 266 { 267 role: 'StaticText', 268 name: 'my fake image', 269 }, 270 ], 271 } 272 : { 273 role: 'generic', 274 name: '', 275 value: 'Edit this image: ', 276 children: [ 277 { 278 role: 'StaticText', 279 name: 'Edit this image:', 280 }, 281 { 282 role: 'img', 283 name: 'my fake image', 284 }, 285 ], 286 }; 287 const snapshot = await page.accessibility.snapshot(); 288 expect(snapshot.children[0]).toEqual(golden); 289 }); 290 it('rich text editable fields with role should have children', async () => { 291 const { page, isFirefox } = getTestState(); 292 293 await page.setContent(` 294 <div contenteditable="true" role='textbox'> 295 Edit this image: <img src="fakeimage.png" alt="my fake image"> 296 </div>`); 297 const golden = isFirefox 298 ? { 299 role: 'entry', 300 name: '', 301 value: 'Edit this image: my fake image', 302 children: [ 303 { 304 role: 'StaticText', 305 name: 'my fake image', 306 }, 307 ], 308 } 309 : { 310 role: 'textbox', 311 name: '', 312 value: 'Edit this image: ', 313 multiline: true, 314 children: [ 315 { 316 role: 'StaticText', 317 name: 'Edit this image:', 318 }, 319 { 320 role: 'img', 321 name: 'my fake image', 322 }, 323 ], 324 }; 325 const snapshot = await page.accessibility.snapshot(); 326 expect(snapshot.children[0]).toEqual(golden); 327 }); 328 329 // Firefox does not support contenteditable="plaintext-only". 330 describeFailsFirefox('plaintext contenteditable', function () { 331 it('plain text field with role should not have children', async () => { 332 const { page } = getTestState(); 333 334 await page.setContent(` 335 <div contenteditable="plaintext-only" role='textbox'>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`); 336 const snapshot = await page.accessibility.snapshot(); 337 expect(snapshot.children[0]).toEqual({ 338 role: 'textbox', 339 name: '', 340 value: 'Edit this image:', 341 multiline: true, 342 }); 343 }); 344 }); 345 it('non editable textbox with role and tabIndex and label should not have children', async () => { 346 const { page, isFirefox } = getTestState(); 347 348 await page.setContent(` 349 <div role="textbox" tabIndex=0 aria-checked="true" aria-label="my favorite textbox"> 350 this is the inner content 351 <img alt="yo" src="fakeimg.png"> 352 </div>`); 353 const golden = isFirefox 354 ? { 355 role: 'entry', 356 name: 'my favorite textbox', 357 value: 'this is the inner content yo', 358 } 359 : { 360 role: 'textbox', 361 name: 'my favorite textbox', 362 value: 'this is the inner content ', 363 }; 364 const snapshot = await page.accessibility.snapshot(); 365 expect(snapshot.children[0]).toEqual(golden); 366 }); 367 it('checkbox with and tabIndex and label should not have children', async () => { 368 const { page, isFirefox } = getTestState(); 369 370 await page.setContent(` 371 <div role="checkbox" tabIndex=0 aria-checked="true" aria-label="my favorite checkbox"> 372 this is the inner content 373 <img alt="yo" src="fakeimg.png"> 374 </div>`); 375 const golden = isFirefox 376 ? { 377 role: 'checkbutton', 378 name: 'my favorite checkbox', 379 checked: true, 380 } 381 : { 382 role: 'checkbox', 383 name: 'my favorite checkbox', 384 checked: true, 385 }; 386 const snapshot = await page.accessibility.snapshot(); 387 expect(snapshot.children[0]).toEqual(golden); 388 }); 389 it('checkbox without label should not have children', async () => { 390 const { page, isFirefox } = getTestState(); 391 392 await page.setContent(` 393 <div role="checkbox" aria-checked="true"> 394 this is the inner content 395 <img alt="yo" src="fakeimg.png"> 396 </div>`); 397 const golden = isFirefox 398 ? { 399 role: 'checkbutton', 400 name: 'this is the inner content yo', 401 checked: true, 402 } 403 : { 404 role: 'checkbox', 405 name: 'this is the inner content yo', 406 checked: true, 407 }; 408 const snapshot = await page.accessibility.snapshot(); 409 expect(snapshot.children[0]).toEqual(golden); 410 }); 411 412 describe('root option', function () { 413 it('should work a button', async () => { 414 const { page } = getTestState(); 415 416 await page.setContent(`<button>My Button</button>`); 417 418 const button = await page.$<HTMLButtonElement>('button'); 419 expect(await page.accessibility.snapshot({ root: button })).toEqual({ 420 role: 'button', 421 name: 'My Button', 422 }); 423 }); 424 it('should work an input', async () => { 425 const { page } = getTestState(); 426 427 await page.setContent(`<input title="My Input" value="My Value">`); 428 429 const input = await page.$('input'); 430 expect(await page.accessibility.snapshot({ root: input })).toEqual({ 431 role: 'textbox', 432 name: 'My Input', 433 value: 'My Value', 434 }); 435 }); 436 it('should work a menu', async () => { 437 const { page } = getTestState(); 438 439 await page.setContent(` 440 <div role="menu" title="My Menu"> 441 <div role="menuitem">First Item</div> 442 <div role="menuitem">Second Item</div> 443 <div role="menuitem">Third Item</div> 444 </div> 445 `); 446 447 const menu = await page.$('div[role="menu"]'); 448 expect(await page.accessibility.snapshot({ root: menu })).toEqual({ 449 role: 'menu', 450 name: 'My Menu', 451 children: [ 452 { role: 'menuitem', name: 'First Item' }, 453 { role: 'menuitem', name: 'Second Item' }, 454 { role: 'menuitem', name: 'Third Item' }, 455 ], 456 }); 457 }); 458 it('should return null when the element is no longer in DOM', async () => { 459 const { page } = getTestState(); 460 461 await page.setContent(`<button>My Button</button>`); 462 const button = await page.$('button'); 463 await page.$eval('button', (button) => button.remove()); 464 expect(await page.accessibility.snapshot({ root: button })).toEqual( 465 null 466 ); 467 }); 468 it('should support the interestingOnly option', async () => { 469 const { page } = getTestState(); 470 471 await page.setContent(`<div><button>My Button</button></div>`); 472 const div = await page.$('div'); 473 expect(await page.accessibility.snapshot({ root: div })).toEqual(null); 474 expect( 475 await page.accessibility.snapshot({ 476 root: div, 477 interestingOnly: false, 478 }) 479 ).toEqual({ 480 role: 'generic', 481 name: '', 482 children: [ 483 { 484 role: 'button', 485 name: 'My Button', 486 children: [{ role: 'StaticText', name: 'My Button' }], 487 }, 488 ], 489 }); 490 }); 491 }); 492 }); 493 function findFocusedNode(node) { 494 if (node.focused) return node; 495 for (const child of node.children || []) { 496 const focusedChild = findFocusedNode(child); 497 if (focusedChild) return focusedChild; 498 } 499 return null; 500 } 501}); 502