Skip to content

Commit

Permalink
feat(types): improve page.evaluate types
Browse files Browse the repository at this point in the history
  • Loading branch information
jackfranklin committed Jul 9, 2020
1 parent 03a87e8 commit 29f828d
Show file tree
Hide file tree
Showing 24 changed files with 185 additions and 93 deletions.
18 changes: 12 additions & 6 deletions src/common/DOMWorld.ts
Expand Up @@ -28,6 +28,9 @@ import {
SerializableOrJSHandle,
EvaluateHandleFn,
WrapElementHandle,
EvaluateFn,
EvaluateFnReturnType,
UnwrapPromiseLike,
} from './EvalTypes';
import { isNode } from '../environment';

Expand Down Expand Up @@ -118,12 +121,15 @@ export class DOMWorld {
return context.evaluateHandle(pageFunction, ...args);
}

async evaluate<ReturnType extends any>(
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
async evaluate<T extends EvaluateFn>(
pageFunction: T,
...args: SerializableOrJSHandle[]
): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> {
const context = await this.executionContext();
return context.evaluate<ReturnType>(pageFunction, ...args);
return context.evaluate<UnwrapPromiseLike<EvaluateFnReturnType<T>>>(
pageFunction,
...args
);
}

async $(selector: string): Promise<ElementHandle | null> {
Expand Down Expand Up @@ -206,7 +212,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((html) => {
await this.evaluate<(x: string) => void>((html) => {
document.open();
document.write(html);
document.close();
Expand Down
10 changes: 8 additions & 2 deletions src/common/EvalTypes.ts
Expand Up @@ -19,12 +19,17 @@ import { JSHandle, ElementHandle } from './JSHandle';
/**
* @public
*/
export type EvaluateFn<T = any> = string | ((arg1: T, ...args: any[]) => any);
export type EvaluateFn<T = unknown> =
| string
| ((arg1: T, ...args: unknown[]) => unknown);

export type UnwrapPromiseLike<T> = T extends PromiseLike<infer U> ? U : T;

/**
* @public
*/
export type EvaluateFnReturnType<T extends EvaluateFn> = T extends (
...args: any[]
...args: unknown[]
) => infer R
? R
: unknown;
Expand All @@ -42,6 +47,7 @@ export type Serializable =
| string
| boolean
| null
| BigInt
| JSONArray
| JSONObject;

Expand Down
13 changes: 8 additions & 5 deletions src/common/FrameManager.ts
Expand Up @@ -32,6 +32,9 @@ import {
SerializableOrJSHandle,
EvaluateHandleFn,
WrapElementHandle,
EvaluateFn,
EvaluateFnReturnType,
UnwrapPromiseLike,
} from './EvalTypes';

const UTILITY_WORLD_NAME = '__puppeteer_utility_world__';
Expand Down Expand Up @@ -687,11 +690,11 @@ export class Frame {
* @param pageFunction - a function that is run within the frame
* @param args - arguments to be passed to the pageFunction
*/
async evaluate<ReturnType extends any>(
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
return this._mainWorld.evaluate<ReturnType>(pageFunction, ...args);
async evaluate<T extends EvaluateFn>(
pageFunction: T,
...args: SerializableOrJSHandle[]
): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> {
return this._mainWorld.evaluate<T>(pageFunction, ...args);
}

/**
Expand Down
15 changes: 8 additions & 7 deletions src/common/JSHandle.ts
Expand Up @@ -29,6 +29,7 @@ import {
EvaluateFnReturnType,
EvaluateHandleFn,
WrapElementHandle,
UnwrapPromiseLike,
} from './EvalTypes';

export interface BoxModel {
Expand Down Expand Up @@ -153,12 +154,10 @@ export class JSHandle {
async evaluate<T extends EvaluateFn>(
pageFunction: T | string,
...args: SerializableOrJSHandle[]
): Promise<EvaluateFnReturnType<T>> {
return await this.executionContext().evaluate<EvaluateFnReturnType<T>>(
pageFunction,
this,
...args
);
): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> {
return await this.executionContext().evaluate<
UnwrapPromiseLike<EvaluateFnReturnType<T>>
>(pageFunction, this, ...args);
}

/**
Expand Down Expand Up @@ -619,7 +618,9 @@ export class ElementHandle<
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element.
*/
async focus(): Promise<void> {
await this.evaluate((element) => element.focus());
await this.evaluate<(element: HTMLElement) => void>((element) =>
element.focus()
);
}

/**
Expand Down
22 changes: 15 additions & 7 deletions src/common/Page.ts
Expand Up @@ -45,6 +45,9 @@ import {
SerializableOrJSHandle,
EvaluateHandleFn,
WrapElementHandle,
EvaluateFn,
EvaluateFnReturnType,
UnwrapPromiseLike,
} from './EvalTypes';

const writeFileAsync = promisify(fs.writeFile);
Expand Down Expand Up @@ -1519,6 +1522,13 @@ export class Page extends EventEmitter {
* const aHandle = await page.evaluate('1 + 2');
* ```
*
* To get the best TypeScript experience, you should pass in as the
* generic the type of `pageFunction`:
*
* ```
* const aHandle = await page.evaluate<() => number>(() => 2);
* ```
*
* @example
*
* {@link ElementHandle} instances (including {@link JSHandle}s) can be passed
Expand All @@ -1535,13 +1545,11 @@ export class Page extends EventEmitter {
*
* @returns the return value of `pageFunction`.
*/
async evaluate<ReturnType extends any>(
pageFunction: Function | string,
...args: unknown[]
): Promise<ReturnType> {
return this._frameManager
.mainFrame()
.evaluate<ReturnType>(pageFunction, ...args);
async evaluate<T extends EvaluateFn>(
pageFunction: T,
...args: SerializableOrJSHandle[]
): Promise<UnwrapPromiseLike<EvaluateFnReturnType<T>>> {
return this._frameManager.mainFrame().evaluate<T>(pageFunction, ...args);
}

async evaluateOnNewDocument(
Expand Down
5 changes: 4 additions & 1 deletion test/browsercontext.spec.ts
Expand Up @@ -66,7 +66,10 @@ describe('BrowserContext', function () {
await page.goto(server.EMPTY_PAGE);
const [popupTarget] = await Promise.all([
utils.waitEvent(browser, 'targetcreated'),
page.evaluate((url) => window.open(url), server.EMPTY_PAGE),
page.evaluate<(url: string) => void>(
(url) => window.open(url),
server.EMPTY_PAGE
),
]);
expect(popupTarget.browserContext()).toBe(context);
await context.close();
Expand Down
8 changes: 4 additions & 4 deletions test/cookies.spec.ts
Expand Up @@ -389,9 +389,9 @@ describe('Cookie specs', () => {

await page.goto(server.PREFIX + '/grid.html');
await page.setCookie({ name: 'localhost-cookie', value: 'best' });
await page.evaluate((src) => {
await page.evaluate<(src: string) => Promise<void>>((src) => {
let fulfill;
const promise = new Promise((x) => (fulfill = x));
const promise = new Promise<void>((x) => (fulfill = x));
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
iframe.onload = fulfill;
Expand Down Expand Up @@ -454,9 +454,9 @@ describe('Cookie specs', () => {

try {
await page.goto(httpsServer.PREFIX + '/grid.html');
await page.evaluate((src) => {
await page.evaluate<(src: string) => Promise<void>>((src) => {
let fulfill;
const promise = new Promise((x) => (fulfill = x));
const promise = new Promise<void>((x) => (fulfill = x));
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
iframe.onload = fulfill;
Expand Down
2 changes: 1 addition & 1 deletion test/coverage.spec.ts
Expand Up @@ -264,7 +264,7 @@ describe('Coverage specs', function () {
const { page, server } = getTestState();

await page.coverage.startCSSCoverage();
await page.evaluate(async (url) => {
await page.evaluate<(url: string) => Promise<void>>(async (url) => {
document.body.textContent = 'hello, world';

const link = document.createElement('link');
Expand Down
25 changes: 18 additions & 7 deletions test/elementhandle.spec.ts
Expand Up @@ -67,7 +67,7 @@ describe('ElementHandle specs', function () {
'<div style="width: 100px; height: 100px">hello</div>'
);
const elementHandle = await page.$('div');
await page.evaluate(
await page.evaluate<(element: HTMLElement) => void>(
(element) => (element.style.height = '200px'),
elementHandle
);
Expand All @@ -84,7 +84,7 @@ describe('ElementHandle specs', function () {
`);
const element = await page.$('#therect');
const pptrBoundingBox = await element.boundingBox();
const webBoundingBox = await page.evaluate((e) => {
const webBoundingBox = await page.evaluate((e: HTMLElement) => {
const rect = e.getBoundingClientRect();
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
}, element);
Expand Down Expand Up @@ -211,7 +211,7 @@ describe('ElementHandle specs', function () {

await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate((button) => button.remove(), button);
await page.evaluate((button: HTMLElement) => button.remove(), button);
let error = null;
await button.click().catch((error_) => (error = error_));
expect(error.message).toBe('Node is detached from document');
Expand All @@ -221,7 +221,10 @@ describe('ElementHandle specs', function () {

await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate((button) => (button.style.display = 'none'), button);
await page.evaluate(
(button: HTMLElement) => (button.style.display = 'none'),
button
);
const error = await button.click().catch((error_) => error_);
expect(error.message).toBe(
'Node is either not visible or not an HTMLElement'
Expand All @@ -233,7 +236,7 @@ describe('ElementHandle specs', function () {
await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(
(button) => (button.parentElement.style.display = 'none'),
(button: HTMLElement) => (button.parentElement.style.display = 'none'),
button
);
const error = await button.click().catch((error_) => error_);
Expand Down Expand Up @@ -295,7 +298,12 @@ describe('ElementHandle specs', function () {
(element, selector) => document.querySelector(`[id="${selector}"]`)
);
const element = await page.$('getById/foo');
expect(await page.evaluate((element) => element.id, element)).toBe('foo');
expect(
await page.evaluate<(element: HTMLElement) => string>(
(element) => element.id,
element
)
).toBe('foo');

// Unregister.
puppeteer.__experimental_unregisterCustomQueryHandler('getById');
Expand Down Expand Up @@ -340,7 +348,10 @@ describe('ElementHandle specs', function () {
const classNames = await Promise.all(
elements.map(
async (element) =>
await page.evaluate((element) => element.className, element)
await page.evaluate<(element: HTMLElement) => string>(
(element) => element.className,
element
)
)
);

Expand Down
2 changes: 1 addition & 1 deletion test/emulation.spec.ts
Expand Up @@ -138,7 +138,7 @@ describe('Emulation', () => {
await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(
(button) => (button.style.marginTop = '200px'),
(button: HTMLElement) => (button.style.marginTop = '200px'),
button
);
await button.click();
Expand Down
21 changes: 14 additions & 7 deletions test/evaluation.spec.ts
Expand Up @@ -40,7 +40,7 @@ describe('Evaluation specs', function () {
(bigint ? it : xit)('should transfer BigInt', async () => {
const { page } = getTestState();

const result = await page.evaluate((a) => a, BigInt(42));
const result = await page.evaluate((a: BigInt) => a, BigInt(42));
expect(result).toBe(BigInt(42));
});
it('should transfer NaN', async () => {
Expand Down Expand Up @@ -155,7 +155,11 @@ describe('Evaluation specs', function () {

// Setup inpage callback, which calls Page.evaluate
await page.exposeFunction('callController', async function (a, b) {
return await page.evaluate((a, b) => a * b, a, b);
return await page.evaluate<(a: number, b: number) => number>(
(a, b) => a * b,
a,
b
);
});
const result = await page.evaluate(async function () {
return await globalThis.callController(9, 3);
Expand Down Expand Up @@ -277,7 +281,7 @@ describe('Evaluation specs', function () {
.jsonValue()
.catch((error_) => error_.message);
const error = await page
.evaluate<Error>((errorText) => {
.evaluate<(errorText: string) => Error>((errorText) => {
throw new Error(errorText);
}, errorText)
.catch((error_) => error_);
Expand Down Expand Up @@ -306,7 +310,10 @@ describe('Evaluation specs', function () {

await page.setContent('<section>42</section>');
const element = await page.$('section');
const text = await page.evaluate((e) => e.textContent, element);
const text = await page.evaluate<(e: HTMLElement) => string>(
(e) => e.textContent,
element
);
expect(text).toBe('42');
});
it('should throw if underlying element was disposed', async () => {
Expand All @@ -318,7 +325,7 @@ describe('Evaluation specs', function () {
await element.dispose();
let error = null;
await page
.evaluate((e) => e.textContent, element)
.evaluate((e: HTMLElement) => e.textContent, element)
.catch((error_) => (error = error_));
expect(error.message).toContain('JSHandle is disposed');
});
Expand All @@ -331,7 +338,7 @@ describe('Evaluation specs', function () {
const bodyHandle = await page.frames()[1].$('body');
let error = null;
await page
.evaluate((body) => body.innerHTML, bodyHandle)
.evaluate((body: HTMLElement) => body.innerHTML, bodyHandle)
.catch((error_) => (error = error_));
expect(error).toBeTruthy();
expect(error.message).toContain(
Expand Down Expand Up @@ -379,7 +386,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<any[]>(() =>
const a = await page.evaluate<() => string>(() =>
Array(100 * 1024 * 1024 + 1).join('a')
);
expect(a.length).toBe(100 * 1024 * 1024);
Expand Down
4 changes: 2 additions & 2 deletions test/frame.spec.ts
Expand Up @@ -200,7 +200,7 @@ describe('Frame specs', function () {
const { page, server } = getTestState();

await page.goto(server.PREFIX + '/shadow.html');
await page.evaluate(async (url) => {
await page.evaluate(async (url: string) => {
const frame = document.createElement('iframe');
frame.src = url;
document.body.shadowRoot.appendChild(frame);
Expand All @@ -213,7 +213,7 @@ describe('Frame specs', function () {
const { page, server } = getTestState();

await utils.attachFrame(page, 'theFrameId', server.EMPTY_PAGE);
await page.evaluate((url) => {
await page.evaluate((url: string) => {
const frame = document.createElement('iframe');
frame.name = 'theFrameName';
frame.src = url;
Expand Down

0 comments on commit 29f828d

Please sign in to comment.