Skip to content

Commit

Permalink
feat(types): improve typing of .evaluate()
Browse files Browse the repository at this point in the history
This is the start of the work to take the types from the
`@types/puppeteer` repository and port them into our repo so we can ship
our built-in types out the box.

This change types the `evaluate` function properly. It takes a generic
type which is the type of the function you're passing, and the arguments
and the return that you get back from the `evaluate` call are typed
correctly.
  • Loading branch information
jackfranklin committed Jun 24, 2020
1 parent db02dfd commit 08d2a0d
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 73 deletions.
9 changes: 5 additions & 4 deletions src/common/DOMWorld.ts
Expand Up @@ -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.
Expand Down Expand Up @@ -157,17 +158,17 @@ export class DOMWorld {

async $eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
pageFunction: EvaluateFn | string,
...args: SerializableOrJSHandle[]
): Promise<ReturnType> {
const document = await this._document();
return document.$eval<ReturnType>(selector, pageFunction, ...args);
}

async $$eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
pageFunction: EvaluateFn | string,
...args: SerializableOrJSHandle[]
): Promise<ReturnType> {
const document = await this._document();
const value = await document.$$eval<ReturnType>(
Expand Down
37 changes: 37 additions & 0 deletions 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<T = any> = string | ((arg1: T, ...args: any[]) => any);
export type EvaluateFnReturnType<T extends EvaluateFn> = 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;
9 changes: 5 additions & 4 deletions src/common/FrameManager.ts
Expand Up @@ -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__';

Expand Down Expand Up @@ -454,16 +455,16 @@ export class Frame {

async $eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
pageFunction: EvaluateFn | string,
...args: SerializableOrJSHandle[]
): Promise<ReturnType> {
return this._mainWorld.$eval<ReturnType>(selector, pageFunction, ...args);
}

async $$eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
pageFunction: EvaluateFn | string,
...args: SerializableOrJSHandle[]
): Promise<ReturnType> {
return this._mainWorld.$$eval<ReturnType>(selector, pageFunction, ...args);
}
Expand Down
132 changes: 71 additions & 61 deletions src/common/JSHandle.ts
Expand Up @@ -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
Expand Down Expand Up @@ -113,11 +118,12 @@ export class JSHandle {
* expect(await tweetHandle.evaluate(node => node.innerText)).toBe('10');
* ```
*/
async evaluate<ReturnType extends any>(
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
return await this.executionContext().evaluate<ReturnType>(

async evaluate<T extends EvaluateFn>(
pageFunction: T | string,
...args: SerializableOrJSHandle[]
): Promise<EvaluateFnReturnType<T>> {
return await this.executionContext().evaluate<EvaluateFnReturnType<T>>(
pageFunction,
this,
...args
Expand Down Expand Up @@ -307,46 +313,48 @@ export class ElementHandle extends JSHandle {
}

private async _scrollIntoViewIfNeeded(): Promise<void> {
const error = await this.evaluate<Promise<string | false>>(
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<string | false>
>(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);
}
Expand Down Expand Up @@ -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<void> {
const isMultiple = await this.evaluate<boolean>(
(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 <input type=file multiple>'
Expand Down Expand Up @@ -772,15 +780,15 @@ export class ElementHandle extends JSHandle {
*/
async $eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
pageFunction: EvaluateFn | string,
...args: SerializableOrJSHandle[]
): Promise<ReturnType> {
const elementHandle = await this.$(selector);
if (!elementHandle)
throw new Error(
`Error: failed to find element matching selector "${selector}"`
);
const result = await elementHandle.evaluate<ReturnType>(
const result = await elementHandle.evaluate<(...args: any[]) => ReturnType>(
pageFunction,
...args
);
Expand Down Expand Up @@ -813,8 +821,8 @@ export class ElementHandle extends JSHandle {
*/
async $$eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
pageFunction: EvaluateFn | string,
...args: SerializableOrJSHandle[]
): Promise<ReturnType> {
const defaultHandler = (element: Element, selector: string) =>
Array.from(element.querySelectorAll(selector));
Expand All @@ -827,7 +835,7 @@ export class ElementHandle extends JSHandle {
queryHandler,
updatedSelector
);
const result = await arrayHandle.evaluate<ReturnType>(
const result = await arrayHandle.evaluate<(...args: any[]) => ReturnType>(
pageFunction,
...args
);
Expand Down Expand Up @@ -868,16 +876,18 @@ export class ElementHandle extends JSHandle {
* Resolves to true if the element is visible in the current viewport.
*/
async isIntersectingViewport(): Promise<boolean> {
return await this.evaluate<Promise<boolean>>(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<boolean>>(
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;
}
);
}
}

Expand Down
9 changes: 5 additions & 4 deletions src/common/Page.ts
Expand Up @@ -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);

Expand Down Expand Up @@ -513,16 +514,16 @@ export class Page extends EventEmitter {

async $eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
pageFunction: EvaluateFn | string,
...args: SerializableOrJSHandle[]
): Promise<ReturnType> {
return this.mainFrame().$eval<ReturnType>(selector, pageFunction, ...args);
}

async $$eval<ReturnType extends any>(
selector: string,
pageFunction: Function | string,
...args: unknown[]
pageFunction: EvaluateFn | string,
...args: SerializableOrJSHandle[]
): Promise<ReturnType> {
return this.mainFrame().$$eval<ReturnType>(selector, pageFunction, ...args);
}
Expand Down

0 comments on commit 08d2a0d

Please sign in to comment.