From 26c3acbb0795eb66f29479f442e156832f794f01 Mon Sep 17 00:00:00 2001 From: jrandolf <101637635+jrandolf@users.noreply.github.com> Date: Thu, 23 Jun 2022 11:29:46 +0200 Subject: [PATCH] feat!: type inference for evaluation types (#8547) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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(a: number, b: number) => {...}, a, b)` ✅ `(await evaluate(a, b) => {...}, a, b)) as ReturnType` ⛔ `evaluateHandle(a: number, b: number) => {...}, a, b)` ✅ `(await evaluateHandle(a, b) => {...}, a, b)) as HandleFor` - 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)` --- src/api-docs-entry.ts | 2 +- src/common/AriaQueryHandler.ts | 4 +- src/common/Connection.ts | 4 +- src/common/DOMWorld.ts | 175 +++++++++++------ src/common/EvalTypes.ts | 83 -------- src/common/ExecutionContext.ts | 63 ++++-- src/common/FileChooser.ts | 4 +- src/common/FrameManager.ts | 240 ++++++++++++----------- src/common/JSHandle.ts | 267 +++++++++++++------------ src/common/Page.ts | 314 +++++++++++++++--------------- src/common/QueryHandler.ts | 17 +- src/common/WebWorker.ts | 30 +-- src/common/types.ts | 32 +++ test/src/ariaqueryhandler.spec.ts | 7 +- test/src/browsercontext.spec.ts | 8 +- test/src/cookies.spec.ts | 6 +- test/src/coverage.spec.ts | 2 +- test/src/elementhandle.spec.ts | 28 ++- test/src/evaluation.spec.ts | 4 +- test/src/idle_override.spec.ts | 5 +- test/src/jshandle.spec.ts | 27 +-- test/src/keyboard.spec.ts | 4 +- test/src/mouse.spec.ts | 8 +- test/src/network.spec.ts | 4 +- test/src/page.spec.ts | 17 +- test/src/queryselector.spec.ts | 4 +- test/src/waittask.spec.ts | 117 ----------- 27 files changed, 701 insertions(+), 775 deletions(-) delete mode 100644 src/common/EvalTypes.ts create mode 100644 src/common/types.ts diff --git a/src/api-docs-entry.ts b/src/api-docs-entry.ts index dbcf14387e43a..bc53f565bd4f5 100644 --- a/src/api-docs-entry.ts +++ b/src/api-docs-entry.ts @@ -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'; diff --git a/src/common/AriaQueryHandler.ts b/src/common/AriaQueryHandler.ts index 22571ccaf927e..f7cb4229250da 100644 --- a/src/common/AriaQueryHandler.ts +++ b/src/common/AriaQueryHandler.ts @@ -141,7 +141,7 @@ const queryAll = async ( const queryAllArray = async ( element: ElementHandle, selector: string -): Promise => { +): Promise> => { 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, diff --git a/src/common/Connection.ts b/src/common/Connection.ts index 07b124a1edc6a..0b5c46f5bec0f 100644 --- a/src/common/Connection.ts +++ b/src/common/Connection.ts @@ -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; } diff --git a/src/common/DOMWorld.ts b/src/common/DOMWorld.ts index 315ee07a56478..bb1813f105a6e 100644 --- a/src/common/DOMWorld.ts +++ b/src/common/DOMWorld.ts @@ -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( - pageFunction: EvaluateHandleFn, - ...args: SerializableOrJSHandle[] - ): Promise { + async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>>> { const context = await this.executionContext(); return context.evaluateHandle(pageFunction, ...args); } - async evaluate( - pageFunction: T, - ...args: SerializableOrJSHandle[] - ): Promise>> { + async evaluate< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>> { const context = await this.executionContext(); - return context.evaluate>>( - pageFunction, - ...args - ); + return context.evaluate(pageFunction, ...args); } - async $( - selector: string - ): Promise | null> { + async $( + selector: Selector + ): Promise | null>; + async $(selector: string): Promise; + async $(selector: string): Promise { const document = await this._document(); - const value = await document.$(selector); + const value = await document.$(selector); + return value; + } + + async $$( + selector: Selector + ): Promise[]>; + async $$(selector: string): Promise; + async $$(selector: string): Promise { + const document = await this._document(); + const value = await document.$$(selector); return value; } @@ -235,40 +243,74 @@ export class DOMWorld { return value; } - async $eval( + 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 + ): Promise>>; + async $eval< + Params extends unknown[], + Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc< + [Element, ...Params] + > + >( selector: string, - pageFunction: ( - element: Element, - ...args: unknown[] - ) => ReturnType | Promise, - ...args: SerializableOrJSHandle[] - ): Promise> { - const document = await this._document(); - return document.$eval(selector, pageFunction, ...args); - } - - async $$eval( + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>>; + async $eval< + Params extends unknown[], + Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc< + [Element, ...Params] + > + >( selector: string, - pageFunction: ( - elements: Element[], - ...args: unknown[] - ) => ReturnType | Promise, - ...args: SerializableOrJSHandle[] - ): Promise> { + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>> { const document = await this._document(); - const value = await document.$$eval( - selector, - pageFunction, - ...args - ); - return value; + return document.$eval(selector, pageFunction, ...args); } - async $$( - selector: string - ): Promise>> { + 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 + ): Promise>>; + async $$eval< + Params extends unknown[], + Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc< + [Element[], ...Params] + > + >( + selector: string, + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>>; + async $$eval< + Params extends unknown[], + Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc< + [Element[], ...Params] + > + >( + selector: string, + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>> { const document = await this._document(); - const value = await document.$$(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 { 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: Selector, + options: WaitForSelectorOptions + ): Promise | null>; + async waitForSelector( + selector: string, + options: WaitForSelectorOptions + ): Promise; 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 { 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; diff --git a/src/common/EvalTypes.ts b/src/common/EvalTypes.ts deleted file mode 100644 index 1bb7f49da9960..0000000000000 --- a/src/common/EvalTypes.ts +++ /dev/null @@ -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 = - | string - | ((arg1: T, ...args: U[]) => V); -/** - * @public - */ -export type UnwrapPromiseLike = T extends PromiseLike ? U : T; - -/** - * @public - */ -export type EvaluateFnReturnType = 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 extends Element ? ElementHandle : X; - -/** - * Unwraps a DOM element out of an ElementHandle instance - * @public - **/ -export type UnwrapElementHandle = X extends ElementHandle ? E : X; diff --git a/src/common/ExecutionContext.ts b/src/common/ExecutionContext.ts index 2c36a6260af10..b32381ca540e4 100644 --- a/src/common/ExecutionContext.ts +++ b/src/common/ExecutionContext.ts @@ -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( - pageFunction: Function | string, - ...args: unknown[] - ): Promise { - return await this.#evaluate(true, pageFunction, ...args); + async evaluate< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>> { + 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( - pageFunction: EvaluateHandleFn, - ...args: SerializableOrJSHandle[] - ): Promise { - return this.#evaluate(false, pageFunction, ...args); + async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>>> { + return this.#evaluate(false, pageFunction, ...args); } - async #evaluate( + async #evaluate< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + returnByValue: true, + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>>; + async #evaluate< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + returnByValue: false, + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>>>; + async #evaluate< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( returnByValue: boolean, - pageFunction: Function | string, - ...args: unknown[] - ): Promise { + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>> | Awaited>> { 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 { + async queryObjects( + prototypeHandle: JSHandle + ): Promise> { 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; } /** diff --git a/src/common/FileChooser.ts b/src/common/FileChooser.ts index 3ce1003bd92c3..4ce632d4aa343 100644 --- a/src/common/FileChooser.ts +++ b/src/common/FileChooser.ts @@ -37,7 +37,7 @@ import {assert} from './assert.js'; * @public */ export class FileChooser { - #element: ElementHandle; + #element: ElementHandle; #multiple: boolean; #handled = false; @@ -45,7 +45,7 @@ export class FileChooser { * @internal */ constructor( - element: ElementHandle, + element: ElementHandle, event: Protocol.Page.FileChooserOpenedEvent ) { this.#element = element; diff --git a/src/common/FrameManager.ts b/src/common/FrameManager.ts index a4c4989b31193..c2454383ef655 100644 --- a/src/common/FrameManager.ts +++ b/src/common/FrameManager.ts @@ -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( - pageFunction: EvaluateHandleFn, - ...args: SerializableOrJSHandle[] - ): Promise { - return this._mainWorld.evaluateHandle(pageFunction, ...args); + async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>>> { + 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( - pageFunction: T, - ...args: SerializableOrJSHandle[] - ): Promise>> { - return this._mainWorld.evaluate(pageFunction, ...args); + async evaluate< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>> { + 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 $( - selector: string - ): Promise | null> { - return this._mainWorld.$(selector); + async $( + selector: Selector + ): Promise | null>; + async $(selector: string): Promise; + async $(selector: string): Promise { + 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: Selector + ): Promise[]>; + async $$(selector: string): Promise; + async $$(selector: string): Promise { + 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( + 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 + ): Promise>>; + async $eval< + Params extends unknown[], + Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc< + [Element, ...Params] + > + >( selector: string, - pageFunction: ( - element: Element, - ...args: unknown[] - ) => ReturnType | Promise, - ...args: SerializableOrJSHandle[] - ): Promise> { - return this._mainWorld.$eval(selector, pageFunction, ...args); + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>>; + async $eval< + Params extends unknown[], + Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc< + [Element, ...Params] + > + >( + selector: string, + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>> { + 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( + 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 + ): Promise>>; + async $$eval< + Params extends unknown[], + Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc< + [Element[], ...Params] + > + >( selector: string, - pageFunction: ( - elements: Element[], - ...args: unknown[] - ) => ReturnType | Promise, - ...args: SerializableOrJSHandle[] - ): Promise> { - return this._mainWorld.$$eval(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 $$( - selector: string - ): Promise>> { - return this._mainWorld.$$(selector); + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>>; + async $$eval< + Params extends unknown[], + Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc< + [Element[], ...Params] + > + >( + selector: string, + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>> { + 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 = {}, - ...args: SerializableOrJSHandle[] - ): Promise { - 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: Selector, + options?: WaitForSelectorOptions + ): Promise | null>; + async waitForSelector( + selector: string, + options?: WaitForSelectorOptions + ): Promise; 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 = EvaluateFunc + >( + pageFunction: Func | string, options: FrameWaitForFunctionOptions = {}, - ...args: SerializableOrJSHandle[] - ): Promise { - return this._mainWorld.waitForFunction(pageFunction, options, ...args); + ...args: EvaluateParams + ): Promise>>> { + // TODO: Fix when NodeHandle has been added. + return this._mainWorld.waitForFunction( + pageFunction, + options, + ...args + ) as Promise>>>; } /** diff --git a/src/common/JSHandle.ts b/src/common/JSHandle.ts index addf5db1d8d7f..11364fd783889 100644 --- a/src/common/JSHandle.ts +++ b/src/common/JSHandle.ts @@ -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 { +export class JSHandle { #client: CDPSession; #disposed = false; #context: ExecutionContext; @@ -179,13 +173,14 @@ export class JSHandle { * ``` */ - async evaluate>( - pageFunction: T | string, - ...args: SerializableOrJSHandle[] - ): Promise>> { - return await this.executionContext().evaluate< - UnwrapPromiseLike> - >(pageFunction, this, ...args); + async evaluate< + Params extends unknown[], + Func extends EvaluateFunc<[T, ...Params]> = EvaluateFunc<[T, ...Params]> + >( + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>> { + return await this.executionContext().evaluate(pageFunction, this, ...args); } /** @@ -203,10 +198,13 @@ export class JSHandle { * * See {@link Page.evaluateHandle} for more details. */ - async evaluateHandle( - pageFunction: EvaluateHandleFn, - ...args: SerializableOrJSHandle[] - ): Promise { + async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc<[T, ...Params]> = EvaluateFunc<[T, ...Params]> + >( + pageFunction: Func, + ...args: EvaluateParams + ): Promise>>> { return await this.executionContext().evaluateHandle( pageFunction, this, @@ -214,22 +212,19 @@ export class JSHandle { ); } - /** Fetches a single property from the referenced object. + /** + * Fetches a single property from the referenced object. */ - async getProperty(propertyName: string): Promise { - const objectHandle = await this.evaluateHandle( - (object: Element, propertyName: keyof Element) => { - const result: Record = {__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( + propertyName: HandleOr + ): Promise>; + async getProperty(propertyName: string): Promise>; + async getProperty( + propertyName: HandleOr + ): Promise> { + 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: Selector, + options?: Exclude + ): Promise | null>; async waitForSelector( selector: string, - options: { - visible?: boolean; - hidden?: boolean; - timeout?: number; - } = {} + options?: Exclude + ): Promise; + async waitForSelector( + selector: string, + options: Exclude = {} ): Promise { const frame = this._context.frame(); assert(frame); @@ -539,10 +538,7 @@ export class ElementHandle< async #scrollIntoViewIfNeeded(): Promise { const error = await this.evaluate( - async ( - element: Element, - pageJavascriptEnabled: boolean - ): Promise => { + async (element, pageJavascriptEnabled): Promise => { 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 ' @@ -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).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 { - await (this as ElementHandle).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 $( - selector: string - ): Promise | null> { + async $( + selector: Selector + ): Promise | null>; + async $(selector: string): Promise; + async $(selector: string): Promise { 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 $$( - selector: string - ): Promise>> { + async $$( + selector: Selector + ): Promise[]>; + async $$(selector: string): Promise; + async $$(selector: string): Promise { 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( + 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 + ): Promise>>; + async $eval< + Params extends unknown[], + Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc< + [Element, ...Params] + > + >( + selector: string, + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>>; + async $eval< + Params extends unknown[], + Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc< + [Element, ...Params] + > + >( selector: string, - pageFunction: ( - element: Element, - ...args: unknown[] - ) => ReturnType | Promise, - ...args: SerializableOrJSHandle[] - ): Promise> { + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>> { 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 - >(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 if it is an ElementHandle, or leave it alone as - * ReturnType if it isn't. - */ - return result as WrapElementHandle; + return result; } /** @@ -1232,28 +1242,44 @@ export class ElementHandle< * .toEqual(['Hello!', 'Hi!']); * ``` */ - async $$eval( + 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 + ): Promise>>; + async $$eval< + Params extends unknown[], + Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc< + [Element[], ...Params] + > + >( selector: string, - pageFunction: EvaluateFn< - Element[], - unknown, - ReturnType | Promise - >, - ...args: SerializableOrJSHandle[] - ): Promise> { + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>>; + async $$eval< + Params extends unknown[], + Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc< + [Element[], ...Params] + > + >( + selector: string, + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>> { const {updatedSelector, queryHandler} = _getQueryHandlerAndSelector(selector); assert(queryHandler.queryAllArray); const arrayHandle = await queryHandler.queryAllArray(this, updatedSelector); - const result = await arrayHandle.evaluate>( - 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; + 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 { - 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 { - 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(resolve => { const observer = new IntersectionObserver(entries => { resolve(entries[0]!.intersectionRatio); diff --git a/src/common/Page.ts b/src/common/Page.ts index d742f2c848c40..eea64080580b4 100644 --- a/src/common/Page.ts +++ b/src/common/Page.ts @@ -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(); - // TODO: improve this typedef - it's a function that takes a file chooser or - // something? - #fileChooserInterceptors = new Set(); + #fileChooserInterceptors = new Set<(chooser: FileChooser) => void>(); #disconnectPromise?: Promise; #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, + 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) => void; + let callback!: (value: FileChooser) => void; const promise = new Promise(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 $( - selector: string - ): Promise | null> { - return this.mainFrame().$(selector); + async $( + selector: Selector + ): Promise | null>; + async $(selector: string): Promise; + async $(selector: string): Promise { + 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: Selector + ): Promise[]>; + async $$(selector: string): Promise; + async $$(selector: string): Promise { + 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( - pageFunction: EvaluateHandleFn, - ...args: SerializableOrJSHandle[] - ): Promise { + async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>>> { const context = await this.mainFrame().executionContext(); - return context.evaluateHandle(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 { + async queryObjects( + prototypeHandle: JSHandle + ): Promise> { 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( + 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 + ): Promise>>; + async $eval< + Params extends unknown[], + Func extends EvaluateFunc<[Element, ...Params]> = EvaluateFunc< + [Element, ...Params] + > + >( + selector: string, + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>>; + 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, - ...args: SerializableOrJSHandle[] - ): Promise> { - return this.mainFrame().$eval(selector, pageFunction, ...args); + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>> { + 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( + 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 + ): Promise>>; + 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, - ...args: SerializableOrJSHandle[] - ): Promise> { - 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 $$( - selector: string - ): Promise>> { - return this.mainFrame().$$(selector); + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>>; + async $$eval< + Params extends unknown[], + Func extends EvaluateFunc<[Element[], ...Params]> = EvaluateFunc< + [Element[], ...Params] + > + >( + selector: string, + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>> { + 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 { 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( - pageFunction: T, - ...args: SerializableOrJSHandle[] - ): Promise>> { - return this.#frameManager.mainFrame().evaluate(pageFunction, ...args); + async evaluate< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>> { + 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 { + async evaluateOnNewDocument< + Params extends unknown[], + Func extends (...args: Params) => unknown = (...args: Params) => unknown + >(pageFunction: Func | string, ...args: Params): Promise { 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 { - 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: Selector, + options?: Exclude + ): Promise | null>; + async waitForSelector( selector: string, - options: { - visible?: boolean; - hidden?: boolean; - timeout?: number; - } = {} + options?: Exclude + ): Promise; + async waitForSelector( + selector: string, + options: Exclude = {} ): Promise { - 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 = EvaluateFunc + >( + pageFunction: Func | string, options: { timeout?: number; polling?: string | number; } = {}, - ...args: SerializableOrJSHandle[] - ): Promise { + ...args: EvaluateParams + ): Promise>>> { return this.mainFrame().waitForFunction(pageFunction, options, ...args); } } diff --git a/src/common/QueryHandler.ts b/src/common/QueryHandler.ts index a071e3db37b3e..0ffb2345210c4 100644 --- a/src/common/QueryHandler.ts +++ b/src/common/QueryHandler.ts @@ -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) => { - return Array.from(res); - } - ); + const resultHandle = (await element.evaluateHandle( + queryAll, + selector + )) as JSHandle>; + 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); diff --git a/src/common/WebWorker.ts b/src/common/WebWorker.ts index e3c19c503a0f5..d7811aa8c6b93 100644 --- a/src/common/WebWorker.ts +++ b/src/common/WebWorker.ts @@ -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( - pageFunction: Function | string, - ...args: any[] - ): Promise { - return (await this.#executionContextPromise).evaluate( + async evaluate< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>> { + 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( - pageFunction: EvaluateHandleFn, - ...args: SerializableOrJSHandle[] - ): Promise { - return (await this.#executionContextPromise).evaluateHandle( + async evaluateHandle< + Params extends unknown[], + Func extends EvaluateFunc = EvaluateFunc + >( + pageFunction: Func | string, + ...args: EvaluateParams + ): Promise>>> { + return (await this.#executionContextPromise).evaluateHandle( pageFunction, ...args ); diff --git a/src/common/types.ts b/src/common/types.ts new file mode 100644 index 0000000000000..fc89417fd2ba9 --- /dev/null +++ b/src/common/types.ts @@ -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 | PromiseLike; + +export type HandleFor = T extends Element ? ElementHandle : JSHandle; +export type HandleOr = HandleFor | JSHandle | T; + +export type EvaluateParams = { + [K in keyof T]: T[K] extends HandleOr ? T[K] : HandleOr; +}; +export type InnerParams = { + [K in keyof T]: T[K] extends HandleOr ? U : never; +}; +export type EvaluateFunc = ( + ...params: InnerParams +) => Awaitable; diff --git a/test/src/ariaqueryhandler.spec.ts b/test/src/ariaqueryhandler.spec.ts index f26a60cc50a11..b25dd35ca64f4 100644 --- a/test/src/ariaqueryhandler.spec.ts +++ b/test/src/ariaqueryhandler.spec.ts @@ -276,8 +276,9 @@ describeChromeOnly('AriaQueryHandler', () => { page.waitForSelector('aria/anything'), page.setContent(`

anything

`), ]); + 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.$$('aria/[role="button"]'); + const found = (await page.$$( + 'aria/[role="button"]' + )) as ElementHandle[]; const ids = await getIds(found); expect(ids).toEqual([ 'node5', diff --git a/test/src/browsercontext.spec.ts b/test/src/browsercontext.spec.ts index d7530e8c95f28..d7d5d6adaa1e6 100644 --- a/test/src/browsercontext.spec.ts +++ b/test/src/browsercontext.spec.ts @@ -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), ]); diff --git a/test/src/cookies.spec.ts b/test/src/cookies.spec.ts index b4c74862e8ce7..428b54deec2ae 100644 --- a/test/src/cookies.spec.ts +++ b/test/src/cookies.spec.ts @@ -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>(src => { + await page.evaluate(src => { let fulfill!: () => void; const promise = new Promise(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>(src => { + await page.evaluate(src => { let fulfill!: () => void; const promise = new Promise(x => { return (fulfill = x); diff --git a/test/src/coverage.spec.ts b/test/src/coverage.spec.ts index 0a753f58ec4b4..41eb089004d8e 100644 --- a/test/src/coverage.spec.ts +++ b/test/src/coverage.spec.ts @@ -287,7 +287,7 @@ describe('Coverage specs', function () { const {page, server} = getTestState(); await page.coverage.startCSSCoverage(); - await page.evaluate<(url: string) => Promise>(async url => { + await page.evaluate(async url => { document.body.textContent = 'hello, world'; const link = document.createElement('link'); diff --git a/test/src/elementhandle.spec.ts b/test/src/elementhandle.spec.ts index 612afbea5138a..ec59d1d8d7ccc 100644 --- a/test/src/elementhandle.spec.ts +++ b/test/src/elementhandle.spec.ts @@ -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(() => { + 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(() => { - 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( diff --git a/test/src/evaluation.spec.ts b/test/src/evaluation.spec.ts index 59823d7483ebf..6cd9cc251d0b2 100644 --- a/test/src/evaluation.spec.ts +++ b/test/src/evaluation.spec.ts @@ -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); diff --git a/test/src/idle_override.spec.ts b/test/src/idle_override.spec.ts index 55ed62a6a1de2..1b397209a83ad 100644 --- a/test/src/idle_override.spec.ts +++ b/test/src/idle_override.spec.ts @@ -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; + return await page.evaluate(element => { return element.innerText; }, stateElement); } diff --git a/test/src/jshandle.spec.ts b/test/src/jshandle.spec.ts index 5da7a5ce4d04a..2651bf3330595 100644 --- a/test/src/jshandle.spec.ts +++ b/test/src/jshandle.spec.ts @@ -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 () { diff --git a/test/src/keyboard.spec.ts b/test/src/keyboard.spec.ts index 035727d251ae8..43ce79b207ead 100644 --- a/test/src/keyboard.spec.ts +++ b/test/src/keyboard.spec.ts @@ -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 🇯🇵'); }); diff --git a/test/src/mouse.spec.ts b/test/src/mouse.spec.ts index c144c6d0d4796..078941e6b6ff2 100644 --- a/test/src/mouse.spec.ts +++ b/test/src/mouse.spec.ts @@ -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)); }); diff --git a/test/src/network.spec.ts b/test/src/network.spec.ts index f7420547c4091..00e255851f904 100644 --- a/test/src/network.spec.ts +++ b/test/src/network.spec.ts @@ -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(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(); diff --git a/test/src/page.spec.ts b/test/src/page.spec.ts index ea091d757cb42..f350fa9bc900b 100644 --- a/test/src/page.spec.ts +++ b/test/src/page.spec.ts @@ -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 () => { diff --git a/test/src/queryselector.spec.ts b/test/src/queryselector.spec.ts index 2d3ae42e1a79c..9881d5e4f965c 100644 --- a/test/src/queryselector.spec.ts +++ b/test/src/queryselector.spec.ts @@ -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); }); diff --git a/test/src/waittask.spec.ts b/test/src/waittask.spec.ts index 413cd3db1c094..b75d19f744931 100644 --- a/test/src/waittask.spec.ts +++ b/test/src/waittask.spec.ts @@ -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(`
some text
`); - 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();