/** * @license * Copyright 2018 Google Inc. * SPDX-License-Identifier: Apache-2.0 */ import assert from 'assert'; import expect from 'expect'; import {Puppeteer} from 'puppeteer-core'; import type {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; describe('Query handler tests', function () { setupTestBrowserHooks(); describe('Pierce selectors', function () { async function setUpPage(): ReturnType { const state = await getTestState(); await state.page.setContent( `` ); return state; } it('should find first element in shadow', async () => { const {page} = await setUpPage(); using div = (await page.$('pierce/.foo')) as ElementHandle; const text = await div.evaluate(element => { return element.textContent; }); expect(text).toBe('Hello'); }); it('should find all elements in shadow', async () => { const {page} = await setUpPage(); const divs = (await page.$$('pierce/.foo')) as Array< ElementHandle >; const text = await Promise.all( divs.map(div => { return div.evaluate(element => { return element.textContent; }); }) ); expect(text.join(' ')).toBe('Hello World'); }); it('should find first child element', async () => { const {page} = await setUpPage(); using parentElement = (await page.$('html > div'))!; using childElement = (await parentElement.$( 'pierce/div' )) as ElementHandle; const text = await childElement.evaluate(element => { return element.textContent; }); expect(text).toBe('Hello'); }); it('should find all child elements', async () => { const {page} = await setUpPage(); using parentElement = (await page.$('html > div'))!; const childElements = (await parentElement.$$('pierce/div')) as Array< ElementHandle >; const text = await Promise.all( childElements.map(div => { return div.evaluate(element => { return element.textContent; }); }) ); expect(text.join(' ')).toBe('Hello World'); }); }); describe('Text selectors', function () { describe('in Page', function () { it('should query existing element', async () => { const {page} = await getTestState(); await page.setContent('
test
'); expect(await page.$('text/test')).toBeTruthy(); expect(await page.$$('text/test')).toHaveLength(1); }); it('should return empty array for non-existing element', async () => { const {page} = await getTestState(); expect(await page.$('text/test')).toBeFalsy(); expect(await page.$$('text/test')).toHaveLength(0); }); it('should return first element', async () => { const {page} = await getTestState(); await page.setContent('
a
a
'); using element = await page.$('text/a'); expect( await element?.evaluate(e => { return e.id; }) ).toBe('1'); }); it('should return multiple elements', async () => { const {page} = await getTestState(); await page.setContent('
a
a
'); const elements = await page.$$('text/a'); expect(elements).toHaveLength(2); }); it('should pierce shadow DOM', async () => { const {page} = await getTestState(); await page.evaluate(() => { const div = document.createElement('div'); const shadow = div.attachShadow({mode: 'open'}); const diva = document.createElement('div'); shadow.append(diva); const divb = document.createElement('div'); shadow.append(divb); diva.innerHTML = 'a'; divb.innerHTML = 'b'; document.body.append(div); }); using element = await page.$('text/a'); expect( await element?.evaluate(e => { return e.textContent; }) ).toBe('a'); }); it('should query deeply nested text', async () => { const {page} = await getTestState(); await page.setContent('
a
b
'); using element = await page.$('text/a'); expect( await element?.evaluate(e => { return e.textContent; }) ).toBe('a'); }); it('should query inputs', async () => { const {page} = await getTestState(); await page.setContent(''); using element = (await page.$( 'text/a' )) as ElementHandle; expect( await element?.evaluate(e => { return e.value; }) ).toBe('a'); }); it('should not query radio', async () => { const {page} = await getTestState(); await page.setContent(''); expect(await page.$('text/a')).toBeNull(); }); it('should query text spanning multiple elements', async () => { const {page} = await getTestState(); await page.setContent('
a b
'); using element = await page.$('text/a b'); expect( await element?.evaluate(e => { return e.textContent; }) ).toBe('a b'); }); it('should clear caches', async () => { const {page} = await getTestState(); await page.setContent( '
text
text
' ); using div = (await page.$('#target1')) as ElementHandle; using input = (await page.$( '#target2' )) as ElementHandle; await div.evaluate(div => { div.textContent = 'text'; }); expect( await page.$eval(`text/text`, e => { return e.id; }) ).toBe('target1'); await div.evaluate(div => { div.textContent = 'foo'; }); expect( await page.$eval(`text/text`, e => { return e.id; }) ).toBe('target2'); await input.evaluate(input => { input.value = ''; }); await input.type('foo'); expect( await page.$eval(`text/text`, e => { return e.id; }) ).toBe('target3'); await div.evaluate(div => { div.textContent = 'text'; }); await input.evaluate(input => { input.value = ''; }); await input.type('text'); expect( await page.$$eval(`text/text`, es => { return es.length; }) ).toBe(3); await div.evaluate(div => { div.textContent = 'foo'; }); expect( await page.$$eval(`text/text`, es => { return es.length; }) ).toBe(2); await input.evaluate(input => { input.value = ''; }); await input.type('foo'); expect( await page.$$eval(`text/text`, es => { return es.length; }) ).toBe(1); }); }); describe('in ElementHandles', function () { it('should query existing element', async () => { const {page} = await getTestState(); await page.setContent('
a
'); using elementHandle = (await page.$('div'))!; expect(await elementHandle.$(`text/a`)).toBeTruthy(); expect(await elementHandle.$$(`text/a`)).toHaveLength(1); }); it('should return null for non-existing element', async () => { const {page} = await getTestState(); await page.setContent('
'); using elementHandle = (await page.$('div'))!; expect(await elementHandle.$(`text/a`)).toBeFalsy(); expect(await elementHandle.$$(`text/a`)).toHaveLength(0); }); }); }); describe('XPath selectors', function () { describe('in Page', function () { it('should query existing element', async () => { const {page} = await getTestState(); await page.setContent('
test
'); expect(await page.$('xpath/html/body/section')).toBeTruthy(); expect(await page.$$('xpath/html/body/section')).toHaveLength(1); }); it('should return empty array for non-existing element', async () => { const {page} = await getTestState(); expect( await page.$('xpath/html/body/non-existing-element') ).toBeFalsy(); expect( await page.$$('xpath/html/body/non-existing-element') ).toHaveLength(0); }); it('should return first element', async () => { const {page} = await getTestState(); await page.setContent('
a
'); using element = await page.$('xpath/html/body/div'); expect( await element?.evaluate(e => { return e.textContent === 'a'; }) ).toBeTruthy(); }); it('should return multiple elements', async () => { const {page} = await getTestState(); await page.setContent('
'); const elements = await page.$$('xpath/html/body/div'); expect(elements).toHaveLength(2); }); }); describe('in ElementHandles', function () { it('should query existing element', async () => { const {page} = await getTestState(); await page.setContent('
a
'); using elementHandle = (await page.$('div'))!; expect(await elementHandle.$(`xpath/span`)).toBeTruthy(); expect(await elementHandle.$$(`xpath/span`)).toHaveLength(1); }); it('should return null for non-existing element', async () => { const {page} = await getTestState(); await page.setContent('
a
'); using elementHandle = (await page.$('div'))!; expect(await elementHandle.$(`xpath/span`)).toBeFalsy(); expect(await elementHandle.$$(`xpath/span`)).toHaveLength(0); }); }); }); describe('P selectors', () => { beforeEach(async () => { Puppeteer.clearCustomQueryHandlers(); }); it('should work with CSS selectors', async () => { const {server, page} = await getTestState(); await page.goto(`${server.PREFIX}/p-selectors.html`); using element = await page.$('div > button'); assert(element, 'Could not find element'); expect( await element.evaluate(element => { return element.id === 'b'; }) ).toBeTruthy(); // Should parse more complex CSS selectors. Listing a few problematic // cases from bug reports. for (const selector of [ '.user_row[data-user-id="\\38 "]:not(.deactivated_user)', `input[value='Search']:not([class='hidden'])`, `[data-test-id^="test-"]:not([data-test-id^="test-foo"])`, ]) { await page.$$(selector); } }); it('should work with deep combinators', async () => { const {server, page} = await getTestState(); await page.goto(`${server.PREFIX}/p-selectors.html`); { using element = await page.$('div >>>> div'); assert(element, 'Could not find element'); expect( await element.evaluate(element => { return element.id === 'c'; }) ).toBeTruthy(); } { const elements = await page.$$('div >>> div'); assert(elements[1], 'Could not find element'); expect( await elements[1]?.evaluate(element => { return element.id === 'd'; }) ).toBeTruthy(); } { const elements = await page.$$('#c >>>> div'); assert(elements[0], 'Could not find element'); expect( await elements[0]?.evaluate(element => { return element.id === 'd'; }) ).toBeTruthy(); } { const elements = await page.$$('#c >>> div'); assert(elements[0], 'Could not find element'); expect( await elements[0]?.evaluate(element => { return element.id === 'd'; }) ).toBeTruthy(); } }); it('should work with text selectors', async () => { const {server, page} = await getTestState(); await page.goto(`${server.PREFIX}/p-selectors.html`); using element = await page.$('div ::-p-text(world)'); assert(element, 'Could not find element'); expect( await element.evaluate(element => { return element.id === 'b'; }) ).toBeTruthy(); }); it('should work ARIA selectors', async () => { const {server, page} = await getTestState(); await page.goto(`${server.PREFIX}/p-selectors.html`); using element = await page.$('div ::-p-aria(world)'); assert(element, 'Could not find element'); expect( await element.evaluate(element => { return element.id === 'b'; }) ).toBeTruthy(); }); it('should work for ARIA selectors in multiple isolated worlds', async () => { const {server, page} = await getTestState(); await page.goto(`${server.PREFIX}/p-selectors.html`); using element = await page.waitForSelector('::-p-aria(world)'); assert(element, 'Could not find element'); expect( await element.evaluate(element => { return element.id === 'b'; }) ).toBeTruthy(); // $ would add ARIA query handler to the main world. await element.$('::-p-aria(world)'); using element2 = await page.waitForSelector('::-p-aria(world)'); assert(element2, 'Could not find element'); expect( await element2.evaluate(element => { return element.id === 'b'; }) ).toBeTruthy(); }); it('should work ARIA selectors with role', async () => { const {server, page} = await getTestState(); await page.goto(`${server.PREFIX}/p-selectors.html`); using element = await page.$('::-p-aria(world[role="button"])'); assert(element, 'Could not find element'); expect( await element.evaluate(element => { return element.id === 'b'; }) ).toBeTruthy(); }); it('should work ARIA selectors with name and role', async () => { const {server, page} = await getTestState(); await page.goto(`${server.PREFIX}/p-selectors.html`); using element = await page.$('::-p-aria([name="world"][role="button"])'); assert(element, 'Could not find element'); expect( await element.evaluate(element => { return element.id === 'b'; }) ).toBeTruthy(); }); it('should work XPath selectors', async () => { const {server, page} = await getTestState(); await page.goto(`${server.PREFIX}/p-selectors.html`); using element = await page.$('div ::-p-xpath(//button)'); assert(element, 'Could not find element'); expect( await element.evaluate(element => { return element.id === 'b'; }) ).toBeTruthy(); }); it('should work with custom selectors', async () => { Puppeteer.registerCustomQueryHandler('div', { queryOne() { return document.querySelector('div'); }, }); const {server, page} = await getTestState(); await page.goto(`${server.PREFIX}/p-selectors.html`); using element = await page.$('::-p-div'); assert(element, 'Could not find element'); expect( await element.evaluate(element => { return element.id === 'a'; }) ).toBeTruthy(); }); it('should work with custom selectors with args', async () => { const {server, page} = await getTestState(); await page.goto(`${server.PREFIX}/p-selectors.html`); Puppeteer.registerCustomQueryHandler('div', { queryOne(_, selector) { if (selector === 'true') { return document.querySelector('div'); } else { return document.querySelector('button'); } }, }); { using element = await page.$('::-p-div(true)'); assert(element, 'Could not find element'); expect( await element.evaluate(element => { return element.id === 'a'; }) ).toBeTruthy(); } { using element = await page.$('::-p-div("true")'); assert(element, 'Could not find element'); expect( await element.evaluate(element => { return element.id === 'a'; }) ).toBeTruthy(); } { using element = await page.$("::-p-div('true')"); assert(element, 'Could not find element'); expect( await element.evaluate(element => { return element.id === 'a'; }) ).toBeTruthy(); } { using element = await page.$('::-p-div'); assert(element, 'Could not find element'); expect( await element.evaluate(element => { return element.id === 'b'; }) ).toBeTruthy(); } }); it('should work with :hover', async () => { const {server, page} = await getTestState(); await page.goto(`${server.PREFIX}/p-selectors.html`); using button = await page.$('div ::-p-text(world)'); assert(button, 'Could not find element'); await button.hover(); using button2 = await page.$('div ::-p-text(world):hover'); assert(button2, 'Could not find element'); const value = await button2.evaluate(span => { return {textContent: span.textContent, tagName: span.tagName}; }); expect(value).toMatchObject({textContent: 'world', tagName: 'BUTTON'}); }); it('should work with selector lists', async () => { const {server, page} = await getTestState(); await page.goto(`${server.PREFIX}/p-selectors.html`); const elements = await page.$$('div, ::-p-text(world)'); expect(elements).toHaveLength(3); }); const permute = (inputs: T[]): T[][] => { const results: T[][] = []; for (let i = 0; i < inputs.length; ++i) { const permutation = permute( inputs.slice(0, i).concat(inputs.slice(i + 1)) ); const value = inputs[i] as T; if (permutation.length === 0) { results.push([value]); continue; } for (const part of permutation) { results.push([value].concat(part)); } } return results; }; it('should match querySelector* ordering', async () => { const {server, page} = await getTestState(); await page.goto(`${server.PREFIX}/p-selectors.html`); for (const list of permute(['div', 'button', 'span'])) { const elements = await page.$$( list .map(selector => { return selector === 'button' ? '::-p-text(world)' : selector; }) .join(',') ); const actual = await Promise.all( elements.map(element => { return element.evaluate(element => { return element.id; }); }) ); expect(actual.join()).toStrictEqual('a,b,f,c'); } }); it('should not have duplicate elements from selector lists', async () => { const {server, page} = await getTestState(); await page.goto(`${server.PREFIX}/p-selectors.html`); const elements = await page.$$('::-p-text(world), button'); expect(elements).toHaveLength(1); }); it('should handle escapes', async () => { const {server, page} = await getTestState(); await page.goto(`${server.PREFIX}/p-selectors.html`); using element = await page.$( ':scope >>> ::-p-text(My name is Jun \\(pronounced like "June"\\))' ); expect(element).toBeTruthy(); using element2 = await page.$( ':scope >>> ::-p-text("My name is Jun (pronounced like \\"June\\")")' ); expect(element2).toBeTruthy(); using element3 = await page.$( ':scope >>> ::-p-text(My name is Jun \\(pronounced like "June"\\)")' ); expect(element3).toBeFalsy(); using element4 = await page.$( ':scope >>> ::-p-text("My name is Jun \\(pronounced like "June"\\))' ); expect(element4).toBeFalsy(); }); }); });