Skip to content

Commit

Permalink
feat!: type inference for evaluation types (#8547)
Browse files Browse the repository at this point in the history
This PR greatly improves the types within Puppeteer:

- **Almost everything** is auto-deduced.
  - Parameters don't need to be specified in the function. They are deduced from the spread.
  - Return types don't need to be specified. They are deduced from the function. (More on this below)
  - Selections based on tag names correctly deduce element type, similar to TypeScript's mechanism for `getElementByTagName`.
- [**BREAKING CHANGE**] We've removed the ability to declare return types in type arguments for the following reasons:
  1. Setting them will indubitably break auto-deduction.
  2. You can just use `as ...` in TypeScript to coerce the correct type (given it makes sense).
- [**BREAKING CHANGE**] `waitFor` is officially gone.

To migrate to these changes, there are only four things you may need to change:
- If you set a return type using the `ReturnType` type parameter, remove it and use `as ...` and `HandleFor` (if necessary).
⛔ `evaluate<ReturnType>(a: number, b: number) => {...}, a, b)`
✅ `(await evaluate(a, b) => {...}, a, b)) as ReturnType`
⛔ `evaluateHandle<ReturnType>(a: number, b: number) => {...}, a, b)`
✅ `(await evaluateHandle(a, b) => {...}, a, b)) as HandleFor<ReturnType>`
- If you set any type parameters in the *parameters* of an evaluation function, remove them.  
⛔ `evaluate(a: number, b: number) => {...}, a, b)`
✅ `evaluate(a, b) => {...}, a, b)`
- If you set any type parameters in the method's declaration, remove them.
⛔ `evaluate<(a: number, b: number) => void>((a, b) => {...}, a, b)`
✅ `evaluate(a, b) => {...}, a, b)`
  • Loading branch information
jrandolf committed Jun 23, 2022
1 parent da269c3 commit 26c3acb
Show file tree
Hide file tree
Showing 27 changed files with 701 additions and 775 deletions.
2 changes: 1 addition & 1 deletion src/api-docs-entry.ts
Expand Up @@ -72,7 +72,7 @@ export * from './common/Tracing.js';
export * from './common/NetworkManager.js';
export * from './common/WebWorker.js';
export * from './common/USKeyboardLayout.js';
export * from './common/EvalTypes.js';
export * from './common/types.js';
export * from './common/PDFOptions.js';
export * from './common/TimeoutSettings.js';
export * from './common/LifecycleWatcher.js';
Expand Down
4 changes: 2 additions & 2 deletions src/common/AriaQueryHandler.ts
Expand Up @@ -141,7 +141,7 @@ const queryAll = async (
const queryAllArray = async (
element: ElementHandle,
selector: string
): Promise<JSHandle> => {
): Promise<JSHandle<Element[]>> => {
const elementHandles = await queryAll(element, selector);
const exeCtx = element.executionContext();
const jsHandle = exeCtx.evaluateHandle((...elements) => {
Expand All @@ -153,7 +153,7 @@ const queryAllArray = async (
/**
* @internal
*/
export const _ariaHandler: InternalQueryHandler = {
export const ariaHandler: InternalQueryHandler = {
queryOne,
waitFor,
queryAll,
Expand Down
4 changes: 2 additions & 2 deletions src/common/Connection.ts
Expand Up @@ -33,8 +33,8 @@ export {ConnectionTransport, ProtocolMapping};
* @public
*/
export interface ConnectionCallback {
resolve: Function;
reject: Function;
resolve(args: unknown): void;
reject(args: unknown): void;
error: ProtocolError;
method: string;
}
Expand Down
175 changes: 112 additions & 63 deletions src/common/DOMWorld.ts
Expand Up @@ -18,28 +18,21 @@ import {Protocol} from 'devtools-protocol';
import {assert} from './assert.js';
import {CDPSession} from './Connection.js';
import {TimeoutError} from './Errors.js';
import {
EvaluateFn,
EvaluateFnReturnType,
EvaluateHandleFn,
SerializableOrJSHandle,
UnwrapPromiseLike,
WrapElementHandle,
} from './EvalTypes.js';
import {ExecutionContext} from './ExecutionContext.js';
import {Frame, FrameManager} from './FrameManager.js';
import {MouseButton} from './Input.js';
import {ElementHandle, JSHandle} from './JSHandle.js';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {_getQueryHandlerAndSelector} from './QueryHandler.js';
import {TimeoutSettings} from './TimeoutSettings.js';
import {EvaluateFunc, EvaluateParams, HandleFor} from './types.js';
import {
debugError,
isNumber,
isString,
makePredicateString,
pageBindingInitString,
} from './util.js';
import {MouseButton} from './Input.js';
import {ElementHandle, JSHandle} from './JSHandle.js';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {_getQueryHandlerAndSelector} from './QueryHandler.js';
import {TimeoutSettings} from './TimeoutSettings.js';

// predicateQueryHandler and checkWaitForOptions are declared here so that
// TypeScript knows about them when used in the predicate function below.
Expand Down Expand Up @@ -184,30 +177,45 @@ export class DOMWorld {
return this.#contextPromise;
}

async evaluateHandle<HandlerType extends JSHandle = JSHandle>(
pageFunction: EvaluateHandleFn,
...args: SerializableOrJSHandle[]
): Promise<HandlerType> {
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
const context = await this.executionContext();
return context.evaluateHandle(pageFunction, ...args);
}

async evaluate<T extends EvaluateFn>(
pageFunction: T,
...args: SerializableOrJSHandle[]
): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> {
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
const context = await this.executionContext();
return context.evaluate<UnwrapPromiseLike<EvaluateFnReturnType<T>>>(
pageFunction,
...args
);
return context.evaluate(pageFunction, ...args);
}

async $<T extends Element = Element>(
selector: string
): Promise<ElementHandle<T> | null> {
async $<Selector extends keyof HTMLElementTagNameMap>(
selector: Selector
): Promise<ElementHandle<HTMLElementTagNameMap[Selector]> | null>;
async $(selector: string): Promise<ElementHandle | null>;
async $(selector: string): Promise<ElementHandle | null> {
const document = await this._document();
const value = await document.$<T>(selector);
const value = await document.$(selector);
return value;
}

async $$<Selector extends keyof HTMLElementTagNameMap>(
selector: Selector
): Promise<ElementHandle<HTMLElementTagNameMap[Selector]>[]>;
async $$(selector: string): Promise<ElementHandle[]>;
async $$(selector: string): Promise<ElementHandle[]> {
const document = await this._document();
const value = await document.$$(selector);
return value;
}

Expand Down Expand Up @@ -235,40 +243,74 @@ export class DOMWorld {
return value;
}

async $eval<ReturnType>(
async $eval<
Selector extends keyof HTMLElementTagNameMap,
Params extends unknown[],
Func extends EvaluateFunc<
[HTMLElementTagNameMap[Selector], ...Params]
> = EvaluateFunc<[HTMLElementTagNameMap[Selector], ...Params]>
>(
selector: Selector,
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>>;
async $eval<
Params extends unknown[],
Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc<
[Element, ...Params]
>
>(
selector: string,
pageFunction: (
element: Element,
...args: unknown[]
) => ReturnType | Promise<ReturnType>,
...args: SerializableOrJSHandle[]
): Promise<WrapElementHandle<ReturnType>> {
const document = await this._document();
return document.$eval<ReturnType>(selector, pageFunction, ...args);
}

async $$eval<ReturnType>(
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>>;
async $eval<
Params extends unknown[],
Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc<
[Element, ...Params]
>
>(
selector: string,
pageFunction: (
elements: Element[],
...args: unknown[]
) => ReturnType | Promise<ReturnType>,
...args: SerializableOrJSHandle[]
): Promise<WrapElementHandle<ReturnType>> {
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
const document = await this._document();
const value = await document.$$eval<ReturnType>(
selector,
pageFunction,
...args
);
return value;
return document.$eval(selector, pageFunction, ...args);
}

async $$<T extends Element = Element>(
selector: string
): Promise<Array<ElementHandle<T>>> {
async $$eval<
Selector extends keyof HTMLElementTagNameMap,
Params extends unknown[],
Func extends EvaluateFunc<
[HTMLElementTagNameMap[Selector][], ...Params]
> = EvaluateFunc<[HTMLElementTagNameMap[Selector][], ...Params]>
>(
selector: Selector,
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>>;
async $$eval<
Params extends unknown[],
Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc<
[Element[], ...Params]
>
>(
selector: string,
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>>;
async $$eval<
Params extends unknown[],
Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc<
[Element[], ...Params]
>
>(
selector: string,
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
const document = await this._document();
const value = await document.$$<T>(selector);
const value = await document.$$eval(selector, pageFunction, ...args);
return value;
}

Expand Down Expand Up @@ -298,7 +340,7 @@ export class DOMWorld {
} = options;
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
// lifecycle event. @see https://crrev.com/608658
await this.evaluate<(x: string) => void>(html => {
await this.evaluate(html => {
document.open();
document.write(html);
document.close();
Expand Down Expand Up @@ -536,7 +578,6 @@ export class DOMWorld {

async function addStyleContent(content: string): Promise<HTMLElement> {
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(content));
const promise = new Promise((res, rej) => {
style.onload = res;
Expand Down Expand Up @@ -598,6 +639,14 @@ export class DOMWorld {
await handle.dispose();
}

async waitForSelector<Selector extends keyof HTMLElementTagNameMap>(
selector: Selector,
options: WaitForSelectorOptions
): Promise<ElementHandle<HTMLElementTagNameMap[Selector]> | null>;
async waitForSelector(
selector: string,
options: WaitForSelectorOptions
): Promise<ElementHandle | null>;
async waitForSelector(
selector: string,
options: WaitForSelectorOptions
Expand Down Expand Up @@ -825,7 +874,7 @@ export class DOMWorld {
waitForFunction(
pageFunction: Function | string,
options: {polling?: string | number; timeout?: number} = {},
...args: SerializableOrJSHandle[]
...args: unknown[]
): Promise<JSHandle> {
const {polling = 'raf', timeout = this.#timeoutSettings.timeout()} =
options;
Expand Down Expand Up @@ -860,7 +909,7 @@ export interface WaitTaskOptions {
polling: string | number;
timeout: number;
binding?: PageBinding;
args: SerializableOrJSHandle[];
args: unknown[];
root?: ElementHandle;
}

Expand All @@ -871,11 +920,11 @@ const noop = (): void => {};
*/
export class WaitTask {
#domWorld: DOMWorld;
#polling: string | number;
#polling: 'raf' | 'mutation' | number;
#timeout: number;
#predicateBody: string;
#predicateAcceptsContextElement: boolean;
#args: SerializableOrJSHandle[];
#args: unknown[];
#binding?: PageBinding;
#runCount = 0;
#resolve: (x: JSHandle) => void = noop;
Expand Down
83 changes: 0 additions & 83 deletions src/common/EvalTypes.ts

This file was deleted.

0 comments on commit 26c3acb

Please sign in to comment.