feat: change QueryHandler to contain QueryOne and QueryAll methods (#6218)

Co-authored-by: Mathias Bynens <mathias@qiwi.be>
This commit is contained in:
Johan Bay 2020-07-17 07:29:42 +02:00 committed by GitHub
parent 82645e85c7
commit 313774c553
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 101 additions and 60 deletions

View File

@ -26,7 +26,7 @@ import { ExecutionContext } from './ExecutionContext.js';
import { TimeoutSettings } from './TimeoutSettings.js'; import { TimeoutSettings } from './TimeoutSettings.js';
import { MouseButton } from './Input.js'; import { MouseButton } from './Input.js';
import { FrameManager, Frame } from './FrameManager.js'; import { FrameManager, Frame } from './FrameManager.js';
import { getQueryHandlerAndSelector, QueryHandler } from './QueryHandler.js'; import { getQueryHandlerAndSelector } from './QueryHandler.js';
import { import {
SerializableOrJSHandle, SerializableOrJSHandle,
EvaluateHandleFn, EvaluateHandleFn,
@ -39,7 +39,10 @@ import { isNode } from '../environment.js';
// This predicateQueryHandler is declared here so that TypeScript knows about it // This predicateQueryHandler is declared here so that TypeScript knows about it
// when it is used in the predicate function below. // when it is used in the predicate function below.
declare const predicateQueryHandler: QueryHandler; declare const predicateQueryHandler: (
element: Element | Document,
selector: string
) => Element | Element[] | NodeListOf<Element>;
/** /**
* @public * @public
@ -506,16 +509,13 @@ export class DOMWorld {
const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${ const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${
waitForHidden ? ' to be hidden' : '' waitForHidden ? ' to be hidden' : ''
}`; }`;
const { const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(
updatedSelector, selectorOrXPath
queryHandler,
} = getQueryHandlerAndSelector(selectorOrXPath, (element, selector) =>
document.querySelector(selector)
); );
const waitTask = new WaitTask( const waitTask = new WaitTask(
this, this,
predicate, predicate,
queryHandler, queryHandler.queryOne,
title, title,
polling, polling,
timeout, timeout,

View File

@ -774,14 +774,14 @@ export class ElementHandle<
* the return value resolves to `null`. * the return value resolves to `null`.
*/ */
async $(selector: string): Promise<ElementHandle | null> { async $(selector: string): Promise<ElementHandle | null> {
const defaultHandler = (element: Element, selector: string) =>
element.querySelector(selector);
const { updatedSelector, queryHandler } = getQueryHandlerAndSelector( const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(
selector, selector
defaultHandler
); );
const handle = await this.evaluateHandle(queryHandler, updatedSelector); const handle = await this.evaluateHandle(
queryHandler.queryOne,
updatedSelector
);
const element = handle.asElement(); const element = handle.asElement();
if (element) return element; if (element) return element;
await handle.dispose(); await handle.dispose();
@ -793,19 +793,16 @@ export class ElementHandle<
* the return value resolves to `[]`. * the return value resolves to `[]`.
*/ */
async $$(selector: string): Promise<ElementHandle[]> { async $$(selector: string): Promise<ElementHandle[]> {
const defaultHandler = (element: Element, selector: string) =>
element.querySelectorAll(selector);
const { updatedSelector, queryHandler } = getQueryHandlerAndSelector( const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(
selector, selector
defaultHandler
); );
const arrayHandle = await this.evaluateHandle( const handles = await this.evaluateHandle(
queryHandler, queryHandler.queryAll,
updatedSelector updatedSelector
); );
const properties = await arrayHandle.getProperties(); const properties = await handles.getProperties();
await arrayHandle.dispose(); await handles.dispose();
const result = []; const result = [];
for (const property of properties.values()) { for (const property of properties.values()) {
const elementHandle = property.asElement(); const elementHandle = property.asElement();
@ -851,8 +848,8 @@ export class ElementHandle<
await elementHandle.dispose(); await elementHandle.dispose();
/** /**
* This as is a little unfortunate but helps TS understand the behavour of * This `as` is a little unfortunate but helps TS understand the behavior of
* `elementHandle.evaluate`. If evalute returns an element it will return an * `elementHandle.evaluate`. If evaluate returns an element it will return an
* ElementHandle instance, rather than the plain object. All the * ElementHandle instance, rather than the plain object. All the
* WrapElementHandle type does is wrap ReturnType into * WrapElementHandle type does is wrap ReturnType into
* ElementHandle<ReturnType> if it is an ElementHandle, or leave it alone as * ElementHandle<ReturnType> if it is an ElementHandle, or leave it alone as
@ -892,15 +889,16 @@ export class ElementHandle<
) => ReturnType | Promise<ReturnType>, ) => ReturnType | Promise<ReturnType>,
...args: SerializableOrJSHandle[] ...args: SerializableOrJSHandle[]
): Promise<WrapElementHandle<ReturnType>> { ): Promise<WrapElementHandle<ReturnType>> {
const defaultHandler = (element: Element, selector: string) =>
Array.from(element.querySelectorAll(selector));
const { updatedSelector, queryHandler } = getQueryHandlerAndSelector( const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(
selector, selector
defaultHandler
); );
const queryHandlerToArray = Function(
'element',
'selector',
`return Array.from((${queryHandler.queryAll})(element, selector));`
) as (...args: unknown[]) => unknown;
const arrayHandle = await this.evaluateHandle( const arrayHandle = await this.evaluateHandle(
queryHandler, queryHandlerToArray,
updatedSelector updatedSelector
); );
const result = await arrayHandle.evaluate< const result = await arrayHandle.evaluate<
@ -910,8 +908,8 @@ export class ElementHandle<
) => ReturnType | Promise<ReturnType> ) => ReturnType | Promise<ReturnType>
>(pageFunction, ...args); >(pageFunction, ...args);
await arrayHandle.dispose(); await arrayHandle.dispose();
/* This as exists for the same reason as the `as` in $eval above. /* This `as` exists for the same reason as the `as` in $eval above.
* See the comment there for a ful explanation. * See the comment there for a full explanation.
*/ */
return result as WrapElementHandle<ReturnType>; return result as WrapElementHandle<ReturnType>;
} }

View File

@ -15,17 +15,18 @@
*/ */
export interface QueryHandler { export interface QueryHandler {
(element: Element | Document, selector: string): queryOne?: (element: Element | Document, selector: string) => Element | null;
| Element queryAll?: (
| Element[] element: Element | Document,
| NodeListOf<Element>; selector: string
) => Element[] | NodeListOf<Element>;
} }
const _customQueryHandlers = new Map<string, QueryHandler>(); const _customQueryHandlers = new Map<string, QueryHandler>();
export function registerCustomQueryHandler( export function registerCustomQueryHandler(
name: string, name: string,
handler: Function handler: QueryHandler
): void { ): void {
if (_customQueryHandlers.get(name)) if (_customQueryHandlers.get(name))
throw new Error(`A custom query handler named "${name}" already exists`); throw new Error(`A custom query handler named "${name}" already exists`);
@ -34,7 +35,7 @@ export function registerCustomQueryHandler(
if (!isValidName) if (!isValidName)
throw new Error(`Custom query handler names may only contain [a-zA-Z]`); throw new Error(`Custom query handler names may only contain [a-zA-Z]`);
_customQueryHandlers.set(name, handler as QueryHandler); _customQueryHandlers.set(name, handler);
} }
/** /**
@ -53,12 +54,17 @@ export function clearQueryHandlers(): void {
} }
export function getQueryHandlerAndSelector( export function getQueryHandlerAndSelector(
selector: string, selector: string
defaultQueryHandler: QueryHandler
): { updatedSelector: string; queryHandler: QueryHandler } { ): { updatedSelector: string; queryHandler: QueryHandler } {
const defaultHandler = {
queryOne: (element: Element, selector: string) =>
element.querySelector(selector),
queryAll: (element: Element, selector: string) =>
element.querySelectorAll(selector),
};
const hasCustomQueryHandler = /^[a-zA-Z]+\//.test(selector); const hasCustomQueryHandler = /^[a-zA-Z]+\//.test(selector);
if (!hasCustomQueryHandler) if (!hasCustomQueryHandler)
return { updatedSelector: selector, queryHandler: defaultQueryHandler }; return { updatedSelector: selector, queryHandler: defaultHandler };
const index = selector.indexOf('/'); const index = selector.indexOf('/');
const name = selector.slice(0, index); const name = selector.slice(0, index);

View File

@ -293,10 +293,10 @@ describe('ElementHandle specs', function () {
await page.setContent('<div id="not-foo"></div><div id="foo"></div>'); await page.setContent('<div id="not-foo"></div><div id="foo"></div>');
// Register. // Register.
puppeteer.__experimental_registerCustomQueryHandler( puppeteer.__experimental_registerCustomQueryHandler('getById', {
'getById', queryOne: (element, selector) =>
(element, selector) => document.querySelector(`[id="${selector}"]`) document.querySelector(`[id="${selector}"]`),
); });
const element = await page.$('getById/foo'); const element = await page.$('getById/foo');
expect( expect(
await page.evaluate<(element: HTMLElement) => string>( await page.evaluate<(element: HTMLElement) => string>(
@ -340,10 +340,10 @@ describe('ElementHandle specs', function () {
await page.setContent( await page.setContent(
'<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>' '<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>'
); );
puppeteer.__experimental_registerCustomQueryHandler( puppeteer.__experimental_registerCustomQueryHandler('getByClass', {
'getByClass', queryAll: (element, selector) =>
(element, selector) => document.querySelectorAll(`.${selector}`) document.querySelectorAll(`.${selector}`),
); });
const elements = await page.$$('getByClass/foo'); const elements = await page.$$('getByClass/foo');
const classNames = await Promise.all( const classNames = await Promise.all(
elements.map( elements.map(
@ -362,10 +362,10 @@ describe('ElementHandle specs', function () {
await page.setContent( await page.setContent(
'<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>' '<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>'
); );
puppeteer.__experimental_registerCustomQueryHandler( puppeteer.__experimental_registerCustomQueryHandler('getByClass', {
'getByClass', queryAll: (element, selector) =>
(element, selector) => document.querySelectorAll(`.${selector}`) document.querySelectorAll(`.${selector}`),
); });
const elements = await page.$$eval( const elements = await page.$$eval(
'getByClass/foo', 'getByClass/foo',
(divs) => divs.length (divs) => divs.length
@ -375,10 +375,9 @@ describe('ElementHandle specs', function () {
}); });
it('should wait correctly with waitForSelector', async () => { it('should wait correctly with waitForSelector', async () => {
const { page, puppeteer } = getTestState(); const { page, puppeteer } = getTestState();
puppeteer.__experimental_registerCustomQueryHandler( puppeteer.__experimental_registerCustomQueryHandler('getByClass', {
'getByClass', queryOne: (element, selector) => element.querySelector(`.${selector}`),
(element, selector) => element.querySelector(`.${selector}`) });
);
const waitFor = page.waitForSelector('getByClass/foo'); const waitFor = page.waitForSelector('getByClass/foo');
// Set the page content after the waitFor has been started. // Set the page content after the waitFor has been started.
@ -391,10 +390,9 @@ describe('ElementHandle specs', function () {
}); });
it('should wait correctly with waitFor', async () => { it('should wait correctly with waitFor', async () => {
const { page, puppeteer } = getTestState(); const { page, puppeteer } = getTestState();
puppeteer.__experimental_registerCustomQueryHandler( puppeteer.__experimental_registerCustomQueryHandler('getByClass', {
'getByClass', queryOne: (element, selector) => element.querySelector(`.${selector}`),
(element, selector) => element.querySelector(`.${selector}`) });
);
const waitFor = page.waitFor('getByClass/foo'); const waitFor = page.waitFor('getByClass/foo');
// Set the page content after the waitFor has been started. // Set the page content after the waitFor has been started.
@ -405,5 +403,44 @@ describe('ElementHandle specs', function () {
expect(element).toBeDefined(); expect(element).toBeDefined();
}); });
it('should work when both queryOne and queryAll are registered', async () => {
const { page, puppeteer } = getTestState();
await page.setContent(
'<div id="not-foo"></div><div class="foo"><div id="nested-foo" class="foo"/></div><div class="foo baz">Foo2</div>'
);
puppeteer.__experimental_registerCustomQueryHandler('getByClass', {
queryOne: (element, selector) => element.querySelector(`.${selector}`),
queryAll: (element, selector) =>
element.querySelectorAll(`.${selector}`),
});
const element = await page.$('getByClass/foo');
expect(element).toBeDefined();
const elements = await page.$$('getByClass/foo');
expect(elements.length).toBe(3);
});
it('should eval when both queryOne and queryAll are registered', async () => {
const { page, puppeteer } = getTestState();
await page.setContent(
'<div id="not-foo"></div><div class="foo">text</div><div class="foo baz">content</div>'
);
puppeteer.__experimental_registerCustomQueryHandler('getByClass', {
queryOne: (element, selector) => element.querySelector(`.${selector}`),
queryAll: (element, selector) =>
element.querySelectorAll(`.${selector}`),
});
const txtContent = await page.$eval(
'getByClass/foo',
(div) => div.textContent
);
expect(txtContent).toBe('text');
const txtContents = await page.$$eval('getByClass/foo', (divs) =>
divs.map((d) => d.textContent).join('')
);
expect(txtContents).toBe('textcontent');
});
}); });
}); });