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