diff --git a/src/common/DOMWorld.ts b/src/common/DOMWorld.ts index 678ddc4300dd1..2722cc35f393b 100644 --- a/src/common/DOMWorld.ts +++ b/src/common/DOMWorld.ts @@ -25,6 +25,7 @@ import { TimeoutSettings } from './TimeoutSettings'; import { MouseButtonInput } from './Input'; import { FrameManager, Frame } from './FrameManager'; import { getQueryHandlerAndSelector, QueryHandler } from './QueryHandler'; +import { EvaluateFn, SerializableOrJSHandle } from './EvalTypes'; // This predicateQueryHandler is declared here so that TypeScript knows about it // when it is used in the predicate function below. @@ -157,8 +158,8 @@ export class DOMWorld { async $eval( selector: string, - pageFunction: Function | string, - ...args: unknown[] + pageFunction: EvaluateFn | string, + ...args: SerializableOrJSHandle[] ): Promise { const document = await this._document(); return document.$eval(selector, pageFunction, ...args); @@ -166,8 +167,8 @@ export class DOMWorld { async $$eval( selector: string, - pageFunction: Function | string, - ...args: unknown[] + pageFunction: EvaluateFn | string, + ...args: SerializableOrJSHandle[] ): Promise { const document = await this._document(); const value = await document.$$eval( diff --git a/src/common/EvalTypes.ts b/src/common/EvalTypes.ts new file mode 100644 index 0000000000000..17019888b408f --- /dev/null +++ b/src/common/EvalTypes.ts @@ -0,0 +1,37 @@ +/** + * 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 } from './JSHandle'; + +export type EvaluateFn = string | ((arg1: T, ...args: any[]) => any); +export type EvaluateFnReturnType = T extends ( + ...args: any[] +) => infer R + ? R + : unknown; + +export type Serializable = + | number + | string + | boolean + | null + | JSONArray + | JSONObject; +export type JSONArray = Serializable[]; +export interface JSONObject { + [key: string]: Serializable; +} +export type SerializableOrJSHandle = Serializable | JSHandle; diff --git a/src/common/FrameManager.ts b/src/common/FrameManager.ts index 6e56c75332952..859b649eaa92b 100644 --- a/src/common/FrameManager.ts +++ b/src/common/FrameManager.ts @@ -29,6 +29,7 @@ import { MouseButtonInput } from './Input'; import { Page } from './Page'; import { HTTPResponse } from './HTTPResponse'; import Protocol from '../protocol'; +import { EvaluateFn, SerializableOrJSHandle } from './EvalTypes'; const UTILITY_WORLD_NAME = '__puppeteer_utility_world__'; @@ -454,16 +455,16 @@ export class Frame { async $eval( selector: string, - pageFunction: Function | string, - ...args: unknown[] + pageFunction: EvaluateFn | string, + ...args: SerializableOrJSHandle[] ): Promise { return this._mainWorld.$eval(selector, pageFunction, ...args); } async $$eval( selector: string, - pageFunction: Function | string, - ...args: unknown[] + pageFunction: EvaluateFn | string, + ...args: SerializableOrJSHandle[] ): Promise { return this._mainWorld.$$eval(selector, pageFunction, ...args); } diff --git a/src/common/JSHandle.ts b/src/common/JSHandle.ts index 4273db864cd90..2a8f0265b728b 100644 --- a/src/common/JSHandle.ts +++ b/src/common/JSHandle.ts @@ -23,6 +23,11 @@ import { KeyInput } from './USKeyboardLayout'; import { FrameManager, Frame } from './FrameManager'; import { getQueryHandlerAndSelector } from './QueryHandler'; import Protocol from '../protocol'; +import { + EvaluateFn, + SerializableOrJSHandle, + EvaluateFnReturnType, +} from './EvalTypes'; /** * @public @@ -113,11 +118,12 @@ export class JSHandle { * expect(await tweetHandle.evaluate(node => node.innerText)).toBe('10'); * ``` */ - async evaluate( - pageFunction: Function | string, - ...args: unknown[] - ): Promise { - return await this.executionContext().evaluate( + + async evaluate( + pageFunction: T | string, + ...args: SerializableOrJSHandle[] + ): Promise> { + return await this.executionContext().evaluate>( pageFunction, this, ...args @@ -307,46 +313,48 @@ export class ElementHandle extends JSHandle { } private async _scrollIntoViewIfNeeded(): Promise { - const error = await this.evaluate>( - async (element: HTMLElement, pageJavascriptEnabled: boolean) => { - if (!element.isConnected) return 'Node is detached from document'; - if (element.nodeType !== Node.ELEMENT_NODE) - return 'Node is not of type HTMLElement'; - // force-scroll if page's javascript is disabled. - if (!pageJavascriptEnabled) { - element.scrollIntoView({ - block: 'center', - inline: 'center', - // Chrome still supports behavior: instant but it's not in the spec - // so TS shouts We don't want to make this breaking change in - // Puppeteer yet so we'll ignore the line. - // @ts-ignore - behavior: 'instant', - }); - return false; - } - const visibleRatio = await new Promise((resolve) => { - const observer = new IntersectionObserver((entries) => { - resolve(entries[0].intersectionRatio); - observer.disconnect(); - }); - observer.observe(element); + const error = await this.evaluate< + ( + element: HTMLElement, + pageJavascriptEnabled: boolean + ) => Promise + >(async (element, pageJavascriptEnabled) => { + if (!element.isConnected) return 'Node is detached from document'; + if (element.nodeType !== Node.ELEMENT_NODE) + return 'Node is not of type HTMLElement'; + // force-scroll if page's javascript is disabled. + if (!pageJavascriptEnabled) { + element.scrollIntoView({ + block: 'center', + inline: 'center', + // Chrome still supports behavior: instant but it's not in the spec + // so TS shouts We don't want to make this breaking change in + // Puppeteer yet so we'll ignore the line. + // @ts-ignore + behavior: 'instant', }); - if (visibleRatio !== 1.0) { - element.scrollIntoView({ - block: 'center', - inline: 'center', - // Chrome still supports behavior: instant but it's not in the spec - // so TS shouts We don't want to make this breaking change in - // Puppeteer yet so we'll ignore the line. - // @ts-ignore - behavior: 'instant', - }); - } return false; - }, - this._page.isJavaScriptEnabled() - ); + } + const visibleRatio = await new Promise((resolve) => { + const observer = new IntersectionObserver((entries) => { + resolve(entries[0].intersectionRatio); + observer.disconnect(); + }); + observer.observe(element); + }); + if (visibleRatio !== 1.0) { + element.scrollIntoView({ + block: 'center', + inline: 'center', + // Chrome still supports behavior: instant but it's not in the spec + // so TS shouts We don't want to make this breaking change in + // Puppeteer yet so we'll ignore the line. + // @ts-ignore + behavior: 'instant', + }); + } + return false; + }, this._page.isJavaScriptEnabled()); if (error) throw new Error(error); } @@ -491,9 +499,9 @@ export class ElementHandle extends JSHandle { * relative to the {@link https://nodejs.org/api/process.html#process_process_cwd | current working directory} */ async uploadFile(...filePaths: string[]): Promise { - const isMultiple = await this.evaluate( - (element: HTMLInputElement) => element.multiple - ); + const isMultiple = await this.evaluate< + (element: HTMLInputElement) => boolean + >((element) => element.multiple); assert( filePaths.length <= 1 || isMultiple, 'Multiple file uploads only work with ' @@ -772,15 +780,15 @@ export class ElementHandle extends JSHandle { */ async $eval( selector: string, - pageFunction: Function | string, - ...args: unknown[] + pageFunction: EvaluateFn | string, + ...args: SerializableOrJSHandle[] ): Promise { const elementHandle = await this.$(selector); if (!elementHandle) throw new Error( `Error: failed to find element matching selector "${selector}"` ); - const result = await elementHandle.evaluate( + const result = await elementHandle.evaluate<(...args: any[]) => ReturnType>( pageFunction, ...args ); @@ -813,8 +821,8 @@ export class ElementHandle extends JSHandle { */ async $$eval( selector: string, - pageFunction: Function | string, - ...args: unknown[] + pageFunction: EvaluateFn | string, + ...args: SerializableOrJSHandle[] ): Promise { const defaultHandler = (element: Element, selector: string) => Array.from(element.querySelectorAll(selector)); @@ -827,7 +835,7 @@ export class ElementHandle extends JSHandle { queryHandler, updatedSelector ); - const result = await arrayHandle.evaluate( + const result = await arrayHandle.evaluate<(...args: any[]) => ReturnType>( pageFunction, ...args ); @@ -868,16 +876,18 @@ export class ElementHandle extends JSHandle { * Resolves to true if the element is visible in the current viewport. */ async isIntersectingViewport(): Promise { - return await this.evaluate>(async (element) => { - const visibleRatio = await new Promise((resolve) => { - const observer = new IntersectionObserver((entries) => { - resolve(entries[0].intersectionRatio); - observer.disconnect(); + return await this.evaluate<(element: Element) => Promise>( + async (element) => { + const visibleRatio = await new Promise((resolve) => { + const observer = new IntersectionObserver((entries) => { + resolve(entries[0].intersectionRatio); + observer.disconnect(); + }); + observer.observe(element); }); - observer.observe(element); - }); - return visibleRatio > 0; - }); + return visibleRatio > 0; + } + ); } } diff --git a/src/common/Page.ts b/src/common/Page.ts index 2ded9343f9f21..0fcc0e267f05d 100644 --- a/src/common/Page.ts +++ b/src/common/Page.ts @@ -41,6 +41,7 @@ import { FileChooser } from './FileChooser'; import { ConsoleMessage, ConsoleMessageType } from './ConsoleMessage'; import { PuppeteerLifeCycleEvent } from './LifecycleWatcher'; import Protocol from '../protocol'; +import { EvaluateFn, SerializableOrJSHandle } from './EvalTypes'; const writeFileAsync = helper.promisify(fs.writeFile); @@ -513,16 +514,16 @@ export class Page extends EventEmitter { async $eval( selector: string, - pageFunction: Function | string, - ...args: unknown[] + pageFunction: EvaluateFn | string, + ...args: SerializableOrJSHandle[] ): Promise { return this.mainFrame().$eval(selector, pageFunction, ...args); } async $$eval( selector: string, - pageFunction: Function | string, - ...args: unknown[] + pageFunction: EvaluateFn | string, + ...args: SerializableOrJSHandle[] ): Promise { return this.mainFrame().$$eval(selector, pageFunction, ...args); }