diff options
Diffstat (limited to 'remote/test/puppeteer/test/src/queryhandler.spec.ts')
-rw-r--r-- | remote/test/puppeteer/test/src/queryhandler.spec.ts | 594 |
1 files changed, 594 insertions, 0 deletions
diff --git a/remote/test/puppeteer/test/src/queryhandler.spec.ts b/remote/test/puppeteer/test/src/queryhandler.spec.ts new file mode 100644 index 0000000000..195a9e7e6f --- /dev/null +++ b/remote/test/puppeteer/test/src/queryhandler.spec.ts @@ -0,0 +1,594 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import assert from 'assert'; + +import expect from 'expect'; +import {Puppeteer} from 'puppeteer-core'; +import {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; + +import { + getTestState, + setupTestBrowserHooks, + setupTestPageAndContextHooks, +} from './mocha-utils.js'; + +describe('Query handler tests', function () { + setupTestBrowserHooks(); + setupTestPageAndContextHooks(); + + describe('Pierce selectors', function () { + beforeEach(async () => { + const {page} = getTestState(); + await page.setContent( + `<script> + const div = document.createElement('div'); + const shadowRoot = div.attachShadow({mode: 'open'}); + const div1 = document.createElement('div'); + div1.textContent = 'Hello'; + div1.className = 'foo'; + const div2 = document.createElement('div'); + div2.textContent = 'World'; + div2.className = 'foo'; + shadowRoot.appendChild(div1); + shadowRoot.appendChild(div2); + document.documentElement.appendChild(div); + </script>` + ); + }); + it('should find first element in shadow', async () => { + const {page} = getTestState(); + const div = (await page.$('pierce/.foo')) as ElementHandle<HTMLElement>; + const text = await div.evaluate(element => { + return element.textContent; + }); + expect(text).toBe('Hello'); + }); + it('should find all elements in shadow', async () => { + const {page} = getTestState(); + const divs = (await page.$$('pierce/.foo')) as Array< + ElementHandle<HTMLElement> + >; + 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} = getTestState(); + const parentElement = (await page.$('html > div'))!; + const childElement = (await parentElement.$( + 'pierce/div' + )) as ElementHandle<HTMLElement>; + const text = await childElement.evaluate(element => { + return element.textContent; + }); + expect(text).toBe('Hello'); + }); + it('should find all child elements', async () => { + const {page} = getTestState(); + const parentElement = (await page.$('html > div'))!; + const childElements = (await parentElement.$$('pierce/div')) as Array< + ElementHandle<HTMLElement> + >; + 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} = getTestState(); + + await page.setContent('<section>test</section>'); + + 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} = getTestState(); + + expect(await page.$('text/test')).toBeFalsy(); + expect(await page.$$('text/test')).toHaveLength(0); + }); + it('should return first element', async () => { + const {page} = getTestState(); + + await page.setContent('<div id="1">a</div><div>a</div>'); + + const element = await page.$('text/a'); + expect( + await element?.evaluate(e => { + return e.id; + }) + ).toBe('1'); + }); + it('should return multiple elements', async () => { + const {page} = getTestState(); + + await page.setContent('<div>a</div><div>a</div>'); + + const elements = await page.$$('text/a'); + expect(elements).toHaveLength(2); + }); + it('should pierce shadow DOM', async () => { + const {page} = 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); + }); + + const element = await page.$('text/a'); + expect( + await element?.evaluate(e => { + return e.textContent; + }) + ).toBe('a'); + }); + it('should query deeply nested text', async () => { + const {page} = getTestState(); + + await page.setContent('<div><div>a</div><div>b</div></div>'); + + const element = await page.$('text/a'); + expect( + await element?.evaluate(e => { + return e.textContent; + }) + ).toBe('a'); + }); + it('should query inputs', async () => { + const {page} = getTestState(); + + await page.setContent('<input value="a">'); + + const element = (await page.$( + 'text/a' + )) as ElementHandle<HTMLInputElement>; + expect( + await element?.evaluate(e => { + return e.value; + }) + ).toBe('a'); + }); + it('should not query radio', async () => { + const {page} = getTestState(); + + await page.setContent('<radio value="a">'); + + expect(await page.$('text/a')).toBeNull(); + }); + it('should query text spanning multiple elements', async () => { + const {page} = getTestState(); + + await page.setContent('<div><span>a</span> <span>b</span><div>'); + + const element = await page.$('text/a b'); + expect( + await element?.evaluate(e => { + return e.textContent; + }) + ).toBe('a b'); + }); + it('should clear caches', async () => { + const {page} = getTestState(); + + await page.setContent( + '<div id=target1>text</div><input id=target2 value=text><div id=target3>text</div>' + ); + const div = (await page.$('#target1')) as ElementHandle<HTMLDivElement>; + const input = (await page.$( + '#target2' + )) as ElementHandle<HTMLInputElement>; + + 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} = getTestState(); + + await page.setContent('<div class="a"><span>a</span></div>'); + + const 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} = getTestState(); + + await page.setContent('<div class="a"></div>'); + + const 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} = getTestState(); + + await page.setContent('<section>test</section>'); + + 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} = 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} = getTestState(); + + await page.setContent('<div>a</div><div></div>'); + + const 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} = getTestState(); + + await page.setContent('<div></div><div></div>'); + + const elements = await page.$$('xpath/html/body/div'); + expect(elements).toHaveLength(2); + }); + }); + describe('in ElementHandles', function () { + it('should query existing element', async () => { + const {page} = getTestState(); + + await page.setContent('<div class="a">a<span></span></div>'); + + const 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} = getTestState(); + + await page.setContent('<div class="a">a</div>'); + + const elementHandle = (await page.$('div'))!; + expect(await elementHandle.$(`xpath/span`)).toBeFalsy(); + expect(await elementHandle.$$(`xpath/span`)).toHaveLength(0); + }); + }); + }); + + describe('P selectors', () => { + beforeEach(async () => { + const {page, server} = getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + Puppeteer.clearCustomQueryHandlers(); + }); + + it('should work with CSS selectors', async () => { + const {page} = getTestState(); + const 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 {page} = getTestState(); + { + const element = await page.$('div >>>> div'); + console.log({element}) + 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 {page} = getTestState(); + const 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 {page} = getTestState(); + const 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 XPath selectors', async () => { + const {page} = getTestState(); + const 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 {page} = getTestState(); + const 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 {page} = getTestState(); + Puppeteer.registerCustomQueryHandler('div', { + queryOne(_, selector) { + if (selector === 'true') { + return document.querySelector('div'); + } else { + return document.querySelector('button'); + } + }, + }); + + { + const element = await page.$('::-p-div(true)'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'a'; + }) + ).toBeTruthy(); + } + { + const element = await page.$('::-p-div("true")'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'a'; + }) + ).toBeTruthy(); + } + { + const element = await page.$("::-p-div('true')"); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'a'; + }) + ).toBeTruthy(); + } + { + const 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 {page} = getTestState(); + let button = await page.$('div ::-p-text(world)'); + assert(button, 'Could not find element'); + await button.hover(); + await button.dispose(); + + button = await page.$('div ::-p-text(world):hover'); + assert(button, 'Could not find element'); + const value = await button.evaluate(span => { + return {textContent: span.textContent, tagName: span.tagName}; + }); + expect(value).toMatchObject({textContent: 'world', tagName: 'BUTTON'}); + }); + + it('should work with selector lists', async () => { + const {page} = getTestState(); + const elements = await page.$$('div, ::-p-text(world)'); + expect(elements).toHaveLength(3); + }); + + const permute = <T>(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 {page} = getTestState(); + 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 {page} = getTestState(); + const elements = await page.$$('::-p-text(world), button'); + expect(elements).toHaveLength(1); + }); + }); +}); |