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/WebWorker.js';
export * from './common/USKeyboardLayout.js';
export * from './common/EvalTypes.js';
export * from './common/types.js';
export * from './common/PDFOptions.js';
export * from './common/TimeoutSettings.js';
export * from './common/LifecycleWatcher.js';

View File

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

View File

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

View File

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

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 {CDPSession} from './Connection.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 {getExceptionMessage, isString, valueFromRemoteObject} from './util.js';
import {ElementHandle, JSHandle, _createJSHandle} from './JSHandle.js';
import {getExceptionMessage, isString, valueFromRemoteObject} from './util.js';
/**
* @public
@ -134,11 +134,14 @@ export class ExecutionContext {
*
* @returns A promise that resolves to the return value of the given function.
*/
async evaluate<ReturnType>(
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
return await this.#evaluate<ReturnType>(true, pageFunction, ...args);
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
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
* as an in-page object (a {@link JSHandle}).
*/
async evaluateHandle<HandleType extends JSHandle | ElementHandle = JSHandle>(
pageFunction: EvaluateHandleFn,
...args: SerializableOrJSHandle[]
): Promise<HandleType> {
return this.#evaluate<HandleType>(false, pageFunction, ...args);
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
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,
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`;
if (isString(pageFunction)) {
@ -365,7 +390,9 @@ export class ExecutionContext {
*
* @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._remoteObject.objectId,
@ -374,7 +401,7 @@ export class ExecutionContext {
const response = await this._client.send('Runtime.queryObjects', {
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
*/
export class FileChooser {
#element: ElementHandle;
#element: ElementHandle<HTMLInputElement>;
#multiple: boolean;
#handled = false;
@ -45,7 +45,7 @@ export class FileChooser {
* @internal
*/
constructor(
element: ElementHandle,
element: ElementHandle<HTMLInputElement>,
event: Protocol.Page.FileChooserOpenedEvent
) {
this.#element = element;

View File

@ -18,27 +18,19 @@ import {Protocol} from 'devtools-protocol';
import {assert} from './assert.js';
import {CDPSession, Connection} from './Connection.js';
import {DOMWorld, WaitForSelectorOptions} from './DOMWorld.js';
import {
EvaluateFn,
EvaluateFnReturnType,
EvaluateHandleFn,
SerializableOrJSHandle,
UnwrapPromiseLike,
WrapElementHandle,
} from './EvalTypes.js';
import {EventEmitter} from './EventEmitter.js';
import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js';
import {HTTPResponse} from './HTTPResponse.js';
import {MouseButton} from './Input.js';
import {ElementHandle, JSHandle} from './JSHandle.js';
import {ElementHandle} from './JSHandle.js';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {NetworkManager} from './NetworkManager.js';
import {Page} from './Page.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 xPathPattern = /^\(\/\/[^\)]+\)|^\/\//;
/**
* 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 args - arguments to be passed to the pageFunction
*/
async evaluateHandle<HandlerType extends JSHandle = JSHandle>(
pageFunction: EvaluateHandleFn,
...args: SerializableOrJSHandle[]
): Promise<HandlerType> {
return this._mainWorld.evaluateHandle<HandlerType>(pageFunction, ...args);
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
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 args - arguments to be passed to the pageFunction
*/
async evaluate<T extends EvaluateFn>(
pageFunction: T,
...args: SerializableOrJSHandle[]
): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> {
return this._mainWorld.evaluate<T>(pageFunction, ...args);
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
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
* element, or `null` if it was not found.
*/
async $<T extends Element = Element>(
selector: string
): Promise<ElementHandle<T> | null> {
return this._mainWorld.$<T>(selector);
async $<Selector extends keyof HTMLElementTagNameMap>(
selector: Selector
): Promise<ElementHandle<HTMLElementTagNameMap[Selector]> | null>;
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 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,
pageFunction: (
element: Element,
...args: unknown[]
) => ReturnType | Promise<ReturnType>,
...args: SerializableOrJSHandle[]
): Promise<WrapElementHandle<ReturnType>> {
return this._mainWorld.$eval<ReturnType>(selector, pageFunction, ...args);
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>>;
async $eval<
Params extends unknown[],
Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc<
[Element, ...Params]
>
>(
selector: string,
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
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 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,
pageFunction: (
elements: Element[],
...args: unknown[]
) => ReturnType | Promise<ReturnType>,
...args: SerializableOrJSHandle[]
): Promise<WrapElementHandle<ReturnType>> {
return this._mainWorld.$$eval<ReturnType>(selector, pageFunction, ...args);
}
/**
* 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 $$<T extends Element = Element>(
selector: string
): Promise<Array<ElementHandle<T>>> {
return this._mainWorld.$$<T>(selector);
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>>;
async $$eval<
Params extends unknown[],
Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc<
[Element[], ...Params]
>
>(
selector: string,
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
return this._mainWorld.$$eval(selector, pageFunction, ...args);
}
/**
@ -1238,66 +1286,6 @@ export class Frame {
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.
*
@ -1357,6 +1345,14 @@ export class Frame {
* @returns a promise which resolves when an element matching the selector
* 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(
selector: string,
options: WaitForSelectorOptions = {}
@ -1438,12 +1434,20 @@ export class Frame {
* @param args - arguments to pass to the `pageFunction`.
* @returns the promise which resolve when the `pageFunction` returns a truthy value.
*/
waitForFunction(
pageFunction: Function | string,
waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
options: FrameWaitForFunctionOptions = {},
...args: SerializableOrJSHandle[]
): Promise<JSHandle> {
return this._mainWorld.waitForFunction(pageFunction, options, ...args);
...args: EvaluateParams<Params>
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
// 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 {assert} from './assert.js';
import {CDPSession} from './Connection.js';
import {
EvaluateFn,
EvaluateFnReturnType,
EvaluateHandleFn,
SerializableOrJSHandle,
UnwrapPromiseLike,
WrapElementHandle,
} from './EvalTypes.js';
import {EvaluateFunc, EvaluateParams, HandleFor, HandleOr} from './types.js';
import {ExecutionContext} from './ExecutionContext.js';
import {Frame, FrameManager} from './FrameManager.js';
import {MouseButton} from './Input.js';
@ -37,6 +30,7 @@ import {
releaseObject,
valueFromRemoteObject,
} from './util.js';
import {WaitForSelectorOptions} from './DOMWorld.js';
/**
* @public
@ -70,7 +64,7 @@ export interface BoundingBox extends Point {
export function _createJSHandle(
context: ExecutionContext,
remoteObject: Protocol.Runtime.RemoteObject
): JSHandle {
): JSHandle | ElementHandle {
const frame = context.frame();
if (remoteObject.subtype === 'node' && frame) {
const frameManager = frame._frameManager;
@ -114,7 +108,7 @@ const applyOffsetsToQuad = (
*
* @public
*/
export class JSHandle<HandleObjectType = unknown> {
export class JSHandle<T = unknown> {
#client: CDPSession;
#disposed = false;
#context: ExecutionContext;
@ -179,13 +173,14 @@ export class JSHandle<HandleObjectType = unknown> {
* ```
*/
async evaluate<T extends EvaluateFn<HandleObjectType>>(
pageFunction: T | string,
...args: SerializableOrJSHandle[]
): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> {
return await this.executionContext().evaluate<
UnwrapPromiseLike<EvaluateFnReturnType<T>>
>(pageFunction, this, ...args);
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<[T, ...Params]> = EvaluateFunc<[T, ...Params]>
>(
pageFunction: Func | string,
...args: EvaluateParams<Params>
): 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.
*/
async evaluateHandle<HandleType extends JSHandle = JSHandle>(
pageFunction: EvaluateHandleFn,
...args: SerializableOrJSHandle[]
): Promise<HandleType> {
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<[T, ...Params]> = EvaluateFunc<[T, ...Params]>
>(
pageFunction: Func,
...args: EvaluateParams<Params>
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return await this.executionContext().evaluateHandle(
pageFunction,
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> {
const objectHandle = await this.evaluateHandle(
(object: Element, propertyName: keyof Element) => {
const result: Record<string, unknown> = {__proto__: null};
result[propertyName] = object[propertyName];
return result;
},
propertyName
);
const properties = await objectHandle.getProperties();
const result = properties.get(propertyName);
assert(result instanceof JSHandle);
await objectHandle.dispose();
return result;
async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>>;
async getProperty(propertyName: string): Promise<JSHandle<unknown>>;
async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>> {
return await this.evaluateHandle((object, propertyName) => {
return object[propertyName];
}, propertyName);
}
/**
@ -412,13 +407,17 @@ export class ElementHandle<
* (30 seconds). Pass `0` to disable timeout. The default value can be changed
* 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(
selector: string,
options: {
visible?: boolean;
hidden?: boolean;
timeout?: number;
} = {}
options?: Exclude<WaitForSelectorOptions, 'root'>
): Promise<ElementHandle | null>;
async waitForSelector(
selector: string,
options: Exclude<WaitForSelectorOptions, 'root'> = {}
): Promise<ElementHandle | null> {
const frame = this._context.frame();
assert(frame);
@ -539,10 +538,7 @@ export class ElementHandle<
async #scrollIntoViewIfNeeded(): Promise<void> {
const error = await this.evaluate(
async (
element: Element,
pageJavascriptEnabled: boolean
): Promise<string | false> => {
async (element, pageJavascriptEnabled): Promise<string | false> => {
if (!element.isConnected) {
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);
if (!(element instanceof HTMLSelectElement)) {
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,
* paths must be absolute.
*/
async uploadFile(...filePaths: string[]): Promise<void> {
const isMultiple = await this.evaluate<(element: Element) => boolean>(
element => {
if (!(element instanceof HTMLInputElement)) {
throw new Error('uploadFile can only be called on an input element.');
}
return element.multiple;
}
);
async uploadFile(
this: ElementHandle<HTMLInputElement>,
...filePaths: string[]
): Promise<void> {
const isMultiple = await this.evaluate(element => {
return element.multiple;
});
assert(
filePaths.length <= 1 || isMultiple,
'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.
*/
if (files.length === 0) {
await (this as ElementHandle<HTMLInputElement>).evaluate(element => {
await this.evaluate(element => {
element.files = new DataTransfer().files;
// 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.
*/
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();
});
}
@ -1126,9 +1123,11 @@ export class ElementHandle<
* @returns `null` if no element matches the selector.
* @throws `Error` if the selector has no associated query handler.
*/
async $<T extends Element = Element>(
selector: string
): Promise<ElementHandle<T> | null> {
async $<Selector extends keyof HTMLElementTagNameMap>(
selector: Selector
): Promise<ElementHandle<HTMLElementTagNameMap[Selector]> | null>;
async $(selector: string): Promise<ElementHandle | null>;
async $(selector: string): Promise<ElementHandle | null> {
const {updatedSelector, queryHandler} =
_getQueryHandlerAndSelector(selector);
assert(
@ -1149,9 +1148,11 @@ export class ElementHandle<
* @returns `[]` if no element matches the selector.
* @throws `Error` if the selector has no associated query handler.
*/
async $$<T extends Element = Element>(
selector: string
): Promise<Array<ElementHandle<T>>> {
async $$<Selector extends keyof HTMLElementTagNameMap>(
selector: Selector
): Promise<ElementHandle<HTMLElementTagNameMap[Selector]>[]>;
async $$(selector: string): Promise<ElementHandle[]>;
async $$(selector: string): Promise<ElementHandle[]> {
const {updatedSelector, queryHandler} =
_getQueryHandlerAndSelector(selector);
assert(
@ -1176,37 +1177,46 @@ export class ElementHandle<
* 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,
pageFunction: (
element: Element,
...args: unknown[]
) => ReturnType | Promise<ReturnType>,
...args: SerializableOrJSHandle[]
): Promise<WrapElementHandle<ReturnType>> {
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>>;
async $eval<
Params extends unknown[],
Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc<
[Element, ...Params]
>
>(
selector: string,
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
const elementHandle = await this.$(selector);
if (!elementHandle) {
throw new Error(
`Error: failed to find element matching selector "${selector}"`
);
}
const result = await elementHandle.evaluate<
(
element: Element,
...args: SerializableOrJSHandle[]
) => ReturnType | Promise<ReturnType>
>(pageFunction, ...args);
const result = await elementHandle.evaluate(pageFunction, ...args);
await elementHandle.dispose();
/**
* 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>;
return result;
}
/**
@ -1232,28 +1242,44 @@ export class ElementHandle<
* .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,
pageFunction: EvaluateFn<
Element[],
unknown,
ReturnType | Promise<ReturnType>
>,
...args: SerializableOrJSHandle[]
): Promise<WrapElementHandle<ReturnType>> {
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>>;
async $$eval<
Params extends unknown[],
Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc<
[Element[], ...Params]
>
>(
selector: string,
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
const {updatedSelector, queryHandler} =
_getQueryHandlerAndSelector(selector);
assert(queryHandler.queryAllArray);
const arrayHandle = await queryHandler.queryAllArray(this, updatedSelector);
const result = await arrayHandle.evaluate<EvaluateFn<Element[]>>(
pageFunction,
...args
);
const result = await arrayHandle.evaluate(pageFunction, ...args);
await arrayHandle.dispose();
/* This `as` exists for the same reason as the `as` in $eval above.
* See the comment there for a full explanation.
*/
return result as WrapElementHandle<ReturnType>;
return result;
}
/**
@ -1262,24 +1288,21 @@ export class ElementHandle<
* @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate}
*/
async $x(expression: string): Promise<ElementHandle[]> {
const arrayHandle = await this.evaluateHandle(
(element: Document, expression: string) => {
const document = element.ownerDocument || element;
const iterator = document.evaluate(
expression,
element,
null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE
);
const array = [];
let item;
while ((item = iterator.iterateNext())) {
array.push(item);
}
return array;
},
expression
);
const arrayHandle = await this.evaluateHandle((element, expression) => {
const document = element.ownerDocument || element;
const iterator = document.evaluate(
expression,
element,
null,
XPathResult.ORDERED_NODE_ITERATOR_TYPE
);
const array = [];
let item;
while ((item = iterator.iterateNext())) {
array.push(item);
}
return array;
}, expression);
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
@ -1298,8 +1321,8 @@ export class ElementHandle<
async isIntersectingViewport(options?: {
threshold?: number;
}): Promise<boolean> {
const {threshold = 0} = options || {};
return await this.evaluate(async (element: Element, threshold: number) => {
const {threshold = 0} = options ?? {};
return await this.evaluate(async (element, threshold) => {
const visibleRatio = await new Promise<number>(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0]!.intersectionRatio);

View File

@ -23,15 +23,8 @@ import {CDPSession, CDPSessionEmittedEvents, Connection} from './Connection.js';
import {ConsoleMessage, ConsoleMessageType} from './ConsoleMessage.js';
import {Coverage} from './Coverage.js';
import {Dialog} from './Dialog.js';
import {WaitForSelectorOptions} from './DOMWorld.js';
import {EmulationManager} from './EmulationManager.js';
import {
EvaluateFn,
EvaluateFnReturnType,
EvaluateHandleFn,
SerializableOrJSHandle,
UnwrapPromiseLike,
WrapElementHandle,
} from './EvalTypes.js';
import {EventEmitter, Handler} from './EventEmitter.js';
import {FileChooser} from './FileChooser.js';
import {
@ -39,6 +32,23 @@ import {
FrameManager,
FrameManagerEmittedEvents,
} 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 {
debugError,
evaluationString,
@ -57,22 +67,6 @@ import {
waitForEvent,
waitWithTimeout,
} 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';
/**
@ -466,9 +460,7 @@ export class Page extends EventEmitter {
#viewport: Viewport | null;
#screenshotTaskQueue: TaskQueue;
#workers = new Map<string, WebWorker>();
// TODO: improve this typedef - it's a function that takes a file chooser or
// something?
#fileChooserInterceptors = new Set<Function>();
#fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
#disconnectPromise?: Promise<Error>;
#userDragInterceptionEnabled = false;
@ -638,9 +630,13 @@ export class Page extends EventEmitter {
const element = await context._adoptBackendNodeId(event.backendNodeId);
const interceptors = Array.from(this.#fileChooserInterceptors);
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) {
interceptor.call(null, fileChooser);
interceptor.call(undefined, fileChooser);
}
}
@ -736,7 +732,7 @@ export class Page extends EventEmitter {
}
const {timeout = this.#timeoutSettings.timeout()} = options;
let callback!: (value: FileChooser | PromiseLike<FileChooser>) => void;
let callback!: (value: FileChooser) => void;
const promise = new Promise<FileChooser>(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}
* to query page for.
*/
async $<T extends Element = Element>(
selector: string
): Promise<ElementHandle<T> | null> {
return this.mainFrame().$<T>(selector);
async $<Selector extends keyof HTMLElementTagNameMap>(
selector: Selector
): Promise<ElementHandle<HTMLElementTagNameMap[Selector]> | null>;
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 args - arguments to be passed to the pageFunction
*/
async evaluateHandle<HandlerType extends JSHandle = JSHandle>(
pageFunction: EvaluateHandleFn,
...args: SerializableOrJSHandle[]
): Promise<HandlerType> {
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
const context = await this.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
* this prototype.
*/
async queryObjects(prototypeHandle: JSHandle): Promise<JSHandle> {
async queryObjects<Prototype>(
prototypeHandle: JSHandle<Prototype>
): Promise<JSHandle<Prototype[]>> {
const context = await this.mainFrame().executionContext();
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
* 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,
pageFunction: (
element: Element,
/* Unfortunately this has to be unknown[] because it's hard to get
* TypeScript to understand that the arguments will be left alone unless
* they are an ElementHandle, in which case they will be unwrapped.
* The nice thing about unknown vs any is that unknown will force the user
* to type the item before using it to avoid errors.
*
* TODO(@jackfranklin): We could fix this by using overloads like
* DefinitelyTyped does:
* https://github.com/DefinitelyTyped/DefinitelyTyped/blob/HEAD/types/puppeteer/index.d.ts#L114
*/
...args: unknown[]
) => ReturnType | Promise<ReturnType>,
...args: SerializableOrJSHandle[]
): Promise<WrapElementHandle<ReturnType>> {
return this.mainFrame().$eval<ReturnType>(selector, pageFunction, ...args);
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>>;
async $eval<
Params extends unknown[],
Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc<
[Element, ...Params]
>
>(
selector: string,
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
return this.mainFrame().$eval(selector, pageFunction, ...args);
}
/**
@ -1244,32 +1275,38 @@ export class Page extends EventEmitter {
* is wrapped in an {@link ElementHandle}, else the raw value itself is
* 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,
pageFunction: (
elements: Element[],
/* These have to be typed as unknown[] for the same reason as the $eval
* definition above, please see that comment for more details and the TODO
* that will improve things.
*/
...args: unknown[]
) => ReturnType | Promise<ReturnType>,
...args: SerializableOrJSHandle[]
): Promise<WrapElementHandle<ReturnType>> {
return this.mainFrame().$$eval<ReturnType>(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);
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>>;
async $$eval<
Params extends unknown[],
Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc<
[Element[], ...Params]
>
>(
selector: string,
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
return this.mainFrame().$$eval(selector, pageFunction, ...args);
}
/**
@ -1390,7 +1427,7 @@ export class Page extends EventEmitter {
*
* NOTE: Functions installed via `page.exposeFunction` survive navigations.
* @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.
* @example
* An example of adding an `md5` function into the page:
@ -1442,7 +1479,7 @@ export class Page extends EventEmitter {
*/
async exposeFunction(
name: string,
puppeteerFunction: Function | {default: Function}
pptrFunction: Function | {default: Function}
): Promise<void> {
if (this.#pageBindings.has(name)) {
throw new Error(
@ -1451,14 +1488,13 @@ export class Page extends EventEmitter {
}
let exposedFunction: Function;
if (typeof puppeteerFunction === 'function') {
exposedFunction = puppeteerFunction;
} else if (typeof puppeteerFunction.default === 'function') {
exposedFunction = puppeteerFunction.default;
} else {
throw new Error(
`Failed to add page binding with name ${name}: ${puppeteerFunction} is not a function or a module with a default export.`
);
switch (typeof pptrFunction) {
case 'function':
exposedFunction = pptrFunction;
break;
default:
exposedFunction = pptrFunction.default;
break;
}
this.#pageBindings.set(name, exposedFunction);
@ -2640,11 +2676,14 @@ export class Page extends EventEmitter {
*
* @returns the return value of `pageFunction`.
*/
async evaluate<T extends EvaluateFn>(
pageFunction: T,
...args: SerializableOrJSHandle[]
): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> {
return this.#frameManager.mainFrame().evaluate<T>(pageFunction, ...args);
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
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);
* ```
*/
async evaluateOnNewDocument(
pageFunction: Function | string,
...args: unknown[]
): Promise<void> {
async evaluateOnNewDocument<
Params extends unknown[],
Func extends (...args: Params) => unknown = (...args: Params) => unknown
>(pageFunction: Func | string, ...args: Params): Promise<void> {
const source = evaluationString(pageFunction, ...args);
await this.#client.send('Page.addScriptToEvaluateOnNewDocument', {
source,
@ -3203,48 +3242,6 @@ export class Page extends EventEmitter {
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.
*
@ -3316,15 +3313,19 @@ export class Page extends EventEmitter {
* (30 seconds). Pass `0` to disable timeout. The default value can be changed
* 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,
options: {
visible?: boolean;
hidden?: boolean;
timeout?: number;
} = {}
options?: Exclude<WaitForSelectorOptions, 'root'>
): Promise<ElementHandle | null>;
async waitForSelector(
selector: string,
options: Exclude<WaitForSelectorOptions, 'root'> = {}
): 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.
*
*/
waitForFunction(
pageFunction: Function | string,
waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
options: {
timeout?: number;
polling?: string | number;
} = {},
...args: SerializableOrJSHandle[]
): Promise<JSHandle> {
...args: EvaluateParams<Params>
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
}
}

View File

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

View File

@ -16,11 +16,11 @@
import {Protocol} from 'devtools-protocol';
import {CDPSession} from './Connection.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 {ExecutionContext} from './ExecutionContext.js';
import {debugError} from './util.js';
import {JSHandle} from './JSHandle.js';
import {debugError} from './util.js';
/**
* @internal
@ -136,11 +136,14 @@ export class WebWorker extends EventEmitter {
* @param args - Arguments to pass to `pageFunction`.
* @returns Promise which resolves to the return value of `pageFunction`.
*/
async evaluate<ReturnType>(
pageFunction: Function | string,
...args: any[]
): Promise<ReturnType> {
return (await this.#executionContextPromise).evaluate<ReturnType>(
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<Awaited<ReturnType<Func>>> {
return (await this.#executionContextPromise).evaluate(
pageFunction,
...args
);
@ -158,11 +161,14 @@ export class WebWorker extends EventEmitter {
* @param args - Arguments to pass to `pageFunction`.
* @returns Promise which resolves to the return value of `pageFunction`.
*/
async evaluateHandle<HandlerType extends JSHandle = JSHandle>(
pageFunction: EvaluateHandleFn,
...args: SerializableOrJSHandle[]
): Promise<JSHandle> {
return (await this.#executionContextPromise).evaluateHandle<HandlerType>(
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>
>(
pageFunction: Func | string,
...args: EvaluateParams<Params>
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return (await this.#executionContextPromise).evaluateHandle(
pageFunction,
...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.setContent(`<h1>anything</h1>`),
]);
assert(handle);
expect(
await page.evaluate((x: HTMLElement) => {
await page.evaluate(x => {
return x.textContent;
}, handle)
).toBe('anything');
@ -651,7 +652,9 @@ describeChromeOnly('AriaQueryHandler', () => {
});
it('should find by role "button"', async () => {
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);
expect(ids).toEqual([
'node5',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,12 +15,11 @@
*/
import expect from 'expect';
import {JSHandle} from '../../lib/cjs/puppeteer/common/JSHandle.js';
import {
getTestState,
itFailsFirefox,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
itFailsFirefox,
shortWaitForArrayToHaveAtLeastNElements,
} from './mocha-utils.js';
@ -43,7 +42,7 @@ describe('JSHandle', function () {
const navigatorHandle = await page.evaluateHandle(() => {
return navigator;
});
const text = await page.evaluate((e: Navigator) => {
const text = await page.evaluate(e => {
return e.userAgent;
}, navigatorHandle);
expect(text).toContain('Mozilla');
@ -68,9 +67,10 @@ describe('JSHandle', function () {
await page
.evaluateHandle(
opts => {
// @ts-expect-error we are deliberately passing a bad type here
// (nested object)
return opts.elem;
},
// @ts-expect-error we are deliberately passing a bad type here (nested object)
{test}
)
.catch(error_ => {
@ -98,8 +98,8 @@ describe('JSHandle', function () {
return window;
});
expect(
await page.evaluate((e: {FOO: number}) => {
return e.FOO;
await page.evaluate(e => {
return (e as any).FOO;
}, aHandle)
).toBe(123);
});
@ -119,21 +119,6 @@ describe('JSHandle', function () {
const twoHandle = await aHandle.getProperty('two');
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 () {

View File

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

View File

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

View File

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

View File

@ -20,7 +20,6 @@ import path from 'path';
import sinon from 'sinon';
import {CDPSession} from '../../lib/cjs/puppeteer/common/Connection.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 {
describeFailsFirefox,
@ -341,8 +340,8 @@ describe('Page', function () {
});
describe('BrowserContext.overridePermissions', function () {
function getPermission(page: Page, name: string) {
return page.evaluate((name: PermissionName) => {
function getPermission(page: Page, name: PermissionName) {
return page.evaluate(name => {
return navigator.permissions.query({name}).then(result => {
return result.state;
});
@ -559,7 +558,7 @@ describe('Page', function () {
return Set.prototype;
});
const objectsHandle = await page.queryObjects(prototypeHandle);
const count = await page.evaluate((objects: JSHandle[]) => {
const count = await page.evaluate(objects => {
return objects.length;
}, objectsHandle);
expect(count).toBe(1);
@ -580,7 +579,7 @@ describe('Page', function () {
return Set.prototype;
});
const objectsHandle = await page.queryObjects(prototypeHandle);
const count = await page.evaluate((objects: JSHandle[]) => {
const count = await page.evaluate(objects => {
return objects.length;
}, objectsHandle);
expect(count).toBe(1);
@ -1246,11 +1245,9 @@ describe('Page', function () {
return {x: a.x + b.x};
}
);
const result = await page.evaluate<() => Promise<{x: number}>>(
async () => {
return (globalThis as any).complexObject({x: 5}, {x: 2});
}
);
const result = await page.evaluate(async () => {
return (globalThis as any).complexObject({x: 5}, {x: 2});
});
expect(result.x).toBe(7);
});
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 second = await html.$x(`./body/div[contains(@class, 'second')]`);
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;
}, inner[0]!);
expect(content).toBe('A');
@ -480,7 +480,7 @@ describe('querySelector', function () {
const elements = await html.$$('allArray/div');
expect(elements.length).toBe(2);
const promises = elements.map(element => {
return page.evaluate((e: HTMLElement) => {
return page.evaluate(e => {
return e.textContent;
}, element);
});

View File

@ -15,7 +15,6 @@
*/
import expect from 'expect';
import sinon from 'sinon';
import {isErrorLike} from '../../lib/cjs/puppeteer/common/util.js';
import {
getTestState,
@ -29,122 +28,6 @@ describe('waittask specs', function () {
setupTestBrowserHooks();
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 () {
it('should accept a string', async () => {
const {page} = getTestState();