From 5049b83186b599ef0b0128cdd3a69b9ffaa0b226 Mon Sep 17 00:00:00 2001 From: Jack Franklin Date: Fri, 3 Jul 2020 15:23:51 +0100 Subject: [PATCH] feat(types): add types for `page.$$eval` (#6139) * feat(types): add types for `page.$$eval` * Add new-docs for $$eval * fix example * linting --- new-docs/puppeteer.elementhandle.__eval.md | 6 +- new-docs/puppeteer.frame.__eval.md | 6 +- new-docs/puppeteer.page.__eval.md | 39 ++++++++++--- src/common/DOMWorld.ts | 10 ++-- src/common/FrameManager.ts | 10 ++-- src/common/JSHandle.ts | 24 +++++--- src/common/Page.ts | 66 +++++++++++++++++----- test/queryselector.spec.ts | 4 +- 8 files changed, 119 insertions(+), 46 deletions(-) diff --git a/new-docs/puppeteer.elementhandle.__eval.md b/new-docs/puppeteer.elementhandle.__eval.md index f351b5ef70c81..ccacd727f91c2 100644 --- a/new-docs/puppeteer.elementhandle.__eval.md +++ b/new-docs/puppeteer.elementhandle.__eval.md @@ -11,7 +11,7 @@ If `pageFunction` returns a Promise, then `frame.$$eval` would wait for the prom Signature: ```typescript -$$eval(selector: string, pageFunction: EvaluateFn | string, ...args: SerializableOrJSHandle[]): Promise; +$$eval(selector: string, pageFunction: (elements: Element[], ...args: unknown[]) => ReturnType | Promise, ...args: SerializableOrJSHandle[]): Promise>; ``` ## Parameters @@ -19,12 +19,12 @@ $$eval(selector: string, pageFunction: EvaluateFn | stri | Parameter | Type | Description | | --- | --- | --- | | selector | string | | -| pageFunction | [EvaluateFn](./puppeteer.evaluatefn.md) \| string | | +| pageFunction | (elements: Element\[\], ...args: unknown\[\]) => ReturnType \| Promise<ReturnType> | | | args | [SerializableOrJSHandle](./puppeteer.serializableorjshandle.md)\[\] | | Returns: -Promise<ReturnType> +Promise<[WrapElementHandle](./puppeteer.wrapelementhandle.md)<ReturnType>> ## Example 1 diff --git a/new-docs/puppeteer.frame.__eval.md b/new-docs/puppeteer.frame.__eval.md index 889af679f5cb0..f441635cca2e8 100644 --- a/new-docs/puppeteer.frame.__eval.md +++ b/new-docs/puppeteer.frame.__eval.md @@ -7,7 +7,7 @@ Signature: ```typescript -$$eval(selector: string, pageFunction: EvaluateFn | string, ...args: SerializableOrJSHandle[]): Promise; +$$eval(selector: string, pageFunction: (elements: Element[], ...args: unknown[]) => ReturnType | Promise, ...args: SerializableOrJSHandle[]): Promise>; ``` ## Parameters @@ -15,10 +15,10 @@ $$eval(selector: string, pageFunction: EvaluateFn | stri | Parameter | Type | Description | | --- | --- | --- | | selector | string | | -| pageFunction | [EvaluateFn](./puppeteer.evaluatefn.md) \| string | | +| pageFunction | (elements: Element\[\], ...args: unknown\[\]) => ReturnType \| Promise<ReturnType> | | | args | [SerializableOrJSHandle](./puppeteer.serializableorjshandle.md)\[\] | | Returns: -Promise<ReturnType> +Promise<[WrapElementHandle](./puppeteer.wrapelementhandle.md)<ReturnType>> diff --git a/new-docs/puppeteer.page.__eval.md b/new-docs/puppeteer.page.__eval.md index 0d9fd00e0f076..f191006aebc1a 100644 --- a/new-docs/puppeteer.page.__eval.md +++ b/new-docs/puppeteer.page.__eval.md @@ -9,7 +9,7 @@ This method runs `Array.from(document.querySelectorAll(selector))` within the pa Signature: ```typescript -$$eval(selector: string, pageFunction: EvaluateFn | string, ...args: SerializableOrJSHandle[]): Promise; +$$eval(selector: string, pageFunction: (elements: Element[], ...args: unknown[]) => ReturnType | Promise, ...args: SerializableOrJSHandle[]): Promise>; ``` ## Parameters @@ -17,14 +17,14 @@ $$eval(selector: string, pageFunction: EvaluateFn | stri | Parameter | Type | Description | | --- | --- | --- | | selector | string | the [selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) to query for | -| pageFunction | [EvaluateFn](./puppeteer.evaluatefn.md) \| string | the function to be evaluated in the page context. Will be passed the result of Array.from(document.querySelectorAll(selector)) as its first argument. | +| pageFunction | (elements: Element\[\], ...args: unknown\[\]) => ReturnType \| Promise<ReturnType> | the function to be evaluated in the page context. Will be passed the result of Array.from(document.querySelectorAll(selector)) as its first argument. | | args | [SerializableOrJSHandle](./puppeteer.serializableorjshandle.md)\[\] | any additional arguments to pass through to pageFunction. | Returns: -Promise<ReturnType> +Promise<[WrapElementHandle](./puppeteer.wrapelementhandle.md)<ReturnType>> -The result of calling `pageFunction`. +The result of calling `pageFunction`. If it returns an element it is wrapped in an [ElementHandle](./puppeteer.elementhandle.md), else the raw value itself is returned. ## Remarks @@ -33,17 +33,40 @@ If `pageFunction` returns a promise `$$eval` will wait for the promise to resolv ## Example 1 -```js +``` +// get the amount of divs on the page const divCount = await page.$$eval('div', divs => divs.length); +// get the text content of all the `.options` elements: +const options = await page.$$eval('div > span.options', options => { + return options.map(option => option.textContent) +}); + ``` +If you are using TypeScript, you may have to provide an explicit type to the first argument of the `pageFunction`. By default it is typed as `Element[]`, but you may need to provide a more specific sub-type: ## Example 2 -```js -const options = await page.$$eval( - 'div > span.options', options => options.map(option => option.textContent)); +``` +// if you don't provide HTMLInputElement here, TS will error +// as `value` is not on `Element` +await page.$$eval('input', (elements: HTMLInputElement[]) => { + return elements.map(e => e.value); +}); + +``` +The compiler should be able to infer the return type from the `pageFunction` you provide. If it is unable to, you can use the generic type to tell the compiler what return type you expect from `$$eval`: + +## Example 3 + + +``` +// The compiler can infer the return type in this case, but if it can't +// or if you want to be more explicit, provide it as the generic type. +const allInputValues = await page.$$eval( + 'input', (elements: HTMLInputElement[]) => elements.map(e => e.textContent) +); ``` diff --git a/src/common/DOMWorld.ts b/src/common/DOMWorld.ts index 1cf644f874e42..f5c075ec33583 100644 --- a/src/common/DOMWorld.ts +++ b/src/common/DOMWorld.ts @@ -25,7 +25,6 @@ import { MouseButton } from './Input'; import { FrameManager, Frame } from './FrameManager'; import { getQueryHandlerAndSelector, QueryHandler } from './QueryHandler'; import { - EvaluateFn, SerializableOrJSHandle, EvaluateHandleFn, WrapElementHandle, @@ -172,11 +171,14 @@ export class DOMWorld { return document.$eval(selector, pageFunction, ...args); } - async $$eval( + async $$eval( selector: string, - pageFunction: EvaluateFn | string, + pageFunction: ( + elements: Element[], + ...args: unknown[] + ) => ReturnType | Promise, ...args: SerializableOrJSHandle[] - ): Promise { + ): Promise> { const document = await this._document(); const value = await document.$$eval( selector, diff --git a/src/common/FrameManager.ts b/src/common/FrameManager.ts index f6c1aa9a1141f..a50ac3b470e96 100644 --- a/src/common/FrameManager.ts +++ b/src/common/FrameManager.ts @@ -30,7 +30,6 @@ import { Page } from './Page'; import { HTTPResponse } from './HTTPResponse'; import Protocol from '../protocol'; import { - EvaluateFn, SerializableOrJSHandle, EvaluateHandleFn, WrapElementHandle, @@ -496,11 +495,14 @@ export class Frame { return this._mainWorld.$eval(selector, pageFunction, ...args); } - async $$eval( + async $$eval( selector: string, - pageFunction: EvaluateFn | string, + pageFunction: ( + elements: Element[], + ...args: unknown[] + ) => ReturnType | Promise, ...args: SerializableOrJSHandle[] - ): Promise { + ): Promise> { return this._mainWorld.$$eval(selector, pageFunction, ...args); } diff --git a/src/common/JSHandle.ts b/src/common/JSHandle.ts index d7b8f66875fca..082d7d761b1e9 100644 --- a/src/common/JSHandle.ts +++ b/src/common/JSHandle.ts @@ -882,11 +882,14 @@ export class ElementHandle< * .toEqual(['Hello!', 'Hi!']); * ``` */ - async $$eval( + async $$eval( selector: string, - pageFunction: EvaluateFn | string, + pageFunction: ( + elements: Element[], + ...args: unknown[] + ) => ReturnType | Promise, ...args: SerializableOrJSHandle[] - ): Promise { + ): Promise> { const defaultHandler = (element: Element, selector: string) => Array.from(element.querySelectorAll(selector)); const { updatedSelector, queryHandler } = getQueryHandlerAndSelector( @@ -898,12 +901,17 @@ export class ElementHandle< queryHandler, updatedSelector ); - const result = await arrayHandle.evaluate<(...args: any[]) => ReturnType>( - pageFunction, - ...args - ); + const result = await arrayHandle.evaluate< + ( + elements: Element[], + ...args: unknown[] + ) => ReturnType | Promise + >(pageFunction, ...args); await arrayHandle.dispose(); - return result; + /* This as exists for the same reason as the `as` in $eval above. + * See the comment there for a ful explanation. + */ + return result as WrapElementHandle; } /** diff --git a/src/common/Page.ts b/src/common/Page.ts index 34c02d5b96135..6e3cc49bf2d59 100644 --- a/src/common/Page.ts +++ b/src/common/Page.ts @@ -43,7 +43,6 @@ import { ConsoleMessage, ConsoleMessageType } from './ConsoleMessage'; import { PuppeteerLifeCycleEvent } from './LifecycleWatcher'; import Protocol from '../protocol'; import { - EvaluateFn, SerializableOrJSHandle, EvaluateHandleFn, WrapElementHandle, @@ -795,31 +794,70 @@ export class Page extends EventEmitter { * resolve and then return its value. * * @example - * ```js + * + * ``` + * // get the amount of divs on the page * const divCount = await page.$$eval('div', divs => divs.length); + * + * // get the text content of all the `.options` elements: + * const options = await page.$$eval('div > span.options', options => { + * return options.map(option => option.textContent) + * }); * ``` * + * If you are using TypeScript, you may have to provide an explicit type to the + * first argument of the `pageFunction`. + * By default it is typed as `Element[]`, but you may need to provide a more + * specific sub-type: + * * @example - * ```js - * const options = await page.$$eval( - * 'div > span.options', options => options.map(option => option.textContent)); + * + * ``` + * // if you don't provide HTMLInputElement here, TS will error + * // as `value` is not on `Element` + * await page.$$eval('input', (elements: HTMLInputElement[]) => { + * return elements.map(e => e.value); + * }); * ``` * - * @param selector - the + * The compiler should be able to infer the return type + * from the `pageFunction` you provide. If it is unable to, you can use the generic + * type to tell the compiler what return type you expect from `$$eval`: + * + * @example + * + * ``` + * // The compiler can infer the return type in this case, but if it can't + * // or if you want to be more explicit, provide it as the generic type. + * const allInputValues = await page.$$eval( + * 'input', (elements: HTMLInputElement[]) => elements.map(e => e.textContent) + * ); + * ``` + * + * @param selector the * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector} * to query for - * @param pageFunction - the function to be evaluated in the page context. - * Will be passed the result of - * `Array.from(document.querySelectorAll(selector))` as its first argument. - * @param args - any additional arguments to pass through to `pageFunction`. + * @param pageFunction the function to be evaluated in the page context. Will + * be passed the result of `Array.from(document.querySelectorAll(selector))` + * as its first argument. + * @param args any additional arguments to pass through to `pageFunction`. * - * @returns The result of calling `pageFunction`. + * @returns The result of calling `pageFunction`. If it returns an element it + * is wrapped in an {@link ElementHandle}, else the raw value itself is + * returned. */ - async $$eval( + async $$eval( selector: string, - pageFunction: EvaluateFn | 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 { + ): Promise> { return this.mainFrame().$$eval(selector, pageFunction, ...args); } diff --git a/test/queryselector.spec.ts b/test/queryselector.spec.ts index db2e36e8ada0e..df484e0b55417 100644 --- a/test/queryselector.spec.ts +++ b/test/queryselector.spec.ts @@ -218,7 +218,7 @@ describe('querySelector', function () { '
' ); const tweet = await page.$('.tweet'); - const content = await tweet.$$eval('.like', (nodes) => + const content = await tweet.$$eval('.like', (nodes: HTMLElement[]) => nodes.map((n) => n.innerText) ); expect(content).toEqual(['100', '10']); @@ -231,7 +231,7 @@ describe('querySelector', function () { '
not-a-child-div
a1-child-div
a2-child-div
'; await page.setContent(htmlContent); const elementHandle = await page.$('#myId'); - const content = await elementHandle.$$eval('.a', (nodes) => + const content = await elementHandle.$$eval('.a', (nodes: HTMLElement[]) => nodes.map((n) => n.innerText) ); expect(content).toEqual(['a1-child-div', 'a2-child-div']);