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