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 expect(snapshot.children[0].roledescription).toEqual('foo'); 172 }); 173 it('orientation', async () => { 174 const { page } = getTestState(); 175 176 await page.setContent( 177 '<a href="" role="slider" aria-orientation="vertical">11</a>' 178 ); 179 const snapshot = await page.accessibility.snapshot(); 180 expect(snapshot.children[0].orientation).toEqual('vertical'); 181 }); 182 it('autocomplete', async () => { 183 const { page } = getTestState(); 184 185 await page.setContent('<input type="number" aria-autocomplete="list" />'); 186 const snapshot = await page.accessibility.snapshot(); 187 expect(snapshot.children[0].autocomplete).toEqual('list'); 188 }); 189 it('multiselectable', async () => { 190 const { page } = getTestState(); 191 192 await page.setContent( 193 '<div role="grid" tabIndex=-1 aria-multiselectable=true>hey</div>' 194 ); 195 const snapshot = await page.accessibility.snapshot(); 196 expect(snapshot.children[0].multiselectable).toEqual(true); 197 }); 198 it('keyshortcuts', async () => { 199 const { page } = getTestState(); 200 201 await page.setContent( 202 '<div role="grid" tabIndex=-1 aria-keyshortcuts="foo">hey</div>' 203 ); 204 const snapshot = await page.accessibility.snapshot(); 205 expect(snapshot.children[0].keyshortcuts).toEqual('foo'); 206 }); 207 describe('filtering children of leaf nodes', function () { 208 it('should not report text nodes inside controls', async () => { 209 const { page, isFirefox } = getTestState(); 210 211 await page.setContent(` 212 <div role="tablist"> 213 <div role="tab" aria-selected="true"><b>Tab1</b></div> 214 <div role="tab">Tab2</div> 215 </div>`); 216 const golden = isFirefox 217 ? { 218 role: 'document', 219 name: '', 220 children: [ 221 { 222 role: 'pagetab', 223 name: 'Tab1', 224 selected: true, 225 }, 226 { 227 role: 'pagetab', 228 name: 'Tab2', 229 }, 230 ], 231 } 232 : { 233 role: 'RootWebArea', 234 name: '', 235 children: [ 236 { 237 role: 'tab', 238 name: 'Tab1', 239 selected: true, 240 }, 241 { 242 role: 'tab', 243 name: 'Tab2', 244 }, 245 ], 246 }; 247 expect(await page.accessibility.snapshot()).toEqual(golden); 248 }); 249 it('rich text editable fields should have children', async () => { 250 const { page, isFirefox } = getTestState(); 251 252 await page.setContent(` 253 <div contenteditable="true"> 254 Edit this image: <img src="fakeimage.png" alt="my fake image"> 255 </div>`); 256 const golden = isFirefox 257 ? { 258 role: 'section', 259 name: '', 260 children: [ 261 { 262 role: 'text leaf', 263 name: 'Edit this image: ', 264 }, 265 { 266 role: 'StaticText', 267 name: 'my fake image', 268 }, 269 ], 270 } 271 : { 272 role: 'generic', 273 name: '', 274 value: 'Edit this image: ', 275 children: [ 276 { 277 role: 'StaticText', 278 name: 'Edit this image:', 279 }, 280 { 281 role: 'img', 282 name: 'my fake image', 283 }, 284 ], 285 }; 286 const snapshot = await page.accessibility.snapshot(); 287 expect(snapshot.children[0]).toEqual(golden); 288 }); 289 it('rich text editable fields with role should have children', async () => { 290 const { page, isFirefox } = getTestState(); 291 292 await page.setContent(` 293 <div contenteditable="true" role='textbox'> 294 Edit this image: <img src="fakeimage.png" alt="my fake image"> 295 </div>`); 296 const golden = isFirefox 297 ? { 298 role: 'entry', 299 name: '', 300 value: 'Edit this image: my fake image', 301 children: [ 302 { 303 role: 'StaticText', 304 name: 'my fake image', 305 }, 306 ], 307 } 308 : { 309 role: 'textbox', 310 name: '', 311 value: 'Edit this image: ', 312 multiline: true, 313 children: [ 314 { 315 role: 'StaticText', 316 name: 'Edit this image:', 317 }, 318 { 319 role: 'img', 320 name: 'my fake image', 321 }, 322 ], 323 }; 324 const snapshot = await page.accessibility.snapshot(); 325 expect(snapshot.children[0]).toEqual(golden); 326 }); 327 328 // Firefox does not support contenteditable="plaintext-only". 329 describeFailsFirefox('plaintext contenteditable', function () { 330 it('plain text field with role should not have children', async () => { 331 const { page } = getTestState(); 332 333 await page.setContent(` 334 <div contenteditable="plaintext-only" role='textbox'>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`); 335 const snapshot = await page.accessibility.snapshot(); 336 expect(snapshot.children[0]).toEqual({ 337 role: 'textbox', 338 name: '', 339 value: 'Edit this image:', 340 multiline: true, 341 }); 342 }); 343 }); 344 it('non editable textbox with role and tabIndex and label should not have children', async () => { 345 const { page, isFirefox } = getTestState(); 346 347 await page.setContent(` 348 <div role="textbox" tabIndex=0 aria-checked="true" aria-label="my favorite textbox"> 349 this is the inner content 350 <img alt="yo" src="fakeimg.png"> 351 </div>`); 352 const golden = isFirefox 353 ? { 354 role: 'entry', 355 name: 'my favorite textbox', 356 value: 'this is the inner content yo', 357 } 358 : { 359 role: 'textbox', 360 name: 'my favorite textbox', 361 value: 'this is the inner content ', 362 }; 363 const snapshot = await page.accessibility.snapshot(); 364 expect(snapshot.children[0]).toEqual(golden); 365 }); 366 it('checkbox with and tabIndex and label should not have children', async () => { 367 const { page, isFirefox } = getTestState(); 368 369 await page.setContent(` 370 <div role="checkbox" tabIndex=0 aria-checked="true" aria-label="my favorite checkbox"> 371 this is the inner content 372 <img alt="yo" src="fakeimg.png"> 373 </div>`); 374 const golden = isFirefox 375 ? { 376 role: 'checkbutton', 377 name: 'my favorite checkbox', 378 checked: true, 379 } 380 : { 381 role: 'checkbox', 382 name: 'my favorite checkbox', 383 checked: true, 384 }; 385 const snapshot = await page.accessibility.snapshot(); 386 expect(snapshot.children[0]).toEqual(golden); 387 }); 388 it('checkbox without label should not have children', async () => { 389 const { page, isFirefox } = getTestState(); 390 391 await page.setContent(` 392 <div role="checkbox" aria-checked="true"> 393 this is the inner content 394 <img alt="yo" src="fakeimg.png"> 395 </div>`); 396 const golden = isFirefox 397 ? { 398 role: 'checkbutton', 399 name: 'this is the inner content yo', 400 checked: true, 401 } 402 : { 403 role: 'checkbox', 404 name: 'this is the inner content yo', 405 checked: true, 406 }; 407 const snapshot = await page.accessibility.snapshot(); 408 expect(snapshot.children[0]).toEqual(golden); 409 }); 410 411 describe('root option', function () { 412 it('should work a button', async () => { 413 const { page } = getTestState(); 414 415 await page.setContent(`<button>My Button</button>`); 416 417 const button = await page.$<HTMLButtonElement>('button'); 418 expect(await page.accessibility.snapshot({ root: button })).toEqual({ 419 role: 'button', 420 name: 'My Button', 421 }); 422 }); 423 it('should work an input', async () => { 424 const { page } = getTestState(); 425 426 await page.setContent(`<input title="My Input" value="My Value">`); 427 428 const input = await page.$('input'); 429 expect(await page.accessibility.snapshot({ root: input })).toEqual({ 430 role: 'textbox', 431 name: 'My Input', 432 value: 'My Value', 433 }); 434 }); 435 it('should work a menu', async () => { 436 const { page } = getTestState(); 437 438 await page.setContent(` 439 <div role="menu" title="My Menu"> 440 <div role="menuitem">First Item</div> 441 <div role="menuitem">Second Item</div> 442 <div role="menuitem">Third Item</div> 443 </div> 444 `); 445 446 const menu = await page.$('div[role="menu"]'); 447 expect(await page.accessibility.snapshot({ root: menu })).toEqual({ 448 role: 'menu', 449 name: 'My Menu', 450 children: [ 451 { role: 'menuitem', name: 'First Item' }, 452 { role: 'menuitem', name: 'Second Item' }, 453 { role: 'menuitem', name: 'Third Item' }, 454 ], 455 }); 456 }); 457 it('should return null when the element is no longer in DOM', async () => { 458 const { page } = getTestState(); 459 460 await page.setContent(`<button>My Button</button>`); 461 const button = await page.$('button'); 462 await page.$eval('button', (button) => button.remove()); 463 expect(await page.accessibility.snapshot({ root: button })).toEqual( 464 null 465 ); 466 }); 467 it('should support the interestingOnly option', async () => { 468 const { page } = getTestState(); 469 470 await page.setContent(`<div><button>My Button</button></div>`); 471 const div = await page.$('div'); 472 expect(await page.accessibility.snapshot({ root: div })).toEqual(null); 473 expect( 474 await page.accessibility.snapshot({ 475 root: div, 476 interestingOnly: false, 477 }) 478 ).toEqual({ 479 role: 'generic', 480 name: '', 481 children: [ 482 { 483 role: 'button', 484 name: 'My Button', 485 children: [{ role: 'StaticText', name: 'My Button' }], 486 }, 487 ], 488 }); 489 }); 490 }); 491 }); 492 function findFocusedNode(node) { 493 if (node.focused) return node; 494 for (const child of node.children || []) { 495 const focusedChild = findFocusedNode(child); 496 if (focusedChild) return focusedChild; 497 } 498 return null; 499 } 500}); 501