1/** 2 * Copyright 2020 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 describeChromeOnly, 23} from './mocha-utils'; // eslint-disable-line import/extensions 24 25import { ElementHandle } from '../lib/cjs/puppeteer/common/JSHandle.js'; 26import utils from './utils.js'; 27 28describeChromeOnly('AriaQueryHandler', () => { 29 setupTestBrowserHooks(); 30 setupTestPageAndContextHooks(); 31 32 describe('parseAriaSelector', () => { 33 beforeEach(async () => { 34 const { page } = getTestState(); 35 await page.setContent( 36 '<button id="btn" role="button"> Submit button and some spaces </button>' 37 ); 38 }); 39 it('should find button', async () => { 40 const { page } = getTestState(); 41 const expectFound = async (button: ElementHandle) => { 42 const id = await button.evaluate((button: Element) => button.id); 43 expect(id).toBe('btn'); 44 }; 45 let button = await page.$( 46 'aria/Submit button and some spaces[role="button"]' 47 ); 48 await expectFound(button); 49 button = await page.$( 50 "aria/Submit button and some spaces[role='button']" 51 ); 52 await expectFound(button); 53 button = await page.$( 54 'aria/ Submit button and some spaces[role="button"]' 55 ); 56 await expectFound(button); 57 button = await page.$( 58 'aria/Submit button and some spaces [role="button"]' 59 ); 60 await expectFound(button); 61 button = await page.$( 62 'aria/Submit button and some spaces [ role = "button" ] ' 63 ); 64 await expectFound(button); 65 button = await page.$( 66 'aria/[role="button"]Submit button and some spaces' 67 ); 68 await expectFound(button); 69 button = await page.$( 70 'aria/Submit button [role="button"]and some spaces' 71 ); 72 await expectFound(button); 73 button = await page.$( 74 'aria/[name=" Submit button and some spaces"][role="button"]' 75 ); 76 await expectFound(button); 77 button = await page.$( 78 "aria/[name=' Submit button and some spaces'][role='button']" 79 ); 80 await expectFound(button); 81 button = await page.$( 82 'aria/ignored[name="Submit button and some spaces"][role="button"]' 83 ); 84 await expectFound(button); 85 await expect(page.$('aria/smth[smth="true"]')).rejects.toThrow( 86 'Unknown aria attribute "smth" in selector' 87 ); 88 }); 89 }); 90 91 describe('queryOne', () => { 92 it('should find button by role', async () => { 93 const { page } = getTestState(); 94 await page.setContent( 95 '<div id="div"><button id="btn" role="button">Submit</button></div>' 96 ); 97 const button = await page.$('aria/[role="button"]'); 98 const id = await button.evaluate((button: Element) => button.id); 99 expect(id).toBe('btn'); 100 }); 101 102 it('should find button by name and role', async () => { 103 const { page } = getTestState(); 104 await page.setContent( 105 '<div id="div"><button id="btn" role="button">Submit</button></div>' 106 ); 107 const button = await page.$('aria/Submit[role="button"]'); 108 const id = await button.evaluate((button: Element) => button.id); 109 expect(id).toBe('btn'); 110 }); 111 112 it('should find first matching element', async () => { 113 const { page } = getTestState(); 114 await page.setContent( 115 ` 116 <div role="menu" id="mnu1" aria-label="menu div"></div> 117 <div role="menu" id="mnu2" aria-label="menu div"></div> 118 ` 119 ); 120 const div = await page.$('aria/menu div'); 121 const id = await div.evaluate((div: Element) => div.id); 122 expect(id).toBe('mnu1'); 123 }); 124 125 it('should find by name', async () => { 126 const { page } = getTestState(); 127 await page.setContent( 128 ` 129 <div role="menu" id="mnu1" aria-label="menu-label1">menu div</div> 130 <div role="menu" id="mnu2" aria-label="menu-label2">menu div</div> 131 ` 132 ); 133 const menu = await page.$('aria/menu-label1'); 134 const id = await menu.evaluate((div: Element) => div.id); 135 expect(id).toBe('mnu1'); 136 }); 137 138 it('should find by name', async () => { 139 const { page } = getTestState(); 140 await page.setContent( 141 ` 142 <div role="menu" id="mnu1" aria-label="menu-label1">menu div</div> 143 <div role="menu" id="mnu2" aria-label="menu-label2">menu div</div> 144 ` 145 ); 146 const menu = await page.$('aria/menu-label2'); 147 const id = await menu.evaluate((div: Element) => div.id); 148 expect(id).toBe('mnu2'); 149 }); 150 }); 151 152 describe('queryAll', () => { 153 it('should find menu by name', async () => { 154 const { page } = getTestState(); 155 await page.setContent( 156 ` 157 <div role="menu" id="mnu1" aria-label="menu div"></div> 158 <div role="menu" id="mnu2" aria-label="menu div"></div> 159 ` 160 ); 161 const divs = await page.$$('aria/menu div'); 162 const ids = await Promise.all( 163 divs.map((n) => n.evaluate((div: Element) => div.id)) 164 ); 165 expect(ids.join(', ')).toBe('mnu1, mnu2'); 166 }); 167 }); 168 describe('queryAllArray', () => { 169 it('$$eval should handle many elements', async () => { 170 const { page } = getTestState(); 171 await page.setContent(''); 172 await page.evaluate( 173 ` 174 for (var i = 0; i <= 10000; i++) { 175 const button = document.createElement('button'); 176 button.textContent = i; 177 document.body.appendChild(button); 178 } 179 ` 180 ); 181 const sum = await page.$$eval('aria/[role="button"]', (buttons) => 182 buttons.reduce((acc, button) => acc + Number(button.textContent), 0) 183 ); 184 expect(sum).toBe(50005000); 185 }); 186 }); 187 188 describe('waitForSelector (aria)', function () { 189 const addElement = (tag) => 190 document.body.appendChild(document.createElement(tag)); 191 192 it('should immediately resolve promise if node exists', async () => { 193 const { page, server } = getTestState(); 194 await page.goto(server.EMPTY_PAGE); 195 await page.evaluate(addElement, 'button'); 196 await page.waitForSelector('aria/[role="button"]'); 197 }); 198 199 it('should work for ElementHandler.waitForSelector', async () => { 200 const { page, server } = getTestState(); 201 await page.goto(server.EMPTY_PAGE); 202 await page.evaluate( 203 () => (document.body.innerHTML = `<div><button>test</button></div>`) 204 ); 205 const element = await page.$('div'); 206 await element.waitForSelector('aria/test'); 207 }); 208 209 it('should persist query handler bindings across reloads', async () => { 210 const { page, server } = getTestState(); 211 await page.goto(server.EMPTY_PAGE); 212 await page.evaluate(addElement, 'button'); 213 await page.waitForSelector('aria/[role="button"]'); 214 await page.reload(); 215 await page.evaluate(addElement, 'button'); 216 await page.waitForSelector('aria/[role="button"]'); 217 }); 218 219 it('should persist query handler bindings across navigations', async () => { 220 const { page, server } = getTestState(); 221 222 // Reset page but make sure that execution context ids start with 1. 223 await page.goto('data:text/html,'); 224 await page.goto(server.EMPTY_PAGE); 225 await page.evaluate(addElement, 'button'); 226 await page.waitForSelector('aria/[role="button"]'); 227 228 // Reset page but again make sure that execution context ids start with 1. 229 await page.goto('data:text/html,'); 230 await page.goto(server.EMPTY_PAGE); 231 await page.evaluate(addElement, 'button'); 232 await page.waitForSelector('aria/[role="button"]'); 233 }); 234 235 it('should work independently of `exposeFunction`', async () => { 236 const { page, server } = getTestState(); 237 await page.goto(server.EMPTY_PAGE); 238 await page.exposeFunction('ariaQuerySelector', (a, b) => a + b); 239 await page.evaluate(addElement, 'button'); 240 await page.waitForSelector('aria/[role="button"]'); 241 const result = await page.evaluate('globalThis.ariaQuerySelector(2,8)'); 242 expect(result).toBe(10); 243 }); 244 245 it('should work with removed MutationObserver', async () => { 246 const { page } = getTestState(); 247 248 await page.evaluate(() => delete window.MutationObserver); 249 const [handle] = await Promise.all([ 250 page.waitForSelector('aria/anything'), 251 page.setContent(`<h1>anything</h1>`), 252 ]); 253 expect( 254 await page.evaluate((x: HTMLElement) => x.textContent, handle) 255 ).toBe('anything'); 256 }); 257 258 it('should resolve promise when node is added', async () => { 259 const { page, server } = getTestState(); 260 261 await page.goto(server.EMPTY_PAGE); 262 const frame = page.mainFrame(); 263 const watchdog = frame.waitForSelector('aria/[role="heading"]'); 264 await frame.evaluate(addElement, 'br'); 265 await frame.evaluate(addElement, 'h1'); 266 const elementHandle = await watchdog; 267 const tagName = await elementHandle 268 .getProperty('tagName') 269 .then((element) => element.jsonValue()); 270 expect(tagName).toBe('H1'); 271 }); 272 273 it('should work when node is added through innerHTML', async () => { 274 const { page, server } = getTestState(); 275 276 await page.goto(server.EMPTY_PAGE); 277 const watchdog = page.waitForSelector('aria/name'); 278 await page.evaluate(addElement, 'span'); 279 await page.evaluate( 280 () => 281 (document.querySelector('span').innerHTML = 282 '<h3><div aria-label="name"></div></h3>') 283 ); 284 await watchdog; 285 }); 286 287 it('Page.waitForSelector is shortcut for main frame', async () => { 288 const { page, server } = getTestState(); 289 290 await page.goto(server.EMPTY_PAGE); 291 await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); 292 const otherFrame = page.frames()[1]; 293 const watchdog = page.waitForSelector('aria/[role="button"]'); 294 await otherFrame.evaluate(addElement, 'button'); 295 await page.evaluate(addElement, 'button'); 296 const elementHandle = await watchdog; 297 expect(elementHandle.executionContext().frame()).toBe(page.mainFrame()); 298 }); 299 300 it('should run in specified frame', async () => { 301 const { page, server } = getTestState(); 302 303 await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); 304 await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE); 305 const frame1 = page.frames()[1]; 306 const frame2 = page.frames()[2]; 307 const waitForSelectorPromise = frame2.waitForSelector( 308 'aria/[role="button"]' 309 ); 310 await frame1.evaluate(addElement, 'button'); 311 await frame2.evaluate(addElement, 'button'); 312 const elementHandle = await waitForSelectorPromise; 313 expect(elementHandle.executionContext().frame()).toBe(frame2); 314 }); 315 316 it('should throw when frame is detached', async () => { 317 const { page, server } = getTestState(); 318 319 await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); 320 const frame = page.frames()[1]; 321 let waitError = null; 322 const waitPromise = frame 323 .waitForSelector('aria/does-not-exist') 324 .catch((error) => (waitError = error)); 325 await utils.detachFrame(page, 'frame1'); 326 await waitPromise; 327 expect(waitError).toBeTruthy(); 328 expect(waitError.message).toContain( 329 'waitForFunction failed: frame got detached.' 330 ); 331 }); 332 333 it('should survive cross-process navigation', async () => { 334 const { page, server } = getTestState(); 335 336 let imgFound = false; 337 const waitForSelector = page 338 .waitForSelector('aria/[role="img"]') 339 .then(() => (imgFound = true)); 340 await page.goto(server.EMPTY_PAGE); 341 expect(imgFound).toBe(false); 342 await page.reload(); 343 expect(imgFound).toBe(false); 344 await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html'); 345 await waitForSelector; 346 expect(imgFound).toBe(true); 347 }); 348 349 it('should wait for visible', async () => { 350 const { page } = getTestState(); 351 352 let divFound = false; 353 const waitForSelector = page 354 .waitForSelector('aria/name', { visible: true }) 355 .then(() => (divFound = true)); 356 await page.setContent( 357 `<div aria-label='name' style='display: none; visibility: hidden;'>1</div>` 358 ); 359 expect(divFound).toBe(false); 360 await page.evaluate(() => 361 document.querySelector('div').style.removeProperty('display') 362 ); 363 expect(divFound).toBe(false); 364 await page.evaluate(() => 365 document.querySelector('div').style.removeProperty('visibility') 366 ); 367 expect(await waitForSelector).toBe(true); 368 expect(divFound).toBe(true); 369 }); 370 371 it('should wait for visible recursively', async () => { 372 const { page } = getTestState(); 373 374 let divVisible = false; 375 const waitForSelector = page 376 .waitForSelector('aria/inner', { visible: true }) 377 .then(() => (divVisible = true)); 378 await page.setContent( 379 `<div style='display: none; visibility: hidden;'><div aria-label="inner">hi</div></div>` 380 ); 381 expect(divVisible).toBe(false); 382 await page.evaluate(() => 383 document.querySelector('div').style.removeProperty('display') 384 ); 385 expect(divVisible).toBe(false); 386 await page.evaluate(() => 387 document.querySelector('div').style.removeProperty('visibility') 388 ); 389 expect(await waitForSelector).toBe(true); 390 expect(divVisible).toBe(true); 391 }); 392 393 it('hidden should wait for visibility: hidden', async () => { 394 const { page } = getTestState(); 395 396 let divHidden = false; 397 await page.setContent( 398 `<div role='button' style='display: block;'></div>` 399 ); 400 const waitForSelector = page 401 .waitForSelector('aria/[role="button"]', { hidden: true }) 402 .then(() => (divHidden = true)); 403 await page.waitForSelector('aria/[role="button"]'); // do a round trip 404 expect(divHidden).toBe(false); 405 await page.evaluate(() => 406 document.querySelector('div').style.setProperty('visibility', 'hidden') 407 ); 408 expect(await waitForSelector).toBe(true); 409 expect(divHidden).toBe(true); 410 }); 411 412 it('hidden should wait for display: none', async () => { 413 const { page } = getTestState(); 414 415 let divHidden = false; 416 await page.setContent(`<div role='main' style='display: block;'></div>`); 417 const waitForSelector = page 418 .waitForSelector('aria/[role="main"]', { hidden: true }) 419 .then(() => (divHidden = true)); 420 await page.waitForSelector('aria/[role="main"]'); // do a round trip 421 expect(divHidden).toBe(false); 422 await page.evaluate(() => 423 document.querySelector('div').style.setProperty('display', 'none') 424 ); 425 expect(await waitForSelector).toBe(true); 426 expect(divHidden).toBe(true); 427 }); 428 429 it('hidden should wait for removal', async () => { 430 const { page } = getTestState(); 431 432 await page.setContent(`<div role='main'></div>`); 433 let divRemoved = false; 434 const waitForSelector = page 435 .waitForSelector('aria/[role="main"]', { hidden: true }) 436 .then(() => (divRemoved = true)); 437 await page.waitForSelector('aria/[role="main"]'); // do a round trip 438 expect(divRemoved).toBe(false); 439 await page.evaluate(() => document.querySelector('div').remove()); 440 expect(await waitForSelector).toBe(true); 441 expect(divRemoved).toBe(true); 442 }); 443 444 it('should return null if waiting to hide non-existing element', async () => { 445 const { page } = getTestState(); 446 447 const handle = await page.waitForSelector('aria/non-existing', { 448 hidden: true, 449 }); 450 expect(handle).toBe(null); 451 }); 452 453 it('should respect timeout', async () => { 454 const { page, puppeteer } = getTestState(); 455 456 let error = null; 457 await page 458 .waitForSelector('aria/[role="button"]', { timeout: 10 }) 459 .catch((error_) => (error = error_)); 460 expect(error).toBeTruthy(); 461 expect(error.message).toContain( 462 'waiting for selector `[role="button"]` failed: timeout' 463 ); 464 expect(error).toBeInstanceOf(puppeteer.errors.TimeoutError); 465 }); 466 467 it('should have an error message specifically for awaiting an element to be hidden', async () => { 468 const { page } = getTestState(); 469 470 await page.setContent(`<div role='main'></div>`); 471 let error = null; 472 await page 473 .waitForSelector('aria/[role="main"]', { hidden: true, timeout: 10 }) 474 .catch((error_) => (error = error_)); 475 expect(error).toBeTruthy(); 476 expect(error.message).toContain( 477 'waiting for selector `[role="main"]` to be hidden failed: timeout' 478 ); 479 }); 480 481 it('should respond to node attribute mutation', async () => { 482 const { page } = getTestState(); 483 484 let divFound = false; 485 const waitForSelector = page 486 .waitForSelector('aria/zombo') 487 .then(() => (divFound = true)); 488 await page.setContent(`<div aria-label='notZombo'></div>`); 489 expect(divFound).toBe(false); 490 await page.evaluate(() => 491 document.querySelector('div').setAttribute('aria-label', 'zombo') 492 ); 493 expect(await waitForSelector).toBe(true); 494 }); 495 496 it('should return the element handle', async () => { 497 const { page } = getTestState(); 498 499 const waitForSelector = page.waitForSelector('aria/zombo'); 500 await page.setContent(`<div aria-label='zombo'>anything</div>`); 501 expect( 502 await page.evaluate( 503 (x: HTMLElement) => x.textContent, 504 await waitForSelector 505 ) 506 ).toBe('anything'); 507 }); 508 509 it('should have correct stack trace for timeout', async () => { 510 const { page } = getTestState(); 511 512 let error; 513 await page 514 .waitForSelector('aria/zombo', { timeout: 10 }) 515 .catch((error_) => (error = error_)); 516 expect(error.stack).toContain('waiting for selector `zombo` failed'); 517 }); 518 }); 519 520 describe('queryOne (Chromium web test)', async () => { 521 beforeEach(async () => { 522 const { page } = getTestState(); 523 await page.setContent( 524 ` 525 <h2 id="shown">title</h2> 526 <h2 id="hidden" aria-hidden="true">title</h2> 527 <div id="node1" aria-labeledby="node2"></div> 528 <div id="node2" aria-label="bar"></div> 529 <div id="node3" aria-label="foo"></div> 530 <div id="node4" class="container"> 531 <div id="node5" role="button" aria-label="foo"></div> 532 <div id="node6" role="button" aria-label="foo"></div> 533 <!-- Accessible name not available when element is hidden --> 534 <div id="node7" hidden role="button" aria-label="foo"></div> 535 <div id="node8" role="button" aria-label="bar"></div> 536 </div> 537 <button id="node10">text content</button> 538 <h1 id="node11">text content</h1> 539 <!-- Accessible name not available when role is "presentation" --> 540 <h1 id="node12" role="presentation">text content</h1> 541 <!-- Elements inside shadow dom should be found --> 542 <script> 543 const div = document.createElement('div'); 544 const shadowRoot = div.attachShadow({mode: 'open'}); 545 const h1 = document.createElement('h1'); 546 h1.textContent = 'text content'; 547 h1.id = 'node13'; 548 shadowRoot.appendChild(h1); 549 document.documentElement.appendChild(div); 550 </script> 551 <img id="node20" src="" alt="Accessible Name"> 552 <input id="node21" type="submit" value="Accessible Name"> 553 <label id="node22" for="node23">Accessible Name</label> 554 <!-- Accessible name for the <input> is "Accessible Name" --> 555 <input id="node23"> 556 <div id="node24" title="Accessible Name"></div> 557 <div role="treeitem" id="node30"> 558 <div role="treeitem" id="node31"> 559 <div role="treeitem" id="node32">item1</div> 560 <div role="treeitem" id="node33">item2</div> 561 </div> 562 <div role="treeitem" id="node34">item3</div> 563 </div> 564 <!-- Accessible name for the <div> is "item1 item2 item3" --> 565 <div aria-describedby="node30"></div> 566 ` 567 ); 568 }); 569 const getIds = async (elements: ElementHandle[]) => 570 Promise.all( 571 elements.map((element) => 572 element.evaluate((element: Element) => element.id) 573 ) 574 ); 575 it('should find by name "foo"', async () => { 576 const { page } = getTestState(); 577 const found = await page.$$('aria/foo'); 578 const ids = await getIds(found); 579 expect(ids).toEqual(['node3', 'node5', 'node6']); 580 }); 581 it('should find by name "bar"', async () => { 582 const { page } = getTestState(); 583 const found = await page.$$('aria/bar'); 584 const ids = await getIds(found); 585 expect(ids).toEqual(['node1', 'node2', 'node8']); 586 }); 587 it('should find treeitem by name', async () => { 588 const { page } = getTestState(); 589 const found = await page.$$('aria/item1 item2 item3'); 590 const ids = await getIds(found); 591 expect(ids).toEqual(['node30']); 592 }); 593 it('should find by role "button"', async () => { 594 const { page } = getTestState(); 595 const found = await page.$$<HTMLButtonElement>('aria/[role="button"]'); 596 const ids = await getIds(found); 597 expect(ids).toEqual(['node5', 'node6', 'node8', 'node10', 'node21']); 598 }); 599 it('should find by role "heading"', async () => { 600 const { page } = getTestState(); 601 const found = await page.$$('aria/[role="heading"]'); 602 const ids = await getIds(found); 603 expect(ids).toEqual(['shown', 'hidden', 'node11', 'node13']); 604 }); 605 it('should find both ignored and unignored', async () => { 606 const { page } = getTestState(); 607 const found = await page.$$('aria/title'); 608 const ids = await getIds(found); 609 expect(ids).toEqual(['shown', 'hidden']); 610 }); 611 }); 612}); 613