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 */
16import expect from 'expect';
17import {
18  getTestState,
19  setupTestBrowserHooks,
20  setupTestPageAndContextHooks,
21} from './mocha-utils'; // eslint-disable-line import/extensions
22import { CustomQueryHandler } from '../lib/cjs/puppeteer/common/QueryHandler.js';
23
24describe('querySelector', function () {
25  setupTestBrowserHooks();
26  setupTestPageAndContextHooks();
27  describe('Page.$eval', function () {
28    it('should work', async () => {
29      const { page } = getTestState();
30
31      await page.setContent('<section id="testAttribute">43543</section>');
32      const idAttribute = await page.$eval('section', (e) => e.id);
33      expect(idAttribute).toBe('testAttribute');
34    });
35    it('should accept arguments', async () => {
36      const { page } = getTestState();
37
38      await page.setContent('<section>hello</section>');
39      const text = await page.$eval(
40        'section',
41        (e, suffix) => e.textContent + suffix,
42        ' world!'
43      );
44      expect(text).toBe('hello world!');
45    });
46    it('should accept ElementHandles as arguments', async () => {
47      const { page } = getTestState();
48
49      await page.setContent('<section>hello</section><div> world</div>');
50      const divHandle = await page.$('div');
51      const text = await page.$eval(
52        'section',
53        (e, div: HTMLElement) => e.textContent + div.textContent,
54        divHandle
55      );
56      expect(text).toBe('hello world');
57    });
58    it('should throw error if no element is found', async () => {
59      const { page } = getTestState();
60
61      let error = null;
62      await page
63        .$eval('section', (e) => e.id)
64        .catch((error_) => (error = error_));
65      expect(error.message).toContain(
66        'failed to find element matching selector "section"'
67      );
68    });
69  });
70
71  describe('pierceHandler', function () {
72    beforeEach(async () => {
73      const { page } = getTestState();
74      await page.setContent(
75        `<script>
76        const div = document.createElement('div');
77        const shadowRoot = div.attachShadow({mode: 'open'});
78        const div1 = document.createElement('div');
79        div1.textContent = 'Hello';
80        div1.className = 'foo';
81        const div2 = document.createElement('div');
82        div2.textContent = 'World';
83        div2.className = 'foo';
84        shadowRoot.appendChild(div1);
85        shadowRoot.appendChild(div2);
86        document.documentElement.appendChild(div);
87        </script>`
88      );
89    });
90    it('should find first element in shadow', async () => {
91      const { page } = getTestState();
92      const div = await page.$('pierce/.foo');
93      const text = await div.evaluate(
94        (element: Element) => element.textContent
95      );
96      expect(text).toBe('Hello');
97    });
98    it('should find all elements in shadow', async () => {
99      const { page } = getTestState();
100      const divs = await page.$$('pierce/.foo');
101      const text = await Promise.all(
102        divs.map((div) =>
103          div.evaluate((element: Element) => element.textContent)
104        )
105      );
106      expect(text.join(' ')).toBe('Hello World');
107    });
108  });
109
110  // The tests for $$eval are repeated later in this file in the test group 'QueryAll'.
111  // This is done to also test a query handler where QueryAll returns an Element[]
112  // as opposed to NodeListOf<Element>.
113  describe('Page.$$eval', function () {
114    it('should work', async () => {
115      const { page } = getTestState();
116
117      await page.setContent(
118        '<div>hello</div><div>beautiful</div><div>world!</div>'
119      );
120      const divsCount = await page.$$eval('div', (divs) => divs.length);
121      expect(divsCount).toBe(3);
122    });
123    it('should accept extra arguments', async () => {
124      const { page } = getTestState();
125      await page.setContent(
126        '<div>hello</div><div>beautiful</div><div>world!</div>'
127      );
128      const divsCountPlus5 = await page.$$eval(
129        'div',
130        (divs, two: number, three: number) => divs.length + two + three,
131        2,
132        3
133      );
134      expect(divsCountPlus5).toBe(8);
135    });
136    it('should accept ElementHandles as arguments', async () => {
137      const { page } = getTestState();
138      await page.setContent(
139        '<section>2</section><section>2</section><section>1</section><div>3</div>'
140      );
141      const divHandle = await page.$('div');
142      const sum = await page.$$eval(
143        'section',
144        (sections, div: HTMLElement) =>
145          sections.reduce(
146            (acc, section) => acc + Number(section.textContent),
147            0
148          ) + Number(div.textContent),
149        divHandle
150      );
151      expect(sum).toBe(8);
152    });
153    it('should handle many elements', async () => {
154      const { page } = getTestState();
155      await page.evaluate(
156        `
157        for (var i = 0; i <= 1000; i++) {
158            const section = document.createElement('section');
159            section.textContent = i;
160            document.body.appendChild(section);
161        }
162        `
163      );
164      const sum = await page.$$eval('section', (sections) =>
165        sections.reduce((acc, section) => acc + Number(section.textContent), 0)
166      );
167      expect(sum).toBe(500500);
168    });
169  });
170
171  describe('Page.$', function () {
172    it('should query existing element', async () => {
173      const { page } = getTestState();
174
175      await page.setContent('<section>test</section>');
176      const element = await page.$('section');
177      expect(element).toBeTruthy();
178    });
179    it('should return null for non-existing element', async () => {
180      const { page } = getTestState();
181
182      const element = await page.$('non-existing-element');
183      expect(element).toBe(null);
184    });
185  });
186
187  describe('Page.$$', function () {
188    it('should query existing elements', async () => {
189      const { page } = getTestState();
190
191      await page.setContent('<div>A</div><br/><div>B</div>');
192      const elements = await page.$$('div');
193      expect(elements.length).toBe(2);
194      const promises = elements.map((element) =>
195        page.evaluate((e: HTMLElement) => e.textContent, element)
196      );
197      expect(await Promise.all(promises)).toEqual(['A', 'B']);
198    });
199    it('should return empty array if nothing is found', async () => {
200      const { page, server } = getTestState();
201
202      await page.goto(server.EMPTY_PAGE);
203      const elements = await page.$$('div');
204      expect(elements.length).toBe(0);
205    });
206  });
207
208  describe('Path.$x', function () {
209    it('should query existing element', async () => {
210      const { page } = getTestState();
211
212      await page.setContent('<section>test</section>');
213      const elements = await page.$x('/html/body/section');
214      expect(elements[0]).toBeTruthy();
215      expect(elements.length).toBe(1);
216    });
217    it('should return empty array for non-existing element', async () => {
218      const { page } = getTestState();
219
220      const element = await page.$x('/html/body/non-existing-element');
221      expect(element).toEqual([]);
222    });
223    it('should return multiple elements', async () => {
224      const { page } = getTestState();
225
226      await page.setContent('<div></div><div></div>');
227      const elements = await page.$x('/html/body/div');
228      expect(elements.length).toBe(2);
229    });
230  });
231
232  describe('ElementHandle.$', function () {
233    it('should query existing element', async () => {
234      const { page, server } = getTestState();
235
236      await page.goto(server.PREFIX + '/playground.html');
237      await page.setContent(
238        '<html><body><div class="second"><div class="inner">A</div></div></body></html>'
239      );
240      const html = await page.$('html');
241      const second = await html.$('.second');
242      const inner = await second.$('.inner');
243      const content = await page.evaluate(
244        (e: HTMLElement) => e.textContent,
245        inner
246      );
247      expect(content).toBe('A');
248    });
249
250    it('should return null for non-existing element', async () => {
251      const { page } = getTestState();
252
253      await page.setContent(
254        '<html><body><div class="second"><div class="inner">B</div></div></body></html>'
255      );
256      const html = await page.$('html');
257      const second = await html.$('.third');
258      expect(second).toBe(null);
259    });
260  });
261  describe('ElementHandle.$eval', function () {
262    it('should work', async () => {
263      const { page } = getTestState();
264
265      await page.setContent(
266        '<html><body><div class="tweet"><div class="like">100</div><div class="retweets">10</div></div></body></html>'
267      );
268      const tweet = await page.$('.tweet');
269      const content = await tweet.$eval(
270        '.like',
271        (node: HTMLElement) => node.innerText
272      );
273      expect(content).toBe('100');
274    });
275
276    it('should retrieve content from subtree', async () => {
277      const { page } = getTestState();
278
279      const htmlContent =
280        '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a-child-div</div></div>';
281      await page.setContent(htmlContent);
282      const elementHandle = await page.$('#myId');
283      const content = await elementHandle.$eval(
284        '.a',
285        (node: HTMLElement) => node.innerText
286      );
287      expect(content).toBe('a-child-div');
288    });
289
290    it('should throw in case of missing selector', async () => {
291      const { page } = getTestState();
292
293      const htmlContent =
294        '<div class="a">not-a-child-div</div><div id="myId"></div>';
295      await page.setContent(htmlContent);
296      const elementHandle = await page.$('#myId');
297      const errorMessage = await elementHandle
298        .$eval('.a', (node: HTMLElement) => node.innerText)
299        .catch((error) => error.message);
300      expect(errorMessage).toBe(
301        `Error: failed to find element matching selector ".a"`
302      );
303    });
304  });
305  describe('ElementHandle.$$eval', function () {
306    it('should work', async () => {
307      const { page } = getTestState();
308
309      await page.setContent(
310        '<html><body><div class="tweet"><div class="like">100</div><div class="like">10</div></div></body></html>'
311      );
312      const tweet = await page.$('.tweet');
313      const content = await tweet.$$eval('.like', (nodes: HTMLElement[]) =>
314        nodes.map((n) => n.innerText)
315      );
316      expect(content).toEqual(['100', '10']);
317    });
318
319    it('should retrieve content from subtree', async () => {
320      const { page } = getTestState();
321
322      const htmlContent =
323        '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a1-child-div</div><div class="a">a2-child-div</div></div>';
324      await page.setContent(htmlContent);
325      const elementHandle = await page.$('#myId');
326      const content = await elementHandle.$$eval('.a', (nodes: HTMLElement[]) =>
327        nodes.map((n) => n.innerText)
328      );
329      expect(content).toEqual(['a1-child-div', 'a2-child-div']);
330    });
331
332    it('should not throw in case of missing selector', async () => {
333      const { page } = getTestState();
334
335      const htmlContent =
336        '<div class="a">not-a-child-div</div><div id="myId"></div>';
337      await page.setContent(htmlContent);
338      const elementHandle = await page.$('#myId');
339      const nodesLength = await elementHandle.$$eval(
340        '.a',
341        (nodes) => nodes.length
342      );
343      expect(nodesLength).toBe(0);
344    });
345  });
346
347  describe('ElementHandle.$$', function () {
348    it('should query existing elements', async () => {
349      const { page } = getTestState();
350
351      await page.setContent(
352        '<html><body><div>A</div><br/><div>B</div></body></html>'
353      );
354      const html = await page.$('html');
355      const elements = await html.$$('div');
356      expect(elements.length).toBe(2);
357      const promises = elements.map((element) =>
358        page.evaluate((e: HTMLElement) => e.textContent, element)
359      );
360      expect(await Promise.all(promises)).toEqual(['A', 'B']);
361    });
362
363    it('should return empty array for non-existing elements', async () => {
364      const { page } = getTestState();
365
366      await page.setContent(
367        '<html><body><span>A</span><br/><span>B</span></body></html>'
368      );
369      const html = await page.$('html');
370      const elements = await html.$$('div');
371      expect(elements.length).toBe(0);
372    });
373  });
374
375  describe('ElementHandle.$x', function () {
376    it('should query existing element', async () => {
377      const { page, server } = getTestState();
378
379      await page.goto(server.PREFIX + '/playground.html');
380      await page.setContent(
381        '<html><body><div class="second"><div class="inner">A</div></div></body></html>'
382      );
383      const html = await page.$('html');
384      const second = await html.$x(`./body/div[contains(@class, 'second')]`);
385      const inner = await second[0].$x(`./div[contains(@class, 'inner')]`);
386      const content = await page.evaluate(
387        (e: HTMLElement) => e.textContent,
388        inner[0]
389      );
390      expect(content).toBe('A');
391    });
392
393    it('should return null for non-existing element', async () => {
394      const { page } = getTestState();
395
396      await page.setContent(
397        '<html><body><div class="second"><div class="inner">B</div></div></body></html>'
398      );
399      const html = await page.$('html');
400      const second = await html.$x(`/div[contains(@class, 'third')]`);
401      expect(second).toEqual([]);
402    });
403  });
404
405  // This is the same tests for `$$eval` and `$$` as above, but with a queryAll
406  // handler that returns an array instead of a list of nodes.
407  describe('QueryAll', function () {
408    const handler: CustomQueryHandler = {
409      queryAll: (element: Element, selector: string) =>
410        Array.from(element.querySelectorAll(selector)),
411    };
412    before(() => {
413      const { puppeteer } = getTestState();
414      puppeteer.registerCustomQueryHandler('allArray', handler);
415    });
416
417    it('should have registered handler', async () => {
418      const { puppeteer } = getTestState();
419      expect(
420        puppeteer.customQueryHandlerNames().includes('allArray')
421      ).toBeTruthy();
422    });
423    it('$$ should query existing elements', async () => {
424      const { page } = getTestState();
425
426      await page.setContent(
427        '<html><body><div>A</div><br/><div>B</div></body></html>'
428      );
429      const html = await page.$('html');
430      const elements = await html.$$('allArray/div');
431      expect(elements.length).toBe(2);
432      const promises = elements.map((element) =>
433        page.evaluate((e: HTMLElement) => e.textContent, element)
434      );
435      expect(await Promise.all(promises)).toEqual(['A', 'B']);
436    });
437
438    it('$$ should return empty array for non-existing elements', async () => {
439      const { page } = getTestState();
440
441      await page.setContent(
442        '<html><body><span>A</span><br/><span>B</span></body></html>'
443      );
444      const html = await page.$('html');
445      const elements = await html.$$('allArray/div');
446      expect(elements.length).toBe(0);
447    });
448    it('$$eval should work', async () => {
449      const { page } = getTestState();
450
451      await page.setContent(
452        '<div>hello</div><div>beautiful</div><div>world!</div>'
453      );
454      const divsCount = await page.$$eval(
455        'allArray/div',
456        (divs) => divs.length
457      );
458      expect(divsCount).toBe(3);
459    });
460    it('$$eval should accept extra arguments', async () => {
461      const { page } = getTestState();
462      await page.setContent(
463        '<div>hello</div><div>beautiful</div><div>world!</div>'
464      );
465      const divsCountPlus5 = await page.$$eval(
466        'allArray/div',
467        (divs, two: number, three: number) => divs.length + two + three,
468        2,
469        3
470      );
471      expect(divsCountPlus5).toBe(8);
472    });
473    it('$$eval should accept ElementHandles as arguments', async () => {
474      const { page } = getTestState();
475      await page.setContent(
476        '<section>2</section><section>2</section><section>1</section><div>3</div>'
477      );
478      const divHandle = await page.$('div');
479      const sum = await page.$$eval(
480        'allArray/section',
481        (sections, div: HTMLElement) =>
482          sections.reduce(
483            (acc, section) => acc + Number(section.textContent),
484            0
485          ) + Number(div.textContent),
486        divHandle
487      );
488      expect(sum).toBe(8);
489    });
490    it('$$eval should handle many elements', async () => {
491      const { page } = getTestState();
492      await page.evaluate(
493        `
494        for (var i = 0; i <= 1000; i++) {
495            const section = document.createElement('section');
496            section.textContent = i;
497            document.body.appendChild(section);
498        }
499        `
500      );
501      const sum = await page.$$eval('allArray/section', (sections) =>
502        sections.reduce((acc, section) => acc + Number(section.textContent), 0)
503      );
504      expect(sum).toBe(500500);
505    });
506  });
507});
508