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