Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 12 commits into from Jul 17, 2020
16 changes: 8 additions & 8 deletions src/common/DOMWorld.ts
Expand Up @@ -26,7 +26,7 @@ import { ExecutionContext } from './ExecutionContext.js';
import { TimeoutSettings } from './TimeoutSettings.js';
import { MouseButton } from './Input.js';
import { FrameManager, Frame } from './FrameManager.js';
import { getQueryHandlerAndSelector, QueryHandler } from './QueryHandler.js';
import { getQueryHandlerAndSelector } from './QueryHandler.js';
import {
SerializableOrJSHandle,
EvaluateHandleFn,
Expand All @@ -39,7 +39,10 @@ import { isNode } from '../environment.js';

// This predicateQueryHandler is declared here so that TypeScript knows about it
// 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
Expand Down Expand Up @@ -506,16 +509,13 @@ export class DOMWorld {
const title = `${isXPath ? 'XPath' : 'selector'} "${selectorOrXPath}"${
waitForHidden ? ' to be hidden' : ''
}`;
const {
updatedSelector,
queryHandler,
} = getQueryHandlerAndSelector(selectorOrXPath, (element, selector) =>
document.querySelector(selector)
const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(
selectorOrXPath
);
const waitTask = new WaitTask(
this,
predicate,
queryHandler,
queryHandler.queryOne,
title,
polling,
timeout,
Expand Down
2 changes: 1 addition & 1 deletion src/common/EvalTypes.ts
Expand Up @@ -37,7 +37,7 @@ export type EvaluateFnReturnType<T extends EvaluateFn> = T extends (
/**
* @public
*/
export type EvaluateHandleFn = string | ((...args: unknown[]) => unknown);
export type EvaluateHandleFn = string | Function;
johanbay marked this conversation as resolved.
Show resolved Hide resolved

/**
* @public
Expand Down
44 changes: 21 additions & 23 deletions src/common/JSHandle.ts
Expand Up @@ -774,14 +774,14 @@ export class ElementHandle<
* the return value resolves to `null`.
*/
async $(selector: string): Promise<ElementHandle | null> {
const defaultHandler = (element: Element, selector: string) =>
element.querySelector(selector);
const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(
selector,
defaultHandler
selector
);

const handle = await this.evaluateHandle(queryHandler, updatedSelector);
const handle = await this.evaluateHandle(
queryHandler.queryOne,
updatedSelector
);
const element = handle.asElement();
if (element) return element;
await handle.dispose();
Expand All @@ -793,19 +793,16 @@ export class ElementHandle<
* the return value resolves to `[]`.
*/
async $$(selector: string): Promise<ElementHandle[]> {
const defaultHandler = (element: Element, selector: string) =>
element.querySelectorAll(selector);
const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(
selector,
defaultHandler
selector
);

const arrayHandle = await this.evaluateHandle(
queryHandler,
const handles = await this.evaluateHandle(
queryHandler.queryAll,
updatedSelector
);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const properties = await handles.getProperties();
await handles.dispose();
const result = [];
for (const property of properties.values()) {
const elementHandle = property.asElement();
Expand Down Expand Up @@ -851,8 +848,8 @@ export class ElementHandle<
await elementHandle.dispose();

/**
* This as is a little unfortunate but helps TS understand the behavour of
* `elementHandle.evaluate`. If evalute returns an element it will return an
* This `as` is a little unfortunate but helps TS understand the behavior of
* `elementHandle.evaluate`. If evaluate returns an element it will return an
* ElementHandle instance, rather than the plain object. All the
* WrapElementHandle type does is wrap ReturnType into
* ElementHandle<ReturnType> if it is an ElementHandle, or leave it alone as
Expand Down Expand Up @@ -892,15 +889,16 @@ export class ElementHandle<
) => ReturnType | Promise<ReturnType>,
...args: SerializableOrJSHandle[]
): Promise<WrapElementHandle<ReturnType>> {
const defaultHandler = (element: Element, selector: string) =>
Array.from(element.querySelectorAll(selector));
const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(
selector,
defaultHandler
selector
);
const queryHandlerToArray = Function(
'element',
'selector',
`return Array.from((${queryHandler.queryAll})(element, selector));`
johanbay marked this conversation as resolved.
Show resolved Hide resolved
);

const arrayHandle = await this.evaluateHandle(
queryHandler,
queryHandlerToArray,
updatedSelector
);
const result = await arrayHandle.evaluate<
Expand All @@ -910,8 +908,8 @@ export class ElementHandle<
) => ReturnType | Promise<ReturnType>
>(pageFunction, ...args);
await arrayHandle.dispose();
/* This as exists for the same reason as the `as` in $eval above.
* See the comment there for a ful explanation.
/* This `as` exists for the same reason as the `as` in $eval above.
* See the comment there for a full explanation.
johanbay marked this conversation as resolved.
Show resolved Hide resolved
*/
return result as WrapElementHandle<ReturnType>;
}
Expand Down
24 changes: 15 additions & 9 deletions src/common/QueryHandler.ts
Expand Up @@ -15,17 +15,18 @@
*/

export interface QueryHandler {
(element: Element | Document, selector: string):
| Element
| Element[]
| NodeListOf<Element>;
queryOne?: (element: Element | Document, selector: string) => Element;
queryAll?: (
element: Element | Document,
selector: string
) => Element[] | NodeListOf<Element>;
}

const _customQueryHandlers = new Map<string, QueryHandler>();

export function registerCustomQueryHandler(
name: string,
handler: Function
handler: QueryHandler
): void {
if (_customQueryHandlers.get(name))
throw new Error(`A custom query handler named "${name}" already exists`);
Expand All @@ -34,7 +35,7 @@ export function registerCustomQueryHandler(
if (!isValidName)
throw new Error(`Custom query handler names may only contain [a-zA-Z]`);

_customQueryHandlers.set(name, handler as QueryHandler);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice work getting rid of this as :)

_customQueryHandlers.set(name, handler);
}

/**
Expand All @@ -53,12 +54,17 @@ export function clearQueryHandlers(): void {
}

export function getQueryHandlerAndSelector(
selector: string,
defaultQueryHandler: QueryHandler
selector: string
): { 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);
if (!hasCustomQueryHandler)
return { updatedSelector: selector, queryHandler: defaultQueryHandler };
return { updatedSelector: selector, queryHandler: defaultHandler };

const index = selector.indexOf('/');
const name = selector.slice(0, index);
Expand Down
77 changes: 57 additions & 20 deletions test/elementhandle.spec.ts
Expand Up @@ -293,10 +293,10 @@ describe('ElementHandle specs', function () {
await page.setContent('<div id="not-foo"></div><div id="foo"></div>');

// Register.
puppeteer.__experimental_registerCustomQueryHandler(
'getById',
(element, selector) => document.querySelector(`[id="${selector}"]`)
);
puppeteer.__experimental_registerCustomQueryHandler('getById', {
queryOne: (element, selector) =>
document.querySelector(`[id="${selector}"]`),
});
const element = await page.$('getById/foo');
expect(
await page.evaluate<(element: HTMLElement) => string>(
Expand Down Expand Up @@ -340,10 +340,10 @@ describe('ElementHandle specs', function () {
await page.setContent(
'<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>'
);
puppeteer.__experimental_registerCustomQueryHandler(
'getByClass',
(element, selector) => document.querySelectorAll(`.${selector}`)
);
puppeteer.__experimental_registerCustomQueryHandler('getByClass', {
queryAll: (element, selector) =>
document.querySelectorAll(`.${selector}`),
});
const elements = await page.$$('getByClass/foo');
const classNames = await Promise.all(
elements.map(
Expand All @@ -362,10 +362,10 @@ describe('ElementHandle specs', function () {
await page.setContent(
'<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>'
);
puppeteer.__experimental_registerCustomQueryHandler(
'getByClass',
(element, selector) => document.querySelectorAll(`.${selector}`)
);
puppeteer.__experimental_registerCustomQueryHandler('getByClass', {
queryAll: (element, selector) =>
document.querySelectorAll(`.${selector}`),
});
const elements = await page.$$eval(
'getByClass/foo',
(divs) => divs.length
Expand All @@ -375,10 +375,9 @@ describe('ElementHandle specs', function () {
});
it('should wait correctly with waitForSelector', async () => {
const { page, puppeteer } = getTestState();
puppeteer.__experimental_registerCustomQueryHandler(
'getByClass',
(element, selector) => element.querySelector(`.${selector}`)
);
puppeteer.__experimental_registerCustomQueryHandler('getByClass', {
queryOne: (element, selector) => element.querySelector(`.${selector}`),
});
const waitFor = page.waitForSelector('getByClass/foo');

// Set the page content after the waitFor has been started.
Expand All @@ -391,10 +390,9 @@ describe('ElementHandle specs', function () {
});
it('should wait correctly with waitFor', async () => {
const { page, puppeteer } = getTestState();
puppeteer.__experimental_registerCustomQueryHandler(
'getByClass',
(element, selector) => element.querySelector(`.${selector}`)
);
puppeteer.__experimental_registerCustomQueryHandler('getByClass', {
queryOne: (element, selector) => element.querySelector(`.${selector}`),
});
mathiasbynens marked this conversation as resolved.
Show resolved Hide resolved
const waitFor = page.waitFor('getByClass/foo');

// Set the page content after the waitFor has been started.
Expand All @@ -405,5 +403,44 @@ describe('ElementHandle specs', function () {

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');
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice tests!

});
});