feat!: type inference for evaluation types (#8547)

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)`
This commit is contained in:
jrandolf 2022-06-23 11:29:46 +02:00 committed by GitHub
parent da269c3f32
commit 26c3acbb07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 701 additions and 775 deletions

View File

@ -72,7 +72,7 @@ export * from './common/Tracing.js';
export * from './common/NetworkManager.js'; export * from './common/NetworkManager.js';
export * from './common/WebWorker.js'; export * from './common/WebWorker.js';
export * from './common/USKeyboardLayout.js'; export * from './common/USKeyboardLayout.js';
export * from './common/EvalTypes.js'; export * from './common/types.js';
export * from './common/PDFOptions.js'; export * from './common/PDFOptions.js';
export * from './common/TimeoutSettings.js'; export * from './common/TimeoutSettings.js';
export * from './common/LifecycleWatcher.js'; export * from './common/LifecycleWatcher.js';

View File

@ -141,7 +141,7 @@ const queryAll = async (
const queryAllArray = async ( const queryAllArray = async (
element: ElementHandle, element: ElementHandle,
selector: string selector: string
): Promise<JSHandle> => { ): Promise<JSHandle<Element[]>> => {
const elementHandles = await queryAll(element, selector); const elementHandles = await queryAll(element, selector);
const exeCtx = element.executionContext(); const exeCtx = element.executionContext();
const jsHandle = exeCtx.evaluateHandle((...elements) => { const jsHandle = exeCtx.evaluateHandle((...elements) => {
@ -153,7 +153,7 @@ const queryAllArray = async (
/** /**
* @internal * @internal
*/ */
export const _ariaHandler: InternalQueryHandler = { export const ariaHandler: InternalQueryHandler = {
queryOne, queryOne,
waitFor, waitFor,
queryAll, queryAll,

View File

@ -33,8 +33,8 @@ export {ConnectionTransport, ProtocolMapping};
* @public * @public
*/ */
export interface ConnectionCallback { export interface ConnectionCallback {
resolve: Function; resolve(args: unknown): void;
reject: Function; reject(args: unknown): void;
error: ProtocolError; error: ProtocolError;
method: string; method: string;
} }

View File

@ -18,16 +18,14 @@ import {Protocol} from 'devtools-protocol';
import {assert} from './assert.js'; import {assert} from './assert.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {TimeoutError} from './Errors.js'; import {TimeoutError} from './Errors.js';
import {
EvaluateFn,
EvaluateFnReturnType,
EvaluateHandleFn,
SerializableOrJSHandle,
UnwrapPromiseLike,
WrapElementHandle,
} from './EvalTypes.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {Frame, FrameManager} from './FrameManager.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 { import {
debugError, debugError,
isNumber, isNumber,
@ -35,11 +33,6 @@ import {
makePredicateString, makePredicateString,
pageBindingInitString, pageBindingInitString,
} from './util.js'; } 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 // predicateQueryHandler and checkWaitForOptions are declared here so that
// TypeScript knows about them when used in the predicate function below. // TypeScript knows about them when used in the predicate function below.
@ -184,30 +177,45 @@ export class DOMWorld {
return this.#contextPromise; return this.#contextPromise;
} }
async evaluateHandle<HandlerType extends JSHandle = JSHandle>( async evaluateHandle<
pageFunction: EvaluateHandleFn, Params extends unknown[],
...args: SerializableOrJSHandle[] Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
): Promise<HandlerType> { >(
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
const context = await this.executionContext(); const context = await this.executionContext();
return context.evaluateHandle(pageFunction, ...args); return context.evaluateHandle(pageFunction, ...args);
} }
async evaluate<T extends EvaluateFn>( async evaluate<
pageFunction: T, Params extends unknown[],
...args: SerializableOrJSHandle[] Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> { >(
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
const context = await this.executionContext(); const context = await this.executionContext();
return context.evaluate<UnwrapPromiseLike<EvaluateFnReturnType<T>>>( return context.evaluate(pageFunction, ...args);
pageFunction,
...args
);
} }
async $<T extends Element = Element>( async $<Selector extends keyof HTMLElementTagNameMap>(
selector: string selector: Selector
): Promise<ElementHandle<T> | null> { ): Promise<ElementHandle<HTMLElementTagNameMap[Selector]> | null>;
async $(selector: string): Promise<ElementHandle | null>;
async $(selector: string): Promise<ElementHandle | null> {
const document = await this._document(); 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; return value;
} }
@ -235,40 +243,74 @@ export class DOMWorld {
return value; 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, selector: string,
pageFunction: ( pageFunction: Func | string,
element: Element, ...args: EvaluateParams<Params>
...args: unknown[] ): Promise<Awaited<ReturnType<Func>>>;
) => ReturnType | Promise<ReturnType>, async $eval<
...args: SerializableOrJSHandle[] Params extends unknown[],
): Promise<WrapElementHandle<ReturnType>> { 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 document = await this._document();
return document.$eval<ReturnType>(selector, pageFunction, ...args); return document.$eval(selector, pageFunction, ...args);
} }
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, selector: string,
pageFunction: ( pageFunction: Func | string,
elements: Element[], ...args: EvaluateParams<Params>
...args: unknown[] ): Promise<Awaited<ReturnType<Func>>>;
) => ReturnType | Promise<ReturnType>, async $$eval<
...args: SerializableOrJSHandle[] Params extends unknown[],
): Promise<WrapElementHandle<ReturnType>> { 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 document = await this._document();
const value = await document.$$eval<ReturnType>( const value = await document.$$eval(selector, pageFunction, ...args);
selector,
pageFunction,
...args
);
return value;
}
async $$<T extends Element = Element>(
selector: string
): Promise<Array<ElementHandle<T>>> {
const document = await this._document();
const value = await document.$$<T>(selector);
return value; return value;
} }
@ -298,7 +340,7 @@ export class DOMWorld {
} = options; } = options;
// We rely upon the fact that document.open() will reset frame lifecycle with "init" // We rely upon the fact that document.open() will reset frame lifecycle with "init"
// lifecycle event. @see https://crrev.com/608658 // lifecycle event. @see https://crrev.com/608658
await this.evaluate<(x: string) => void>(html => { await this.evaluate(html => {
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
@ -536,7 +578,6 @@ export class DOMWorld {
async function addStyleContent(content: string): Promise<HTMLElement> { async function addStyleContent(content: string): Promise<HTMLElement> {
const style = document.createElement('style'); const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(content)); style.appendChild(document.createTextNode(content));
const promise = new Promise((res, rej) => { const promise = new Promise((res, rej) => {
style.onload = res; style.onload = res;
@ -598,6 +639,14 @@ export class DOMWorld {
await handle.dispose(); 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( async waitForSelector(
selector: string, selector: string,
options: WaitForSelectorOptions options: WaitForSelectorOptions
@ -825,7 +874,7 @@ export class DOMWorld {
waitForFunction( waitForFunction(
pageFunction: Function | string, pageFunction: Function | string,
options: {polling?: string | number; timeout?: number} = {}, options: {polling?: string | number; timeout?: number} = {},
...args: SerializableOrJSHandle[] ...args: unknown[]
): Promise<JSHandle> { ): Promise<JSHandle> {
const {polling = 'raf', timeout = this.#timeoutSettings.timeout()} = const {polling = 'raf', timeout = this.#timeoutSettings.timeout()} =
options; options;
@ -860,7 +909,7 @@ export interface WaitTaskOptions {
polling: string | number; polling: string | number;
timeout: number; timeout: number;
binding?: PageBinding; binding?: PageBinding;
args: SerializableOrJSHandle[]; args: unknown[];
root?: ElementHandle; root?: ElementHandle;
} }
@ -871,11 +920,11 @@ const noop = (): void => {};
*/ */
export class WaitTask { export class WaitTask {
#domWorld: DOMWorld; #domWorld: DOMWorld;
#polling: string | number; #polling: 'raf' | 'mutation' | number;
#timeout: number; #timeout: number;
#predicateBody: string; #predicateBody: string;
#predicateAcceptsContextElement: boolean; #predicateAcceptsContextElement: boolean;
#args: SerializableOrJSHandle[]; #args: unknown[];
#binding?: PageBinding; #binding?: PageBinding;
#runCount = 0; #runCount = 0;
#resolve: (x: JSHandle) => void = noop; #resolve: (x: JSHandle) => void = noop;

View File

@ -1,83 +0,0 @@
/**
* Copyright 2020 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 {JSHandle, ElementHandle} from './JSHandle.js';
/**
* @public
*/
export type EvaluateFn<T = any, U = any, V = any> =
| string
| ((arg1: T, ...args: U[]) => V);
/**
* @public
*/
export type UnwrapPromiseLike<T> = T extends PromiseLike<infer U> ? U : T;
/**
* @public
*/
export type EvaluateFnReturnType<T extends EvaluateFn> = T extends (
...args: any[]
) => infer R
? R
: any;
/**
* @public
*/
export type EvaluateHandleFn = string | ((...args: any[]) => any);
/**
* @public
*/
export type Serializable =
| number
| string
| boolean
| null
| bigint
| JSONArray
| JSONObject;
/**
* @public
*/
export type JSONArray = readonly Serializable[];
/**
* @public
*/
export interface JSONObject {
[key: string]: Serializable;
}
/**
* @public
*/
export type SerializableOrJSHandle = Serializable | JSHandle;
/**
* Wraps a DOM element into an ElementHandle instance
* @public
**/
export type WrapElementHandle<X> = X extends Element ? ElementHandle<X> : X;
/**
* Unwraps a DOM element out of an ElementHandle instance
* @public
**/
export type UnwrapElementHandle<X> = X extends ElementHandle<infer E> ? E : X;

View File

@ -18,10 +18,10 @@ import {Protocol} from 'devtools-protocol';
import {assert} from './assert.js'; import {assert} from './assert.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {DOMWorld} from './DOMWorld.js'; import {DOMWorld} from './DOMWorld.js';
import {EvaluateHandleFn, SerializableOrJSHandle} from './EvalTypes.js'; import {EvaluateFunc, HandleFor, EvaluateParams} from './types.js';
import {Frame} from './FrameManager.js'; import {Frame} from './FrameManager.js';
import {getExceptionMessage, isString, valueFromRemoteObject} from './util.js';
import {ElementHandle, JSHandle, _createJSHandle} from './JSHandle.js'; import {ElementHandle, JSHandle, _createJSHandle} from './JSHandle.js';
import {getExceptionMessage, isString, valueFromRemoteObject} from './util.js';
/** /**
* @public * @public
@ -134,11 +134,14 @@ export class ExecutionContext {
* *
* @returns A promise that resolves to the return value of the given function. * @returns A promise that resolves to the return value of the given function.
*/ */
async evaluate<ReturnType>( async evaluate<
pageFunction: Function | string, Params extends unknown[],
...args: unknown[] Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
): Promise<ReturnType> { >(
return await this.#evaluate<ReturnType>(true, pageFunction, ...args); pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
return await this.#evaluate(true, pageFunction, ...args);
} }
/** /**
@ -183,18 +186,40 @@ export class ExecutionContext {
* @returns A promise that resolves to the return value of the given function * @returns A promise that resolves to the return value of the given function
* as an in-page object (a {@link JSHandle}). * as an in-page object (a {@link JSHandle}).
*/ */
async evaluateHandle<HandleType extends JSHandle | ElementHandle = JSHandle>( async evaluateHandle<
pageFunction: EvaluateHandleFn, Params extends unknown[],
...args: SerializableOrJSHandle[] Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
): Promise<HandleType> { >(
return this.#evaluate<HandleType>(false, pageFunction, ...args); pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this.#evaluate(false, pageFunction, ...args);
} }
async #evaluate<ReturnType>( async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
returnByValue: true,
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>>;
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
returnByValue: false,
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
async #evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
returnByValue: boolean, returnByValue: boolean,
pageFunction: Function | string, pageFunction: Func | string,
...args: unknown[] ...args: EvaluateParams<Params>
): Promise<ReturnType> { ): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`; const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`;
if (isString(pageFunction)) { if (isString(pageFunction)) {
@ -365,7 +390,9 @@ export class ExecutionContext {
* *
* @returns A handle to an array of objects with the given prototype. * @returns A handle to an array of objects with the given prototype.
*/ */
async queryObjects(prototypeHandle: JSHandle): Promise<JSHandle> { async queryObjects<Prototype>(
prototypeHandle: JSHandle<Prototype>
): Promise<HandleFor<Prototype[]>> {
assert(!prototypeHandle._disposed, 'Prototype JSHandle is disposed!'); assert(!prototypeHandle._disposed, 'Prototype JSHandle is disposed!');
assert( assert(
prototypeHandle._remoteObject.objectId, prototypeHandle._remoteObject.objectId,
@ -374,7 +401,7 @@ export class ExecutionContext {
const response = await this._client.send('Runtime.queryObjects', { const response = await this._client.send('Runtime.queryObjects', {
prototypeObjectId: prototypeHandle._remoteObject.objectId, prototypeObjectId: prototypeHandle._remoteObject.objectId,
}); });
return _createJSHandle(this, response.objects); return _createJSHandle(this, response.objects) as HandleFor<Prototype[]>;
} }
/** /**

View File

@ -37,7 +37,7 @@ import {assert} from './assert.js';
* @public * @public
*/ */
export class FileChooser { export class FileChooser {
#element: ElementHandle; #element: ElementHandle<HTMLInputElement>;
#multiple: boolean; #multiple: boolean;
#handled = false; #handled = false;
@ -45,7 +45,7 @@ export class FileChooser {
* @internal * @internal
*/ */
constructor( constructor(
element: ElementHandle, element: ElementHandle<HTMLInputElement>,
event: Protocol.Page.FileChooserOpenedEvent event: Protocol.Page.FileChooserOpenedEvent
) { ) {
this.#element = element; this.#element = element;

View File

@ -18,27 +18,19 @@ import {Protocol} from 'devtools-protocol';
import {assert} from './assert.js'; import {assert} from './assert.js';
import {CDPSession, Connection} from './Connection.js'; import {CDPSession, Connection} from './Connection.js';
import {DOMWorld, WaitForSelectorOptions} from './DOMWorld.js'; import {DOMWorld, WaitForSelectorOptions} from './DOMWorld.js';
import {
EvaluateFn,
EvaluateFnReturnType,
EvaluateHandleFn,
SerializableOrJSHandle,
UnwrapPromiseLike,
WrapElementHandle,
} from './EvalTypes.js';
import {EventEmitter} from './EventEmitter.js'; import {EventEmitter} from './EventEmitter.js';
import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js'; import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js';
import {HTTPResponse} from './HTTPResponse.js'; import {HTTPResponse} from './HTTPResponse.js';
import {MouseButton} from './Input.js'; import {MouseButton} from './Input.js';
import {ElementHandle, JSHandle} from './JSHandle.js'; import {ElementHandle} from './JSHandle.js';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {NetworkManager} from './NetworkManager.js'; import {NetworkManager} from './NetworkManager.js';
import {Page} from './Page.js'; import {Page} from './Page.js';
import {TimeoutSettings} from './TimeoutSettings.js'; import {TimeoutSettings} from './TimeoutSettings.js';
import {debugError, isErrorLike, isNumber, isString} from './util.js'; import {EvaluateFunc, EvaluateParams, HandleFor} from './types.js';
import {debugError, isErrorLike} from './util.js';
const UTILITY_WORLD_NAME = '__puppeteer_utility_world__'; const UTILITY_WORLD_NAME = '__puppeteer_utility_world__';
const xPathPattern = /^\(\/\/[^\)]+\)|^\/\//;
/** /**
* We use symbols to prevent external parties listening to these events. * We use symbols to prevent external parties listening to these events.
@ -892,11 +884,14 @@ export class Frame {
* @param pageFunction - a function that is run within the frame * @param pageFunction - a function that is run within the frame
* @param args - arguments to be passed to the pageFunction * @param args - arguments to be passed to the pageFunction
*/ */
async evaluateHandle<HandlerType extends JSHandle = JSHandle>( async evaluateHandle<
pageFunction: EvaluateHandleFn, Params extends unknown[],
...args: SerializableOrJSHandle[] Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
): Promise<HandlerType> { >(
return this._mainWorld.evaluateHandle<HandlerType>(pageFunction, ...args); pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this._mainWorld.evaluateHandle(pageFunction, ...args);
} }
/** /**
@ -908,11 +903,14 @@ export class Frame {
* @param pageFunction - a function that is run within the frame * @param pageFunction - a function that is run within the frame
* @param args - arguments to be passed to the pageFunction * @param args - arguments to be passed to the pageFunction
*/ */
async evaluate<T extends EvaluateFn>( async evaluate<
pageFunction: T, Params extends unknown[],
...args: SerializableOrJSHandle[] Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> { >(
return this._mainWorld.evaluate<T>(pageFunction, ...args); pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
return this._mainWorld.evaluate(pageFunction, ...args);
} }
/** /**
@ -922,10 +920,26 @@ export class Frame {
* @returns A promise which resolves to an `ElementHandle` pointing at the * @returns A promise which resolves to an `ElementHandle` pointing at the
* element, or `null` if it was not found. * element, or `null` if it was not found.
*/ */
async $<T extends Element = Element>( async $<Selector extends keyof HTMLElementTagNameMap>(
selector: string selector: Selector
): Promise<ElementHandle<T> | null> { ): Promise<ElementHandle<HTMLElementTagNameMap[Selector]> | null>;
return this._mainWorld.$<T>(selector); async $(selector: string): Promise<ElementHandle | null>;
async $(selector: string): Promise<ElementHandle | null> {
return this._mainWorld.$(selector);
}
/**
* This runs `document.querySelectorAll` in the frame and returns the result.
*
* @param selector - a selector to search for
* @returns An array of element handles pointing to the found frame elements.
*/
async $$<Selector extends keyof HTMLElementTagNameMap>(
selector: Selector
): Promise<ElementHandle<HTMLElementTagNameMap[Selector]>[]>;
async $$(selector: string): Promise<ElementHandle[]>;
async $$(selector: string): Promise<ElementHandle[]> {
return this._mainWorld.$$(selector);
} }
/** /**
@ -956,15 +970,38 @@ export class Frame {
* @param pageFunction - the function to be evaluated in the frame's context * @param pageFunction - the function to be evaluated in the frame's context
* @param args - additional arguments to pass to `pageFunction` * @param args - additional arguments to pass to `pageFunction`
*/ */
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, selector: string,
pageFunction: ( pageFunction: Func | string,
element: Element, ...args: EvaluateParams<Params>
...args: unknown[] ): Promise<Awaited<ReturnType<Func>>>;
) => ReturnType | Promise<ReturnType>, async $eval<
...args: SerializableOrJSHandle[] Params extends unknown[],
): Promise<WrapElementHandle<ReturnType>> { Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc<
return this._mainWorld.$eval<ReturnType>(selector, pageFunction, ...args); [Element, ...Params]
>
>(
selector: string,
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
return this._mainWorld.$eval(selector, pageFunction, ...args);
} }
/** /**
@ -986,27 +1023,38 @@ export class Frame {
* @param pageFunction - the function to be evaluated in the frame's context * @param pageFunction - the function to be evaluated in the frame's context
* @param args - additional arguments to pass to `pageFunction` * @param args - additional arguments to pass to `pageFunction`
*/ */
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, selector: string,
pageFunction: ( pageFunction: Func | string,
elements: Element[], ...args: EvaluateParams<Params>
...args: unknown[] ): Promise<Awaited<ReturnType<Func>>>;
) => ReturnType | Promise<ReturnType>, async $$eval<
...args: SerializableOrJSHandle[] Params extends unknown[],
): Promise<WrapElementHandle<ReturnType>> { Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc<
return this._mainWorld.$$eval<ReturnType>(selector, pageFunction, ...args); [Element[], ...Params]
} >
>(
/** selector: string,
* This runs `document.querySelectorAll` in the frame and returns the result. pageFunction: Func | string,
* ...args: EvaluateParams<Params>
* @param selector - a selector to search for ): Promise<Awaited<ReturnType<Func>>> {
* @returns An array of element handles pointing to the found frame elements. return this._mainWorld.$$eval(selector, pageFunction, ...args);
*/
async $$<T extends Element = Element>(
selector: string
): Promise<Array<ElementHandle<T>>> {
return this._mainWorld.$$<T>(selector);
} }
/** /**
@ -1238,66 +1286,6 @@ export class Frame {
return this._mainWorld.type(selector, text, options); return this._mainWorld.type(selector, text, options);
} }
/**
* @remarks
*
* This method behaves differently depending on the first parameter. If it's a
* `string`, it will be treated as a `selector` or `xpath` (if the string
* starts with `//`). This method then is a shortcut for
* {@link Frame.waitForSelector} or {@link Frame.waitForXPath}.
*
* If the first argument is a function this method is a shortcut for
* {@link Frame.waitForFunction}.
*
* If the first argument is a `number`, it's treated as a timeout in
* milliseconds and the method returns a promise which resolves after the
* timeout.
*
* @param selectorOrFunctionOrTimeout - a selector, predicate or timeout to
* wait for.
* @param options - optional waiting parameters.
* @param args - arguments to pass to `pageFunction`.
*
* @deprecated Don't use this method directly. Instead use the more explicit
* methods available: {@link Frame.waitForSelector},
* {@link Frame.waitForXPath}, {@link Frame.waitForFunction} or
* {@link Frame.waitForTimeout}.
*/
waitFor(
selectorOrFunctionOrTimeout: string | number | Function,
options: Record<string, unknown> = {},
...args: SerializableOrJSHandle[]
): Promise<JSHandle | null> {
console.warn(
'waitFor is deprecated and will be removed in a future release. See https://github.com/puppeteer/puppeteer/issues/6214 for details and how to migrate your code.'
);
if (isString(selectorOrFunctionOrTimeout)) {
const string = selectorOrFunctionOrTimeout;
if (xPathPattern.test(string)) {
return this.waitForXPath(string, options);
}
return this.waitForSelector(string, options);
}
if (isNumber(selectorOrFunctionOrTimeout)) {
return new Promise(fulfill => {
return setTimeout(fulfill, selectorOrFunctionOrTimeout);
});
}
if (typeof selectorOrFunctionOrTimeout === 'function') {
return this.waitForFunction(
selectorOrFunctionOrTimeout,
options,
...args
);
}
return Promise.reject(
new Error(
'Unsupported target type: ' + typeof selectorOrFunctionOrTimeout
)
);
}
/** /**
* Causes your script to wait for the given number of milliseconds. * Causes your script to wait for the given number of milliseconds.
* *
@ -1357,6 +1345,14 @@ export class Frame {
* @returns a promise which resolves when an element matching the selector * @returns a promise which resolves when an element matching the selector
* string is added to the DOM. * string is added to the DOM.
*/ */
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( async waitForSelector(
selector: string, selector: string,
options: WaitForSelectorOptions = {} options: WaitForSelectorOptions = {}
@ -1438,12 +1434,20 @@ export class Frame {
* @param args - arguments to pass to the `pageFunction`. * @param args - arguments to pass to the `pageFunction`.
* @returns the promise which resolve when the `pageFunction` returns a truthy value. * @returns the promise which resolve when the `pageFunction` returns a truthy value.
*/ */
waitForFunction( waitForFunction<
pageFunction: Function | string, Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
options: FrameWaitForFunctionOptions = {}, options: FrameWaitForFunctionOptions = {},
...args: SerializableOrJSHandle[] ...args: EvaluateParams<Params>
): Promise<JSHandle> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this._mainWorld.waitForFunction(pageFunction, options, ...args); // TODO: Fix when NodeHandle has been added.
return this._mainWorld.waitForFunction(
pageFunction,
options,
...args
) as Promise<HandleFor<Awaited<ReturnType<Func>>>>;
} }
/** /**

View File

@ -17,14 +17,7 @@
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {assert} from './assert.js'; import {assert} from './assert.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import { import {EvaluateFunc, EvaluateParams, HandleFor, HandleOr} from './types.js';
EvaluateFn,
EvaluateFnReturnType,
EvaluateHandleFn,
SerializableOrJSHandle,
UnwrapPromiseLike,
WrapElementHandle,
} from './EvalTypes.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {Frame, FrameManager} from './FrameManager.js'; import {Frame, FrameManager} from './FrameManager.js';
import {MouseButton} from './Input.js'; import {MouseButton} from './Input.js';
@ -37,6 +30,7 @@ import {
releaseObject, releaseObject,
valueFromRemoteObject, valueFromRemoteObject,
} from './util.js'; } from './util.js';
import {WaitForSelectorOptions} from './DOMWorld.js';
/** /**
* @public * @public
@ -70,7 +64,7 @@ export interface BoundingBox extends Point {
export function _createJSHandle( export function _createJSHandle(
context: ExecutionContext, context: ExecutionContext,
remoteObject: Protocol.Runtime.RemoteObject remoteObject: Protocol.Runtime.RemoteObject
): JSHandle { ): JSHandle | ElementHandle {
const frame = context.frame(); const frame = context.frame();
if (remoteObject.subtype === 'node' && frame) { if (remoteObject.subtype === 'node' && frame) {
const frameManager = frame._frameManager; const frameManager = frame._frameManager;
@ -114,7 +108,7 @@ const applyOffsetsToQuad = (
* *
* @public * @public
*/ */
export class JSHandle<HandleObjectType = unknown> { export class JSHandle<T = unknown> {
#client: CDPSession; #client: CDPSession;
#disposed = false; #disposed = false;
#context: ExecutionContext; #context: ExecutionContext;
@ -179,13 +173,14 @@ export class JSHandle<HandleObjectType = unknown> {
* ``` * ```
*/ */
async evaluate<T extends EvaluateFn<HandleObjectType>>( async evaluate<
pageFunction: T | string, Params extends unknown[],
...args: SerializableOrJSHandle[] Func extends EvaluateFunc<[T, ...Params]> = EvaluateFunc<[T, ...Params]>
): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> { >(
return await this.executionContext().evaluate< pageFunction: Func | string,
UnwrapPromiseLike<EvaluateFnReturnType<T>> ...args: EvaluateParams<Params>
>(pageFunction, this, ...args); ): Promise<Awaited<ReturnType<Func>>> {
return await this.executionContext().evaluate(pageFunction, this, ...args);
} }
/** /**
@ -203,10 +198,13 @@ export class JSHandle<HandleObjectType = unknown> {
* *
* See {@link Page.evaluateHandle} for more details. * See {@link Page.evaluateHandle} for more details.
*/ */
async evaluateHandle<HandleType extends JSHandle = JSHandle>( async evaluateHandle<
pageFunction: EvaluateHandleFn, Params extends unknown[],
...args: SerializableOrJSHandle[] Func extends EvaluateFunc<[T, ...Params]> = EvaluateFunc<[T, ...Params]>
): Promise<HandleType> { >(
pageFunction: Func,
...args: EvaluateParams<Params>
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return await this.executionContext().evaluateHandle( return await this.executionContext().evaluateHandle(
pageFunction, pageFunction,
this, this,
@ -214,22 +212,19 @@ export class JSHandle<HandleObjectType = unknown> {
); );
} }
/** Fetches a single property from the referenced object. /**
* Fetches a single property from the referenced object.
*/ */
async getProperty(propertyName: string): Promise<JSHandle> { async getProperty<K extends keyof T>(
const objectHandle = await this.evaluateHandle( propertyName: HandleOr<K>
(object: Element, propertyName: keyof Element) => { ): Promise<HandleFor<T[K]>>;
const result: Record<string, unknown> = {__proto__: null}; async getProperty(propertyName: string): Promise<JSHandle<unknown>>;
result[propertyName] = object[propertyName]; async getProperty<K extends keyof T>(
return result; propertyName: HandleOr<K>
}, ): Promise<HandleFor<T[K]>> {
propertyName return await this.evaluateHandle((object, propertyName) => {
); return object[propertyName];
const properties = await objectHandle.getProperties(); }, propertyName);
const result = properties.get(propertyName);
assert(result instanceof JSHandle);
await objectHandle.dispose();
return result;
} }
/** /**
@ -412,13 +407,17 @@ export class ElementHandle<
* (30 seconds). Pass `0` to disable timeout. The default value can be changed * (30 seconds). Pass `0` to disable timeout. The default value can be changed
* by using the {@link Page.setDefaultTimeout} method. * by using the {@link Page.setDefaultTimeout} method.
*/ */
async waitForSelector<Selector extends keyof HTMLElementTagNameMap>(
selector: Selector,
options?: Exclude<WaitForSelectorOptions, 'root'>
): Promise<ElementHandle<HTMLElementTagNameMap[Selector]> | null>;
async waitForSelector( async waitForSelector(
selector: string, selector: string,
options: { options?: Exclude<WaitForSelectorOptions, 'root'>
visible?: boolean; ): Promise<ElementHandle | null>;
hidden?: boolean; async waitForSelector(
timeout?: number; selector: string,
} = {} options: Exclude<WaitForSelectorOptions, 'root'> = {}
): Promise<ElementHandle | null> { ): Promise<ElementHandle | null> {
const frame = this._context.frame(); const frame = this._context.frame();
assert(frame); assert(frame);
@ -539,10 +538,7 @@ export class ElementHandle<
async #scrollIntoViewIfNeeded(): Promise<void> { async #scrollIntoViewIfNeeded(): Promise<void> {
const error = await this.evaluate( const error = await this.evaluate(
async ( async (element, pageJavascriptEnabled): Promise<string | false> => {
element: Element,
pageJavascriptEnabled: boolean
): Promise<string | false> => {
if (!element.isConnected) { if (!element.isConnected) {
return 'Node is detached from document'; return 'Node is detached from document';
} }
@ -828,7 +824,7 @@ export class ElementHandle<
); );
} }
return this.evaluate((element: Element, vals: string[]): string[] => { return this.evaluate((element, vals): string[] => {
const values = new Set(vals); const values = new Set(vals);
if (!(element instanceof HTMLSelectElement)) { if (!(element instanceof HTMLSelectElement)) {
throw new Error('Element is not a <select> element.'); throw new Error('Element is not a <select> element.');
@ -870,15 +866,13 @@ export class ElementHandle<
* Note for locals script connecting to remote chrome environments, * Note for locals script connecting to remote chrome environments,
* paths must be absolute. * paths must be absolute.
*/ */
async uploadFile(...filePaths: string[]): Promise<void> { async uploadFile(
const isMultiple = await this.evaluate<(element: Element) => boolean>( this: ElementHandle<HTMLInputElement>,
element => { ...filePaths: string[]
if (!(element instanceof HTMLInputElement)) { ): Promise<void> {
throw new Error('uploadFile can only be called on an input element.'); const isMultiple = await this.evaluate(element => {
} return element.multiple;
return element.multiple; });
}
);
assert( assert(
filePaths.length <= 1 || isMultiple, filePaths.length <= 1 || isMultiple,
'Multiple file uploads only work with <input type=file multiple>' 'Multiple file uploads only work with <input type=file multiple>'
@ -912,7 +906,7 @@ export class ElementHandle<
so the solution is to eval the element value to a new FileList directly. so the solution is to eval the element value to a new FileList directly.
*/ */
if (files.length === 0) { if (files.length === 0) {
await (this as ElementHandle<HTMLInputElement>).evaluate(element => { await this.evaluate(element => {
element.files = new DataTransfer().files; element.files = new DataTransfer().files;
// Dispatch events for this case because it should behave akin to a user action. // Dispatch events for this case because it should behave akin to a user action.
@ -943,7 +937,10 @@ export class ElementHandle<
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element. * Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element.
*/ */
async focus(): Promise<void> { async focus(): Promise<void> {
await (this as ElementHandle<HTMLElement>).evaluate(element => { await this.evaluate(element => {
if (!(element instanceof HTMLElement)) {
throw new Error('Cannot focus non-HTMLElement');
}
return element.focus(); return element.focus();
}); });
} }
@ -1126,9 +1123,11 @@ export class ElementHandle<
* @returns `null` if no element matches the selector. * @returns `null` if no element matches the selector.
* @throws `Error` if the selector has no associated query handler. * @throws `Error` if the selector has no associated query handler.
*/ */
async $<T extends Element = Element>( async $<Selector extends keyof HTMLElementTagNameMap>(
selector: string selector: Selector
): Promise<ElementHandle<T> | null> { ): Promise<ElementHandle<HTMLElementTagNameMap[Selector]> | null>;
async $(selector: string): Promise<ElementHandle | null>;
async $(selector: string): Promise<ElementHandle | null> {
const {updatedSelector, queryHandler} = const {updatedSelector, queryHandler} =
_getQueryHandlerAndSelector(selector); _getQueryHandlerAndSelector(selector);
assert( assert(
@ -1149,9 +1148,11 @@ export class ElementHandle<
* @returns `[]` if no element matches the selector. * @returns `[]` if no element matches the selector.
* @throws `Error` if the selector has no associated query handler. * @throws `Error` if the selector has no associated query handler.
*/ */
async $$<T extends Element = Element>( async $$<Selector extends keyof HTMLElementTagNameMap>(
selector: string selector: Selector
): Promise<Array<ElementHandle<T>>> { ): Promise<ElementHandle<HTMLElementTagNameMap[Selector]>[]>;
async $$(selector: string): Promise<ElementHandle[]>;
async $$(selector: string): Promise<ElementHandle[]> {
const {updatedSelector, queryHandler} = const {updatedSelector, queryHandler} =
_getQueryHandlerAndSelector(selector); _getQueryHandlerAndSelector(selector);
assert( assert(
@ -1176,37 +1177,46 @@ export class ElementHandle<
* expect(await tweetHandle.$eval('.retweets', node => node.innerText)).toBe('10'); * expect(await tweetHandle.$eval('.retweets', node => node.innerText)).toBe('10');
* ``` * ```
*/ */
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, selector: string,
pageFunction: ( pageFunction: Func | string,
element: Element, ...args: EvaluateParams<Params>
...args: unknown[] ): Promise<Awaited<ReturnType<Func>>>;
) => ReturnType | Promise<ReturnType>, async $eval<
...args: SerializableOrJSHandle[] Params extends unknown[],
): Promise<WrapElementHandle<ReturnType>> { Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc<
[Element, ...Params]
>
>(
selector: string,
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
const elementHandle = await this.$(selector); const elementHandle = await this.$(selector);
if (!elementHandle) { if (!elementHandle) {
throw new Error( throw new Error(
`Error: failed to find element matching selector "${selector}"` `Error: failed to find element matching selector "${selector}"`
); );
} }
const result = await elementHandle.evaluate< const result = await elementHandle.evaluate(pageFunction, ...args);
(
element: Element,
...args: SerializableOrJSHandle[]
) => ReturnType | Promise<ReturnType>
>(pageFunction, ...args);
await elementHandle.dispose(); await elementHandle.dispose();
return result;
/**
* 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
* ReturnType if it isn't.
*/
return result as WrapElementHandle<ReturnType>;
} }
/** /**
@ -1232,28 +1242,44 @@ export class ElementHandle<
* .toEqual(['Hello!', 'Hi!']); * .toEqual(['Hello!', 'Hi!']);
* ``` * ```
*/ */
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, selector: string,
pageFunction: EvaluateFn< pageFunction: Func | string,
Element[], ...args: EvaluateParams<Params>
unknown, ): Promise<Awaited<ReturnType<Func>>>;
ReturnType | Promise<ReturnType> async $$eval<
>, Params extends unknown[],
...args: SerializableOrJSHandle[] Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc<
): Promise<WrapElementHandle<ReturnType>> { [Element[], ...Params]
>
>(
selector: string,
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
const {updatedSelector, queryHandler} = const {updatedSelector, queryHandler} =
_getQueryHandlerAndSelector(selector); _getQueryHandlerAndSelector(selector);
assert(queryHandler.queryAllArray); assert(queryHandler.queryAllArray);
const arrayHandle = await queryHandler.queryAllArray(this, updatedSelector); const arrayHandle = await queryHandler.queryAllArray(this, updatedSelector);
const result = await arrayHandle.evaluate<EvaluateFn<Element[]>>( const result = await arrayHandle.evaluate(pageFunction, ...args);
pageFunction,
...args
);
await arrayHandle.dispose(); await arrayHandle.dispose();
/* This `as` exists for the same reason as the `as` in $eval above. return result;
* See the comment there for a full explanation.
*/
return result as WrapElementHandle<ReturnType>;
} }
/** /**
@ -1262,24 +1288,21 @@ export class ElementHandle<
* @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate} * @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate}
*/ */
async $x(expression: string): Promise<ElementHandle[]> { async $x(expression: string): Promise<ElementHandle[]> {
const arrayHandle = await this.evaluateHandle( const arrayHandle = await this.evaluateHandle((element, expression) => {
(element: Document, expression: string) => { const document = element.ownerDocument || element;
const document = element.ownerDocument || element; const iterator = document.evaluate(
const iterator = document.evaluate( expression,
expression, element,
element, null,
null, XPathResult.ORDERED_NODE_ITERATOR_TYPE
XPathResult.ORDERED_NODE_ITERATOR_TYPE );
); const array = [];
const array = []; let item;
let item; while ((item = iterator.iterateNext())) {
while ((item = iterator.iterateNext())) { array.push(item);
array.push(item); }
} return array;
return array; }, expression);
},
expression
);
const properties = await arrayHandle.getProperties(); const properties = await arrayHandle.getProperties();
await arrayHandle.dispose(); await arrayHandle.dispose();
const result = []; const result = [];
@ -1298,8 +1321,8 @@ export class ElementHandle<
async isIntersectingViewport(options?: { async isIntersectingViewport(options?: {
threshold?: number; threshold?: number;
}): Promise<boolean> { }): Promise<boolean> {
const {threshold = 0} = options || {}; const {threshold = 0} = options ?? {};
return await this.evaluate(async (element: Element, threshold: number) => { return await this.evaluate(async (element, threshold) => {
const visibleRatio = await new Promise<number>(resolve => { const visibleRatio = await new Promise<number>(resolve => {
const observer = new IntersectionObserver(entries => { const observer = new IntersectionObserver(entries => {
resolve(entries[0]!.intersectionRatio); resolve(entries[0]!.intersectionRatio);

View File

@ -23,15 +23,8 @@ import {CDPSession, CDPSessionEmittedEvents, Connection} from './Connection.js';
import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js'; import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js';
import {Coverage} from './Coverage.js'; import {Coverage} from './Coverage.js';
import {Dialog} from './Dialog.js'; import {Dialog} from './Dialog.js';
import {WaitForSelectorOptions} from './DOMWorld.js';
import {EmulationManager} from './EmulationManager.js'; import {EmulationManager} from './EmulationManager.js';
import {
EvaluateFn,
EvaluateFnReturnType,
EvaluateHandleFn,
SerializableOrJSHandle,
UnwrapPromiseLike,
WrapElementHandle,
} from './EvalTypes.js';
import {EventEmitter, Handler} from './EventEmitter.js'; import {EventEmitter, Handler} from './EventEmitter.js';
import {FileChooser} from './FileChooser.js'; import {FileChooser} from './FileChooser.js';
import { import {
@ -39,6 +32,23 @@ import {
FrameManager, FrameManager,
FrameManagerEmittedEvents, FrameManagerEmittedEvents,
} from './FrameManager.js'; } from './FrameManager.js';
import {HTTPRequest} from './HTTPRequest.js';
import {HTTPResponse} from './HTTPResponse.js';
import {Keyboard, Mouse, MouseButton, Touchscreen} from './Input.js';
import {ElementHandle, JSHandle, _createJSHandle} from './JSHandle.js';
import {PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {
Credentials,
NetworkConditions,
NetworkManagerEmittedEvents,
} from './NetworkManager.js';
import {LowerCasePaperFormat, PDFOptions, _paperFormats} from './PDFOptions.js';
import {Viewport} from './PuppeteerViewport.js';
import {Target} from './Target.js';
import {TaskQueue} from './TaskQueue.js';
import {TimeoutSettings} from './TimeoutSettings.js';
import {Tracing} from './Tracing.js';
import {EvaluateFunc, EvaluateParams, HandleFor} from './types.js';
import { import {
debugError, debugError,
evaluationString, evaluationString,
@ -57,22 +67,6 @@ import {
waitForEvent, waitForEvent,
waitWithTimeout, waitWithTimeout,
} from './util.js'; } from './util.js';
import {HTTPRequest} from './HTTPRequest.js';
import {HTTPResponse} from './HTTPResponse.js';
import {Keyboard, Mouse, MouseButton, Touchscreen} from './Input.js';
import {ElementHandle, JSHandle, _createJSHandle} from './JSHandle.js';
import {PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {
Credentials,
NetworkConditions,
NetworkManagerEmittedEvents,
} from './NetworkManager.js';
import {LowerCasePaperFormat, PDFOptions, _paperFormats} from './PDFOptions.js';
import {Viewport} from './PuppeteerViewport.js';
import {Target} from './Target.js';
import {TaskQueue} from './TaskQueue.js';
import {TimeoutSettings} from './TimeoutSettings.js';
import {Tracing} from './Tracing.js';
import {WebWorker} from './WebWorker.js'; import {WebWorker} from './WebWorker.js';
/** /**
@ -466,9 +460,7 @@ export class Page extends EventEmitter {
#viewport: Viewport | null; #viewport: Viewport | null;
#screenshotTaskQueue: TaskQueue; #screenshotTaskQueue: TaskQueue;
#workers = new Map<string, WebWorker>(); #workers = new Map<string, WebWorker>();
// TODO: improve this typedef - it's a function that takes a file chooser or #fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
// something?
#fileChooserInterceptors = new Set<Function>();
#disconnectPromise?: Promise<Error>; #disconnectPromise?: Promise<Error>;
#userDragInterceptionEnabled = false; #userDragInterceptionEnabled = false;
@ -638,9 +630,13 @@ export class Page extends EventEmitter {
const element = await context._adoptBackendNodeId(event.backendNodeId); const element = await context._adoptBackendNodeId(event.backendNodeId);
const interceptors = Array.from(this.#fileChooserInterceptors); const interceptors = Array.from(this.#fileChooserInterceptors);
this.#fileChooserInterceptors.clear(); this.#fileChooserInterceptors.clear();
const fileChooser = new FileChooser(element, event); const fileChooser = new FileChooser(
// This is guaranteed by the event.
element as ElementHandle<HTMLInputElement>,
event
);
for (const interceptor of interceptors) { for (const interceptor of interceptors) {
interceptor.call(null, fileChooser); interceptor.call(undefined, fileChooser);
} }
} }
@ -736,7 +732,7 @@ export class Page extends EventEmitter {
} }
const {timeout = this.#timeoutSettings.timeout()} = options; const {timeout = this.#timeoutSettings.timeout()} = options;
let callback!: (value: FileChooser | PromiseLike<FileChooser>) => void; let callback!: (value: FileChooser) => void;
const promise = new Promise<FileChooser>(x => { const promise = new Promise<FileChooser>(x => {
return (callback = x); return (callback = x);
}); });
@ -1008,10 +1004,27 @@ export class Page extends EventEmitter {
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector} * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
* to query page for. * to query page for.
*/ */
async $<T extends Element = Element>( async $<Selector extends keyof HTMLElementTagNameMap>(
selector: string selector: Selector
): Promise<ElementHandle<T> | null> { ): Promise<ElementHandle<HTMLElementTagNameMap[Selector]> | null>;
return this.mainFrame().$<T>(selector); async $(selector: string): Promise<ElementHandle | null>;
async $(selector: string): Promise<ElementHandle | null> {
return this.mainFrame().$(selector);
}
/**
* The method runs `document.querySelectorAll` within the page. If no elements
* match the selector, the return value resolves to `[]`.
* @remarks
* Shortcut for {@link Frame.$$ | Page.mainFrame().$$(selector) }.
* @param selector - A `selector` to query page for
*/
async $$<Selector extends keyof HTMLElementTagNameMap>(
selector: Selector
): Promise<ElementHandle<HTMLElementTagNameMap[Selector]>[]>;
async $$(selector: string): Promise<ElementHandle[]>;
async $$(selector: string): Promise<ElementHandle[]> {
return this.mainFrame().$$(selector);
} }
/** /**
@ -1063,12 +1076,15 @@ export class Page extends EventEmitter {
* @param pageFunction - a function that is run within the page * @param pageFunction - a function that is run within the page
* @param args - arguments to be passed to the pageFunction * @param args - arguments to be passed to the pageFunction
*/ */
async evaluateHandle<HandlerType extends JSHandle = JSHandle>( async evaluateHandle<
pageFunction: EvaluateHandleFn, Params extends unknown[],
...args: SerializableOrJSHandle[] Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
): Promise<HandlerType> { >(
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
const context = await this.mainFrame().executionContext(); const context = await this.mainFrame().executionContext();
return context.evaluateHandle<HandlerType>(pageFunction, ...args); return context.evaluateHandle(pageFunction, ...args);
} }
/** /**
@ -1098,7 +1114,9 @@ export class Page extends EventEmitter {
* @returns Promise which resolves to a handle to an array of objects with * @returns Promise which resolves to a handle to an array of objects with
* this prototype. * this prototype.
*/ */
async queryObjects(prototypeHandle: JSHandle): Promise<JSHandle> { async queryObjects<Prototype>(
prototypeHandle: JSHandle<Prototype>
): Promise<JSHandle<Prototype[]>> {
const context = await this.mainFrame().executionContext(); const context = await this.mainFrame().executionContext();
return context.queryObjects(prototypeHandle); return context.queryObjects(prototypeHandle);
} }
@ -1161,25 +1179,38 @@ export class Page extends EventEmitter {
* is wrapped in an {@link ElementHandle}, else the raw value itself is * is wrapped in an {@link ElementHandle}, else the raw value itself is
* returned. * returned.
*/ */
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, selector: string,
pageFunction: ( pageFunction: Func | string,
element: Element, ...args: EvaluateParams<Params>
/* Unfortunately this has to be unknown[] because it's hard to get ): Promise<Awaited<ReturnType<Func>>>;
* TypeScript to understand that the arguments will be left alone unless async $eval<
* they are an ElementHandle, in which case they will be unwrapped. Params extends unknown[],
* The nice thing about unknown vs any is that unknown will force the user Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc<
* to type the item before using it to avoid errors. [Element, ...Params]
* >
* TODO(@jackfranklin): We could fix this by using overloads like >(
* DefinitelyTyped does: selector: string,
* https://github.com/DefinitelyTyped/DefinitelyTyped/blob/HEAD/types/puppeteer/index.d.ts#L114 pageFunction: Func | string,
*/ ...args: EvaluateParams<Params>
...args: unknown[] ): Promise<Awaited<ReturnType<Func>>> {
) => ReturnType | Promise<ReturnType>, return this.mainFrame().$eval(selector, pageFunction, ...args);
...args: SerializableOrJSHandle[]
): Promise<WrapElementHandle<ReturnType>> {
return this.mainFrame().$eval<ReturnType>(selector, pageFunction, ...args);
} }
/** /**
@ -1244,32 +1275,38 @@ export class Page extends EventEmitter {
* is wrapped in an {@link ElementHandle}, else the raw value itself is * is wrapped in an {@link ElementHandle}, else the raw value itself is
* returned. * returned.
*/ */
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, selector: string,
pageFunction: ( pageFunction: Func | string,
elements: Element[], ...args: EvaluateParams<Params>
/* These have to be typed as unknown[] for the same reason as the $eval ): Promise<Awaited<ReturnType<Func>>>;
* definition above, please see that comment for more details and the TODO async $$eval<
* that will improve things. Params extends unknown[],
*/ Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc<
...args: unknown[] [Element[], ...Params]
) => ReturnType | Promise<ReturnType>, >
...args: SerializableOrJSHandle[] >(
): Promise<WrapElementHandle<ReturnType>> { selector: string,
return this.mainFrame().$$eval<ReturnType>(selector, pageFunction, ...args); pageFunction: Func | string,
} ...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
/** return this.mainFrame().$$eval(selector, pageFunction, ...args);
* The method runs `document.querySelectorAll` within the page. If no elements
* match the selector, the return value resolves to `[]`.
* @remarks
* Shortcut for {@link Frame.$$ | Page.mainFrame().$$(selector) }.
* @param selector - A `selector` to query page for
*/
async $$<T extends Element = Element>(
selector: string
): Promise<Array<ElementHandle<T>>> {
return this.mainFrame().$$<T>(selector);
} }
/** /**
@ -1390,7 +1427,7 @@ export class Page extends EventEmitter {
* *
* NOTE: Functions installed via `page.exposeFunction` survive navigations. * NOTE: Functions installed via `page.exposeFunction` survive navigations.
* @param name - Name of the function on the window object * @param name - Name of the function on the window object
* @param puppeteerFunction - Callback function which will be called in * @param pptrFunction - Callback function which will be called in
* Puppeteer's context. * Puppeteer's context.
* @example * @example
* An example of adding an `md5` function into the page: * An example of adding an `md5` function into the page:
@ -1442,7 +1479,7 @@ export class Page extends EventEmitter {
*/ */
async exposeFunction( async exposeFunction(
name: string, name: string,
puppeteerFunction: Function | {default: Function} pptrFunction: Function | {default: Function}
): Promise<void> { ): Promise<void> {
if (this.#pageBindings.has(name)) { if (this.#pageBindings.has(name)) {
throw new Error( throw new Error(
@ -1451,14 +1488,13 @@ export class Page extends EventEmitter {
} }
let exposedFunction: Function; let exposedFunction: Function;
if (typeof puppeteerFunction === 'function') { switch (typeof pptrFunction) {
exposedFunction = puppeteerFunction; case 'function':
} else if (typeof puppeteerFunction.default === 'function') { exposedFunction = pptrFunction;
exposedFunction = puppeteerFunction.default; break;
} else { default:
throw new Error( exposedFunction = pptrFunction.default;
`Failed to add page binding with name ${name}: ${puppeteerFunction} is not a function or a module with a default export.` break;
);
} }
this.#pageBindings.set(name, exposedFunction); this.#pageBindings.set(name, exposedFunction);
@ -2640,11 +2676,14 @@ export class Page extends EventEmitter {
* *
* @returns the return value of `pageFunction`. * @returns the return value of `pageFunction`.
*/ */
async evaluate<T extends EvaluateFn>( async evaluate<
pageFunction: T, Params extends unknown[],
...args: SerializableOrJSHandle[] Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> { >(
return this.#frameManager.mainFrame().evaluate<T>(pageFunction, ...args); pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
return this.#frameManager.mainFrame().evaluate(pageFunction, ...args);
} }
/** /**
@ -2678,10 +2717,10 @@ export class Page extends EventEmitter {
* await page.evaluateOnNewDocument(preloadFile); * await page.evaluateOnNewDocument(preloadFile);
* ``` * ```
*/ */
async evaluateOnNewDocument( async evaluateOnNewDocument<
pageFunction: Function | string, Params extends unknown[],
...args: unknown[] Func extends (...args: Params) => unknown = (...args: Params) => unknown
): Promise<void> { >(pageFunction: Func | string, ...args: Params): Promise<void> {
const source = evaluationString(pageFunction, ...args); const source = evaluationString(pageFunction, ...args);
await this.#client.send('Page.addScriptToEvaluateOnNewDocument', { await this.#client.send('Page.addScriptToEvaluateOnNewDocument', {
source, source,
@ -3203,48 +3242,6 @@ export class Page extends EventEmitter {
return this.mainFrame().type(selector, text, options); return this.mainFrame().type(selector, text, options);
} }
/**
* @remarks
*
* This method behaves differently depending on the first parameter. If it's a
* `string`, it will be treated as a `selector` or `xpath` (if the string
* starts with `//`). This method then is a shortcut for
* {@link Page.waitForSelector} or {@link Page.waitForXPath}.
*
* If the first argument is a function this method is a shortcut for
* {@link Page.waitForFunction}.
*
* If the first argument is a `number`, it's treated as a timeout in
* milliseconds and the method returns a promise which resolves after the
* timeout.
*
* @param selectorOrFunctionOrTimeout - a selector, predicate or timeout to
* wait for.
* @param options - optional waiting parameters.
* @param args - arguments to pass to `pageFunction`.
*
* @deprecated Don't use this method directly. Instead use the more explicit
* methods available: {@link Page.waitForSelector},
* {@link Page.waitForXPath}, {@link Page.waitForFunction} or
* {@link Page.waitForTimeout}.
*/
waitFor(
selectorOrFunctionOrTimeout: string | number | Function,
options: {
visible?: boolean;
hidden?: boolean;
timeout?: number;
polling?: string | number;
} = {},
...args: SerializableOrJSHandle[]
): Promise<JSHandle | null> {
return this.mainFrame().waitFor(
selectorOrFunctionOrTimeout,
options,
...args
);
}
/** /**
* Causes your script to wait for the given number of milliseconds. * Causes your script to wait for the given number of milliseconds.
* *
@ -3316,15 +3313,19 @@ export class Page extends EventEmitter {
* (30 seconds). Pass `0` to disable timeout. The default value can be changed * (30 seconds). Pass `0` to disable timeout. The default value can be changed
* by using the {@link Page.setDefaultTimeout} method. * by using the {@link Page.setDefaultTimeout} method.
*/ */
waitForSelector( async waitForSelector<Selector extends keyof HTMLElementTagNameMap>(
selector: Selector,
options?: Exclude<WaitForSelectorOptions, 'root'>
): Promise<ElementHandle<HTMLElementTagNameMap[Selector]> | null>;
async waitForSelector(
selector: string, selector: string,
options: { options?: Exclude<WaitForSelectorOptions, 'root'>
visible?: boolean; ): Promise<ElementHandle | null>;
hidden?: boolean; async waitForSelector(
timeout?: number; selector: string,
} = {} options: Exclude<WaitForSelectorOptions, 'root'> = {}
): Promise<ElementHandle | null> { ): Promise<ElementHandle | null> {
return this.mainFrame().waitForSelector(selector, options); return await this.mainFrame().waitForSelector(selector, options);
} }
/** /**
@ -3452,14 +3453,17 @@ export class Page extends EventEmitter {
* {@link Page.setDefaultTimeout | page.setDefaultTimeout(timeout)} method. * {@link Page.setDefaultTimeout | page.setDefaultTimeout(timeout)} method.
* *
*/ */
waitForFunction( waitForFunction<
pageFunction: Function | string, Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
options: { options: {
timeout?: number; timeout?: number;
polling?: string | number; polling?: string | number;
} = {}, } = {},
...args: SerializableOrJSHandle[] ...args: EvaluateParams<Params>
): Promise<JSHandle> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this.mainFrame().waitForFunction(pageFunction, options, ...args); return this.mainFrame().waitForFunction(pageFunction, options, ...args);
} }
} }

View File

@ -16,7 +16,7 @@
import {WaitForSelectorOptions, DOMWorld} from './DOMWorld.js'; import {WaitForSelectorOptions, DOMWorld} from './DOMWorld.js';
import {ElementHandle, JSHandle} from './JSHandle.js'; import {ElementHandle, JSHandle} from './JSHandle.js';
import {_ariaHandler} from './AriaQueryHandler.js'; import {ariaHandler} from './AriaQueryHandler.js';
/** /**
* @internal * @internal
@ -99,12 +99,13 @@ function makeQueryHandler(handler: CustomQueryHandler): InternalQueryHandler {
return result; return result;
}; };
internalHandler.queryAllArray = async (element, selector) => { internalHandler.queryAllArray = async (element, selector) => {
const resultHandle = await element.evaluateHandle(queryAll, selector); const resultHandle = (await element.evaluateHandle(
const arrayHandle = await resultHandle.evaluateHandle( queryAll,
(res: Element[] | NodeListOf<Element>) => { selector
return Array.from(res); )) as JSHandle<Element[] | NodeListOf<Element>>;
} const arrayHandle = await resultHandle.evaluateHandle(res => {
); return Array.from(res);
});
return arrayHandle; return arrayHandle;
}; };
} }
@ -172,7 +173,7 @@ const pierceHandler = makeQueryHandler({
}); });
const builtInHandlers = new Map([ const builtInHandlers = new Map([
['aria', _ariaHandler], ['aria', ariaHandler],
['pierce', pierceHandler], ['pierce', pierceHandler],
]); ]);
const queryHandlers = new Map(builtInHandlers); const queryHandlers = new Map(builtInHandlers);

View File

@ -16,11 +16,11 @@
import {Protocol} from 'devtools-protocol'; import {Protocol} from 'devtools-protocol';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {ConsoleMessageType} from './ConsoleMessage.js'; import {ConsoleMessageType} from './ConsoleMessage.js';
import {EvaluateHandleFn, SerializableOrJSHandle} from './EvalTypes.js'; import {EvaluateFunc, EvaluateParams, HandleFor} from './types.js';
import {EventEmitter} from './EventEmitter.js'; import {EventEmitter} from './EventEmitter.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {debugError} from './util.js';
import {JSHandle} from './JSHandle.js'; import {JSHandle} from './JSHandle.js';
import {debugError} from './util.js';
/** /**
* @internal * @internal
@ -136,11 +136,14 @@ export class WebWorker extends EventEmitter {
* @param args - Arguments to pass to `pageFunction`. * @param args - Arguments to pass to `pageFunction`.
* @returns Promise which resolves to the return value of `pageFunction`. * @returns Promise which resolves to the return value of `pageFunction`.
*/ */
async evaluate<ReturnType>( async evaluate<
pageFunction: Function | string, Params extends unknown[],
...args: any[] Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
): Promise<ReturnType> { >(
return (await this.#executionContextPromise).evaluate<ReturnType>( pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
return (await this.#executionContextPromise).evaluate(
pageFunction, pageFunction,
...args ...args
); );
@ -158,11 +161,14 @@ export class WebWorker extends EventEmitter {
* @param args - Arguments to pass to `pageFunction`. * @param args - Arguments to pass to `pageFunction`.
* @returns Promise which resolves to the return value of `pageFunction`. * @returns Promise which resolves to the return value of `pageFunction`.
*/ */
async evaluateHandle<HandlerType extends JSHandle = JSHandle>( async evaluateHandle<
pageFunction: EvaluateHandleFn, Params extends unknown[],
...args: SerializableOrJSHandle[] Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
): Promise<JSHandle> { >(
return (await this.#executionContextPromise).evaluateHandle<HandlerType>( pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return (await this.#executionContextPromise).evaluateHandle(
pageFunction, pageFunction,
...args ...args
); );

32
src/common/types.ts Normal file
View File

@ -0,0 +1,32 @@
/**
* Copyright 2020 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 {JSHandle, ElementHandle} from './JSHandle.js';
export type Awaitable<T> = T | PromiseLike<T>;
export type HandleFor<T> = T extends Element ? ElementHandle<T> : JSHandle<T>;
export type HandleOr<T> = HandleFor<T> | JSHandle<T> | T;
export type EvaluateParams<T extends unknown[]> = {
[K in keyof T]: T[K] extends HandleOr<unknown> ? T[K] : HandleOr<T[K]>;
};
export type InnerParams<T extends unknown[]> = {
[K in keyof T]: T[K] extends HandleOr<infer U> ? U : never;
};
export type EvaluateFunc<T extends unknown[]> = (
...params: InnerParams<T>
) => Awaitable<unknown>;

View File

@ -276,8 +276,9 @@ describeChromeOnly('AriaQueryHandler', () => {
page.waitForSelector('aria/anything'), page.waitForSelector('aria/anything'),
page.setContent(`<h1>anything</h1>`), page.setContent(`<h1>anything</h1>`),
]); ]);
assert(handle);
expect( expect(
await page.evaluate((x: HTMLElement) => { await page.evaluate(x => {
return x.textContent; return x.textContent;
}, handle) }, handle)
).toBe('anything'); ).toBe('anything');
@ -651,7 +652,9 @@ describeChromeOnly('AriaQueryHandler', () => {
}); });
it('should find by role "button"', async () => { it('should find by role "button"', async () => {
const {page} = getTestState(); const {page} = getTestState();
const found = await page.$$<HTMLButtonElement>('aria/[role="button"]'); const found = (await page.$$(
'aria/[role="button"]'
)) as ElementHandle<HTMLButtonElement>[];
const ids = await getIds(found); const ids = await getIds(found);
expect(ids).toEqual([ expect(ids).toEqual([
'node5', 'node5',

View File

@ -17,10 +17,10 @@
import expect from 'expect'; import expect from 'expect';
import { import {
getTestState, getTestState,
setupTestBrowserHooks,
itFailsFirefox, itFailsFirefox,
setupTestBrowserHooks,
} from './mocha-utils.js'; } from './mocha-utils.js';
import utils from './utils.js'; import {waitEvent} from './utils.js';
describe('BrowserContext', function () { describe('BrowserContext', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();
@ -67,8 +67,8 @@ describe('BrowserContext', function () {
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const [popupTarget] = await Promise.all([ const [popupTarget] = await Promise.all([
utils.waitEvent(browser, 'targetcreated'), waitEvent(browser, 'targetcreated'),
page.evaluate<(url: string) => void>(url => { page.evaluate(url => {
return window.open(url); return window.open(url);
}, server.EMPTY_PAGE), }, server.EMPTY_PAGE),
]); ]);

View File

@ -430,12 +430,12 @@ describe('Cookie specs', () => {
await page.goto(server.PREFIX + '/grid.html'); await page.goto(server.PREFIX + '/grid.html');
await page.setCookie({name: 'localhost-cookie', value: 'best'}); await page.setCookie({name: 'localhost-cookie', value: 'best'});
await page.evaluate<(src: string) => Promise<void>>(src => { await page.evaluate(src => {
let fulfill!: () => void; let fulfill!: () => void;
const promise = new Promise<void>(x => { const promise = new Promise<void>(x => {
return (fulfill = x); return (fulfill = x);
}); });
const iframe = document.createElement('iframe') as HTMLIFrameElement; const iframe = document.createElement('iframe');
document.body.appendChild(iframe); document.body.appendChild(iframe);
iframe.onload = fulfill; iframe.onload = fulfill;
iframe.src = src; iframe.src = src;
@ -499,7 +499,7 @@ describe('Cookie specs', () => {
try { try {
await page.goto(httpsServer.PREFIX + '/grid.html'); await page.goto(httpsServer.PREFIX + '/grid.html');
await page.evaluate<(src: string) => Promise<void>>(src => { await page.evaluate(src => {
let fulfill!: () => void; let fulfill!: () => void;
const promise = new Promise<void>(x => { const promise = new Promise<void>(x => {
return (fulfill = x); return (fulfill = x);

View File

@ -287,7 +287,7 @@ describe('Coverage specs', function () {
const {page, server} = getTestState(); const {page, server} = getTestState();
await page.coverage.startCSSCoverage(); await page.coverage.startCSSCoverage();
await page.evaluate<(url: string) => Promise<void>>(async url => { await page.evaluate(async url => {
document.body.textContent = 'hello, world'; document.body.textContent = 'hello, world';
const link = document.createElement('link'); const link = document.createElement('link');

View File

@ -17,15 +17,14 @@
import expect from 'expect'; import expect from 'expect';
import sinon from 'sinon'; import sinon from 'sinon';
import { import {
describeFailsFirefox,
getTestState, getTestState,
itFailsFirefox,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
describeFailsFirefox,
itFailsFirefox,
} from './mocha-utils.js'; } from './mocha-utils.js';
import utils from './utils.js'; import utils from './utils.js';
import {ElementHandle} from '../../lib/cjs/puppeteer/common/JSHandle.js';
describe('ElementHandle specs', function () { describe('ElementHandle specs', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();
@ -86,7 +85,7 @@ describe('ElementHandle specs', function () {
`); `);
const element = (await page.$('#therect'))!; const element = (await page.$('#therect'))!;
const pptrBoundingBox = await element.boundingBox(); const pptrBoundingBox = await element.boundingBox();
const webBoundingBox = await page.evaluate((e: HTMLElement) => { const webBoundingBox = await page.evaluate(e => {
const rect = e.getBoundingClientRect(); const rect = e.getBoundingClientRect();
return {x: rect.x, y: rect.y, width: rect.width, height: rect.height}; return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
}, element); }, element);
@ -189,9 +188,9 @@ describe('ElementHandle specs', function () {
const {page, server} = getTestState(); const {page, server} = getTestState();
await page.goto(server.PREFIX + '/shadow.html'); await page.goto(server.PREFIX + '/shadow.html');
const buttonHandle = await page.evaluateHandle<ElementHandle>(() => { const buttonHandle = await page.evaluateHandle(() => {
// @ts-expect-error button is expected to be in the page's scope. // @ts-expect-error button is expected to be in the page's scope.
return button; return button as HTMLButtonElement;
}); });
await buttonHandle.click(); await buttonHandle.click();
expect( expect(
@ -205,8 +204,8 @@ describe('ElementHandle specs', function () {
const {page, server} = getTestState(); const {page, server} = getTestState();
await page.goto(server.PREFIX + '/input/button.html'); await page.goto(server.PREFIX + '/input/button.html');
const buttonTextNode = await page.evaluateHandle<ElementHandle>(() => { const buttonTextNode = await page.evaluateHandle(() => {
return document.querySelector('button')!.firstChild; return document.querySelector('button')!.firstChild as HTMLElement;
}); });
let error!: Error; let error!: Error;
await buttonTextNode.click().catch(error_ => { await buttonTextNode.click().catch(error_ => {
@ -401,7 +400,7 @@ describe('ElementHandle specs', function () {
}); });
const element = (await page.$('getById/foo'))!; const element = (await page.$('getById/foo'))!;
expect( expect(
await page.evaluate<(element: HTMLElement) => string>(element => { await page.evaluate(element => {
return element.id; return element.id;
}, element) }, element)
).toBe('foo'); ).toBe('foo');
@ -454,12 +453,9 @@ describe('ElementHandle specs', function () {
const elements = await page.$$('getByClass/foo'); const elements = await page.$$('getByClass/foo');
const classNames = await Promise.all( const classNames = await Promise.all(
elements.map(async element => { elements.map(async element => {
return await page.evaluate<(element: HTMLElement) => string>( return await page.evaluate(element => {
element => { return element.className;
return element.className; }, element);
},
element
);
}) })
); );
@ -539,7 +535,7 @@ describe('ElementHandle specs', function () {
return element.querySelector(`.${selector}`); return element.querySelector(`.${selector}`);
}, },
}); });
const waitFor = page.waitFor('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.
await page.setContent( await page.setContent(

View File

@ -357,7 +357,7 @@ describe('Evaluation specs', function () {
return error_.message; return error_.message;
}); });
const error = await page const error = await page
.evaluate<(errorText: string) => Error>(errorText => { .evaluate(errorText => {
throw new Error(errorText); throw new Error(errorText);
}, errorText) }, errorText)
.catch(error_ => { .catch(error_ => {
@ -477,7 +477,7 @@ describe('Evaluation specs', function () {
it('should transfer 100Mb of data from page to node.js', async function () { it('should transfer 100Mb of data from page to node.js', async function () {
const {page} = getTestState(); const {page} = getTestState();
const a = await page.evaluate<() => string>(() => { const a = await page.evaluate(() => {
return Array(100 * 1024 * 1024 + 1).join('a'); return Array(100 * 1024 * 1024 + 1).join('a');
}); });
expect(a.length).toBe(100 * 1024 * 1024); expect(a.length).toBe(100 * 1024 * 1024);

View File

@ -15,6 +15,7 @@
*/ */
import expect from 'expect'; import expect from 'expect';
import {ElementHandle} from '../../lib/cjs/puppeteer/common/JSHandle.js';
import { import {
getTestState, getTestState,
setupTestBrowserHooks, setupTestBrowserHooks,
@ -29,8 +30,8 @@ describeFailsFirefox('Emulate idle state', () => {
async function getIdleState() { async function getIdleState() {
const {page} = getTestState(); const {page} = getTestState();
const stateElement = (await page.$('#state'))!; const stateElement = (await page.$('#state')) as ElementHandle<HTMLElement>;
return await page.evaluate((element: HTMLElement) => { return await page.evaluate(element => {
return element.innerText; return element.innerText;
}, stateElement); }, stateElement);
} }

View File

@ -15,12 +15,11 @@
*/ */
import expect from 'expect'; import expect from 'expect';
import {JSHandle} from '../../lib/cjs/puppeteer/common/JSHandle.js';
import { import {
getTestState, getTestState,
itFailsFirefox,
setupTestBrowserHooks, setupTestBrowserHooks,
setupTestPageAndContextHooks, setupTestPageAndContextHooks,
itFailsFirefox,
shortWaitForArrayToHaveAtLeastNElements, shortWaitForArrayToHaveAtLeastNElements,
} from './mocha-utils.js'; } from './mocha-utils.js';
@ -43,7 +42,7 @@ describe('JSHandle', function () {
const navigatorHandle = await page.evaluateHandle(() => { const navigatorHandle = await page.evaluateHandle(() => {
return navigator; return navigator;
}); });
const text = await page.evaluate((e: Navigator) => { const text = await page.evaluate(e => {
return e.userAgent; return e.userAgent;
}, navigatorHandle); }, navigatorHandle);
expect(text).toContain('Mozilla'); expect(text).toContain('Mozilla');
@ -68,9 +67,10 @@ describe('JSHandle', function () {
await page await page
.evaluateHandle( .evaluateHandle(
opts => { opts => {
// @ts-expect-error we are deliberately passing a bad type here
// (nested object)
return opts.elem; return opts.elem;
}, },
// @ts-expect-error we are deliberately passing a bad type here (nested object)
{test} {test}
) )
.catch(error_ => { .catch(error_ => {
@ -98,8 +98,8 @@ describe('JSHandle', function () {
return window; return window;
}); });
expect( expect(
await page.evaluate((e: {FOO: number}) => { await page.evaluate(e => {
return e.FOO; return (e as any).FOO;
}, aHandle) }, aHandle)
).toBe(123); ).toBe(123);
}); });
@ -119,21 +119,6 @@ describe('JSHandle', function () {
const twoHandle = await aHandle.getProperty('two'); const twoHandle = await aHandle.getProperty('two');
expect(await twoHandle.jsonValue()).toEqual(2); expect(await twoHandle.jsonValue()).toEqual(2);
}); });
it('should return a JSHandle even if the property does not exist', async () => {
const {page} = getTestState();
const aHandle = await page.evaluateHandle(() => {
return {
one: 1,
two: 2,
three: 3,
};
});
const undefinedHandle = await aHandle.getProperty('doesnotexist');
expect(undefinedHandle).toBeInstanceOf(JSHandle);
expect(await undefinedHandle.jsonValue()).toBe(undefined);
});
}); });
describe('JSHandle.jsonValue', function () { describe('JSHandle.jsonValue', function () {

View File

@ -467,7 +467,7 @@ describe('Keyboard', function () {
await page.type('textarea', '👹 Tokyo street Japan 🇯🇵'); await page.type('textarea', '👹 Tokyo street Japan 🇯🇵');
expect( expect(
await page.$eval('textarea', textarea => { await page.$eval('textarea', textarea => {
return (textarea as HTMLInputElement).value; return textarea.value;
}) })
).toBe('👹 Tokyo street Japan 🇯🇵'); ).toBe('👹 Tokyo street Japan 🇯🇵');
}); });
@ -485,7 +485,7 @@ describe('Keyboard', function () {
await textarea.type('👹 Tokyo street Japan 🇯🇵'); await textarea.type('👹 Tokyo street Japan 🇯🇵');
expect( expect(
await frame.$eval('textarea', textarea => { await frame.$eval('textarea', textarea => {
return (textarea as HTMLInputElement).value; return textarea.value;
}) })
).toBe('👹 Tokyo street Japan 🇯🇵'); ).toBe('👹 Tokyo street Japan 🇯🇵');
}); });

View File

@ -61,7 +61,7 @@ describe('Mouse', function () {
}); });
}); });
await page.mouse.click(50, 60); await page.mouse.click(50, 60);
const event = await page.evaluate<() => MouseEvent>(() => { const event = await page.evaluate(() => {
return (globalThis as any).clickPromise; return (globalThis as any).clickPromise;
}); });
expect(event.type).toBe('click'); expect(event.type).toBe('click');
@ -75,15 +75,13 @@ describe('Mouse', function () {
const {page, server} = getTestState(); const {page, server} = getTestState();
await page.goto(server.PREFIX + '/input/textarea.html'); await page.goto(server.PREFIX + '/input/textarea.html');
const {x, y, width, height} = await page.evaluate<() => Dimensions>( const {x, y, width, height} = await page.evaluate(dimensions);
dimensions
);
const mouse = page.mouse; const mouse = page.mouse;
await mouse.move(x + width - 4, y + height - 4); await mouse.move(x + width - 4, y + height - 4);
await mouse.down(); await mouse.down();
await mouse.move(x + width + 100, y + height + 100); await mouse.move(x + width + 100, y + height + 100);
await mouse.up(); await mouse.up();
const newDimensions = await page.evaluate<() => Dimensions>(dimensions); const newDimensions = await page.evaluate(dimensions);
expect(newDimensions.width).toBe(Math.round(width + 104)); expect(newDimensions.width).toBe(Math.round(width + 104));
expect(newDimensions.height).toBe(Math.round(height + 104)); expect(newDimensions.height).toBe(Math.round(height + 104));
}); });

View File

@ -422,7 +422,7 @@ describe('network', function () {
}); });
// Trigger a request with a preflight. // Trigger a request with a preflight.
await page.evaluate<(src: string) => void>(async src => { await page.evaluate(async src => {
const response = await fetch(src, { const response = await fetch(src, {
method: 'POST', method: 'POST',
headers: {'x-ping': 'pong'}, headers: {'x-ping': 'pong'},
@ -855,7 +855,7 @@ describe('network', function () {
const response = await new Promise<HTTPResponse>(resolve => { const response = await new Promise<HTTPResponse>(resolve => {
page.on('response', resolve); page.on('response', resolve);
const url = httpsServer.CROSS_PROCESS_PREFIX + '/setcookie.html'; const url = httpsServer.CROSS_PROCESS_PREFIX + '/setcookie.html';
page.evaluate<(src: string) => void>(src => { page.evaluate(src => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('GET', src); xhr.open('GET', src);
xhr.send(); xhr.send();

View File

@ -20,7 +20,6 @@ import path from 'path';
import sinon from 'sinon'; import sinon from 'sinon';
import {CDPSession} from '../../lib/cjs/puppeteer/common/Connection.js'; import {CDPSession} from '../../lib/cjs/puppeteer/common/Connection.js';
import {ConsoleMessage} from '../../lib/cjs/puppeteer/common/ConsoleMessage.js'; import {ConsoleMessage} from '../../lib/cjs/puppeteer/common/ConsoleMessage.js';
import {JSHandle} from '../../lib/cjs/puppeteer/common/JSHandle.js';
import {Metrics, Page} from '../../lib/cjs/puppeteer/common/Page.js'; import {Metrics, Page} from '../../lib/cjs/puppeteer/common/Page.js';
import { import {
describeFailsFirefox, describeFailsFirefox,
@ -341,8 +340,8 @@ describe('Page', function () {
}); });
describe('BrowserContext.overridePermissions', function () { describe('BrowserContext.overridePermissions', function () {
function getPermission(page: Page, name: string) { function getPermission(page: Page, name: PermissionName) {
return page.evaluate((name: PermissionName) => { return page.evaluate(name => {
return navigator.permissions.query({name}).then(result => { return navigator.permissions.query({name}).then(result => {
return result.state; return result.state;
}); });
@ -559,7 +558,7 @@ describe('Page', function () {
return Set.prototype; return Set.prototype;
}); });
const objectsHandle = await page.queryObjects(prototypeHandle); const objectsHandle = await page.queryObjects(prototypeHandle);
const count = await page.evaluate((objects: JSHandle[]) => { const count = await page.evaluate(objects => {
return objects.length; return objects.length;
}, objectsHandle); }, objectsHandle);
expect(count).toBe(1); expect(count).toBe(1);
@ -580,7 +579,7 @@ describe('Page', function () {
return Set.prototype; return Set.prototype;
}); });
const objectsHandle = await page.queryObjects(prototypeHandle); const objectsHandle = await page.queryObjects(prototypeHandle);
const count = await page.evaluate((objects: JSHandle[]) => { const count = await page.evaluate(objects => {
return objects.length; return objects.length;
}, objectsHandle); }, objectsHandle);
expect(count).toBe(1); expect(count).toBe(1);
@ -1246,11 +1245,9 @@ describe('Page', function () {
return {x: a.x + b.x}; return {x: a.x + b.x};
} }
); );
const result = await page.evaluate<() => Promise<{x: number}>>( const result = await page.evaluate(async () => {
async () => { return (globalThis as any).complexObject({x: 5}, {x: 2});
return (globalThis as any).complexObject({x: 5}, {x: 2}); });
}
);
expect(result.x).toBe(7); expect(result.x).toBe(7);
}); });
it('should fallback to default export when passed a module object', async () => { it('should fallback to default export when passed a module object', async () => {

View File

@ -433,7 +433,7 @@ describe('querySelector', function () {
const html = (await page.$('html'))!; const html = (await page.$('html'))!;
const second = await html.$x(`./body/div[contains(@class, 'second')]`); const second = await html.$x(`./body/div[contains(@class, 'second')]`);
const inner = await second[0]!.$x(`./div[contains(@class, 'inner')]`); const inner = await second[0]!.$x(`./div[contains(@class, 'inner')]`);
const content = await page.evaluate((e: HTMLElement) => { const content = await page.evaluate(e => {
return e.textContent; return e.textContent;
}, inner[0]!); }, inner[0]!);
expect(content).toBe('A'); expect(content).toBe('A');
@ -480,7 +480,7 @@ describe('querySelector', function () {
const elements = await html.$$('allArray/div'); const elements = await html.$$('allArray/div');
expect(elements.length).toBe(2); expect(elements.length).toBe(2);
const promises = elements.map(element => { const promises = elements.map(element => {
return page.evaluate((e: HTMLElement) => { return page.evaluate(e => {
return e.textContent; return e.textContent;
}, element); }, element);
}); });

View File

@ -15,7 +15,6 @@
*/ */
import expect from 'expect'; import expect from 'expect';
import sinon from 'sinon';
import {isErrorLike} from '../../lib/cjs/puppeteer/common/util.js'; import {isErrorLike} from '../../lib/cjs/puppeteer/common/util.js';
import { import {
getTestState, getTestState,
@ -29,122 +28,6 @@ describe('waittask specs', function () {
setupTestBrowserHooks(); setupTestBrowserHooks();
setupTestPageAndContextHooks(); setupTestPageAndContextHooks();
describe('Page.waitFor', function () {
/* This method is deprecated but we don't want the warnings showing up in
* tests. Until we remove this method we still want to ensure we don't break
* it.
*/
beforeEach(() => {
return sinon.stub(console, 'warn').callsFake(() => {});
});
it('should wait for selector', async () => {
const {page, server} = getTestState();
let found = false;
const waitFor = page.waitForSelector('div').then(() => {
return (found = true);
});
await page.goto(server.EMPTY_PAGE);
expect(found).toBe(false);
await page.goto(server.PREFIX + '/grid.html');
await waitFor;
expect(found).toBe(true);
});
it('should wait for an xpath', async () => {
const {page, server} = getTestState();
let found = false;
const waitFor = page.waitFor('//div').then(() => {
return (found = true);
});
await page.goto(server.EMPTY_PAGE);
expect(found).toBe(false);
await page.goto(server.PREFIX + '/grid.html');
await waitFor;
expect(found).toBe(true);
});
it('should allow you to select an element with parenthesis-starting xpath', async () => {
const {page, server} = getTestState();
let found = false;
const waitFor = page.waitFor('(//img)[200]').then(() => {
found = true;
});
await page.goto(server.EMPTY_PAGE);
expect(found).toBe(false);
await page.goto(server.PREFIX + '/grid.html');
await waitFor;
expect(found).toBe(true);
});
it('should not allow you to select an element with single slash xpath', async () => {
const {page} = getTestState();
await page.setContent(`<div>some text</div>`);
let error!: Error;
await page.waitFor('/html/body/div').catch(error_ => {
return (error = error_);
});
expect(error).toBeTruthy();
});
it('should timeout', async () => {
const {page} = getTestState();
const startTime = Date.now();
const timeout = 42;
await page.waitFor(timeout);
expect(Date.now() - startTime).not.toBeLessThan(timeout / 2);
});
it('should work with multiline body', async () => {
const {page} = getTestState();
const result = await page.waitForFunction(`
(() => true)()
`);
expect(await result.jsonValue()).toBe(true);
});
it('should wait for predicate', async () => {
const {page} = getTestState();
await Promise.all([
page.waitFor(() => {
return window.innerWidth < 100;
}),
page.setViewport({width: 10, height: 10}),
]);
});
it('should wait for predicate with arguments', async () => {
const {page} = getTestState();
await page.waitFor(
(arg1: number, arg2: number) => {
return arg1 !== arg2;
},
{},
1,
2
);
});
it('should log a deprecation warning', async () => {
const {page} = getTestState();
await page.waitFor(() => {
return true;
});
const consoleWarnStub = console.warn as sinon.SinonSpy;
expect(consoleWarnStub.calledOnce).toBe(true);
expect(
consoleWarnStub.firstCall.calledWith(
'waitFor is deprecated and will be removed in a future release. See https://github.com/puppeteer/puppeteer/issues/6214 for details and how to migrate your code.'
)
).toBe(true);
expect((console.warn as sinon.SinonSpy).calledOnce).toBe(true);
});
});
describe('Frame.waitForFunction', function () { describe('Frame.waitForFunction', function () {
it('should accept a string', async () => { it('should accept a string', async () => {
const {page} = getTestState(); const {page} = getTestState();