From 8d8b57da9c9b7b15188027019183016679d17eed Mon Sep 17 00:00:00 2001 From: Brian Schiller Date: Mon, 4 Feb 2019 09:19:25 -0700 Subject: [PATCH 1/2] Preserve type information in Page.evaluate and friends When the EvaluteFn is not a string, it may have type information. We should try and use its type to inform the return type of Page.evaluate, or whichever similar function was used. Some functions (.evaluateHandle, .waitForFunction) return a JSHandle. JSHandles are not parameterized, so we do not know the underlying type. It may be worth changing that in a future PR. --- types/puppeteer/index.d.ts | 27 ++++++++++++++------------- types/puppeteer/puppeteer-tests.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/types/puppeteer/index.d.ts b/types/puppeteer/index.d.ts index 66c73e1f96..caf25ad674 100644 --- a/types/puppeteer/index.d.ts +++ b/types/puppeteer/index.d.ts @@ -490,6 +490,7 @@ export interface EmulateOptions { } export type EvaluateFn = string | ((...args: any[]) => any); +export type EvaluateFnReturnType = T extends ((...args: any[]) => infer R) ? R : any; export type LoadEvent = | "load" @@ -729,10 +730,10 @@ export interface Worker { * If the function passed to the `worker.evaluate` returns a non-Serializable value, * then `worker.evaluate` resolves to `undefined`. */ - evaluate( - pageFunction: (...args: any[]) => T | Promise, + evaluate( + pageFunction: T, ...args: SerializableOrJSHandle[], - ): Promise; + ): Promise>; /** * The only difference between `worker.evaluate` and `worker.evaluateHandle` is @@ -841,10 +842,10 @@ export interface ElementHandle extends JSHandle, Ev /** The class represents a context for JavaScript execution. */ export interface ExecutionContext { - evaluate( - fn: EvaluateFn, + evaluate( + fn: F, ...args: SerializableOrJSHandle[] - ): Promise; + ): Promise>; evaluateHandle( fn: EvaluateFn, ...args: SerializableOrJSHandle[] @@ -1152,10 +1153,10 @@ export interface FrameBase extends Evalable { * @param fn Function to be evaluated in browser context * @param args Arguments to pass to `fn` */ - evaluate( - fn: EvaluateFn, + evaluate( + fn: F, ...args: SerializableOrJSHandle[] - ): Promise; + ): Promise>; /** * Evaluates a function in the page context. @@ -1233,19 +1234,19 @@ export interface FrameBase extends Evalable { * Shortcut for waitForFunction. */ waitFor( - selector: ((...args: any[]) => any) | string, + selector: EvaluateFn, options?: WaitForSelectorOptions, ...args: SerializableOrJSHandle[] - ): Promise; + ): Promise; /** * Allows waiting for various conditions. */ waitForFunction( - fn: string | ((...args: any[]) => any), + fn: EvaluateFn, options?: PageFnOptions, ...args: SerializableOrJSHandle[] - ): Promise; + ): Promise; /** * Wait for the page navigation occur. diff --git a/types/puppeteer/puppeteer-tests.ts b/types/puppeteer/puppeteer-tests.ts index 6c74a780c2..bd6278c371 100644 --- a/types/puppeteer/puppeteer-tests.ts +++ b/types/puppeteer/puppeteer-tests.ts @@ -513,3 +513,31 @@ puppeteer.launch().then(async browser => { }); }); }); + +// evaluate returns type of inner function +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + const s = await page.evaluate(() => document.body.innerHTML); + console.log('body html has length', s.length); +}); + +// even through a double promise. +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + const s = await page.evaluate(() => Promise.resolve(document.body.innerHTML)); + console.log('body html has length', s.length); +}); + +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + const s = await page + .waitForFunction( + (searchStrs: string[]) => searchStrs.find(v => document.body.innerText.includes(v)), + { timeout: 2000 }, + ['once', 'upon', 'a', 'midnight', 'dreary']) + .then(j => j.jsonValue()); + console.log('found in page', s.toLowerCase()); +}); From e7a236a531f8ab15afdf08579615ea8497af1da2 Mon Sep 17 00:00:00 2001 From: Brian Schiller Date: Mon, 4 Feb 2019 11:10:35 -0700 Subject: [PATCH 2/2] add more tests --- types/puppeteer/puppeteer-tests.ts | 45 +++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/types/puppeteer/puppeteer-tests.ts b/types/puppeteer/puppeteer-tests.ts index e8dcff23f0..e5c287b6ee 100644 --- a/types/puppeteer/puppeteer-tests.ts +++ b/types/puppeteer/puppeteer-tests.ts @@ -531,7 +531,7 @@ puppeteer.launch().then(async browser => { }); }); -// evaluate returns type of inner function +// evaluates return type of inner function (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); @@ -547,6 +547,7 @@ puppeteer.launch().then(async browser => { console.log('body html has length', s.length); }); +// JSHandle.jsonValue produces compatible type (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); @@ -575,3 +576,45 @@ puppeteer.launch().then(async browser => { a: '1' }); }); + +// ElementHandles are well-typed +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + const link: puppeteer.JSHandle = await page.evaluateHandle( + () => document.body.querySelector('a') + ); + const linkEl: puppeteer.ElementHandle | null = link.asElement(); + if (linkEl !== null) { + const href = await page.evaluate( + (el: HTMLElement): string | null => el.getAttribute('href'), + linkEl); + console.log('href is', href); + } +}); + +// test $$eval return type +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + + const paragraphContents: string[] = await page.$$eval( + 'p', (ps: Element[]): string[] => ps.map(p => p.textContent || '')); + console.log('pgraph contents', paragraphContents); +}); + +// JSHandle of non-serializable works +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + + const reHandle: puppeteer.JSHandle = await page.evaluateHandle( + () => /\s*bananas?\s*/i, + ); + const numMatchingEls: number = await page.$$eval( + 'p', (els: Element[], re: RegExp) => + els.filter(el => el.textContent && re.test(el.textContent)).length, + reHandle + ); + console.log('there are', numMatchingEls, 'banana paragaphs'); +});