diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index 770da8c509ced..6a1242084837b 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -745,6 +745,9 @@ await bodyHandle.DisposeAsync(); Optional argument to pass to [`param: expression`]. +### option: Frame.evaluate.exposeFunctions = %%-js-evaluate-expose-functions-%% +* since: v1.62 + ## async method: Frame.evaluateHandle * since: v1.8 - returns: <[JSHandle]> @@ -857,6 +860,9 @@ await resultHandle.DisposeAsync(); Optional argument to pass to [`param: expression`]. +### option: Frame.evaluateHandle.exposeFunctions = %%-js-evaluate-expose-functions-%% +* since: v1.62 + ## async method: Frame.fill * since: v1.8 * discouraged: Use locator-based [`method: Locator.fill`] instead. Read more about [locators](../locators.md). diff --git a/docs/src/api/class-jshandle.md b/docs/src/api/class-jshandle.md index 9f38b18447016..5a883bf8892d6 100644 --- a/docs/src/api/class-jshandle.md +++ b/docs/src/api/class-jshandle.md @@ -96,6 +96,9 @@ Assert.AreEqual("10 retweets", await tweetHandle.EvaluateAsync("node => node.inn Optional argument to pass to [`param: expression`]. +### option: JSHandle.evaluate.exposeFunctions = %%-js-evaluate-expose-functions-%% +* since: v1.62 + ## async method: JSHandle.evaluateHandle * since: v1.8 - returns: <[JSHandle]> @@ -123,6 +126,9 @@ See [`method: Page.evaluateHandle`] for more details. Optional argument to pass to [`param: expression`]. +### option: JSHandle.evaluateHandle.exposeFunctions = %%-js-evaluate-expose-functions-%% +* since: v1.62 + ## async method: JSHandle.getProperties * since: v1.8 - returns: <[Map]<[string], [JSHandle]>> diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index f9bc16b0d6f94..c252b318fdd91 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -1123,6 +1123,9 @@ Console.WriteLine(result); // prints "myId text 56" Optional argument to pass to [`param: expression`]. +### option: Locator.evaluate.exposeFunctions = %%-js-evaluate-expose-functions-%% +* since: v1.62 + ### option: Locator.evaluate.timeout * since: v1.14 * langs: python, java, csharp @@ -1222,6 +1225,9 @@ See [`method: Page.evaluateHandle`] for more details. Optional argument to pass to [`param: expression`]. +### option: Locator.evaluateHandle.exposeFunctions = %%-js-evaluate-expose-functions-%% +* since: v1.62 + ### option: Locator.evaluateHandle.timeout * since: v1.14 * langs: python, java, csharp diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 4db5da98e309c..04d0932668163 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -1587,6 +1587,9 @@ await bodyHandle.DisposeAsync(); Optional argument to pass to [`param: expression`]. +### option: Page.evaluate.exposeFunctions = %%-js-evaluate-expose-functions-%% +* since: v1.62 + ## async method: Page.evaluateHandle * since: v1.8 - returns: <[JSHandle]> @@ -1696,6 +1699,9 @@ await resultHandle.DisposeAsync(); Optional argument to pass to [`param: expression`]. +### option: Page.evaluateHandle.exposeFunctions = %%-js-evaluate-expose-functions-%% +* since: v1.62 + ## async method: Page.exposeBinding * since: v1.8 - returns: <[Disposable]> diff --git a/docs/src/api/params.md b/docs/src/api/params.md index ce5c909e6b9c1..aca66190bbe45 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -584,6 +584,12 @@ to a function, the function is automatically invoked. Function to be evaluated in the page context. +## js-evaluate-expose-functions +* langs: js +- `exposeFunctions` <[boolean]> + +When set to `true`, functions passed inside [`param: arg`] are exposed in the page and can be called from the page function. Calling one returns a [Promise] of its result. The page-side functions are scoped to the execution context they were passed to and disappear when the page navigates. Defaults to `false`, in which case functions are not serializable and passing one throws an error, as before. + ## js-evalonselector-pagefunction * langs: js - `pageFunction` <[function]\([Element]\)|[string]> diff --git a/packages/injected/src/bindingsController.ts b/packages/injected/src/bindingsController.ts index 7f58b482a5f00..5066e75ee4bcb 100644 --- a/packages/injected/src/bindingsController.ts +++ b/packages/injected/src/bindingsController.ts @@ -40,28 +40,32 @@ export class BindingsController { this._globalBindingName = globalBindingName; } - addBinding(bindingName: string) { + addBinding(bindingName: string, noGlobal?: boolean) { const data: BindingData = { callbacks: new Map(), lastSeq: 0, removed: false, }; this._bindings.set(bindingName, data); - (this._global as any)[bindingName] = (...args: any[]) => { - if (data.removed) - throw new Error(`binding "${bindingName}" has been removed`); - const seq = ++data.lastSeq; - const promise = new Promise((resolve, reject) => data.callbacks.set(seq, { resolve, reject })); - const serializedArgs = []; - for (let i = 0; i < args.length; i++) { - serializedArgs[i] = serializeAsCallArgument(args[i], v => { - return { fallThrough: v }; - }); - } - const payload: BindingPayload = { name: bindingName, seq, serializedArgs }; - (this._global as any)[this._globalBindingName](JSON.stringify(payload)); - return promise; - }; + if (!noGlobal) + (this._global as any)[bindingName] = (...args: any[]) => this.callBinding(bindingName, ...args); + } + + callBinding(bindingName: string, ...args: any[]): Promise { + const data = this._bindings.get(bindingName); + if (!data || data.removed) + throw new Error(`binding "${bindingName}" has been removed`); + const seq = ++data.lastSeq; + const promise = new Promise((resolve, reject) => data.callbacks.set(seq, { resolve, reject })); + const serializedArgs = []; + for (let i = 0; i < args.length; i++) { + serializedArgs[i] = serializeAsCallArgument(args[i], v => { + return { fallThrough: v }; + }); + } + const payload: BindingPayload = { name: bindingName, seq, serializedArgs }; + (this._global as any)[this._globalBindingName](JSON.stringify(payload)); + return promise; } removeBinding(bindingName: string) { diff --git a/packages/isomorphic/utilityScriptSerializers.ts b/packages/isomorphic/utilityScriptSerializers.ts index 6be0773adb097..012fa00e14e61 100644 --- a/packages/isomorphic/utilityScriptSerializers.ts +++ b/packages/isomorphic/utilityScriptSerializers.ts @@ -16,6 +16,13 @@ type TypedArrayKind = 'i8' | 'ui8' | 'ui8c' | 'i16' | 'ui16' | 'i32' | 'ui32' | 'f32' | 'f64' | 'bi64' | 'bui64'; +// Name prefix of the page bindings backing the functions passed to evaluate() +// as arguments. Only functions carrying this prefix serialize as { fn }, +// arbitrary functions are dropped as before. +export const kFunctionBindingPrefix = '__pw_fn_'; + +export const kBindingsControllerProperty = '__playwright__binding__controller__'; + export type SerializedValue = undefined | boolean | number | string | { v: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0' } | @@ -28,6 +35,7 @@ export type SerializedValue = { o: { k: string, v: SerializedValue }[], id: number } | { ref: number } | { h: number } | + { fn: string } | { ta: { b: string, k: TypedArrayKind } } | { ab: { b: string } }; @@ -177,6 +185,11 @@ export function parseEvaluationResultValue(value: SerializedValue, handles: any[ } if ('h' in value) return handles[value.h]; + if ('fn' in value) { + const name = value.fn; + // eslint-disable-next-line no-restricted-globals + return (...args: any[]) => (globalThis as any)[kBindingsControllerProperty].callBinding(name, ...args); + } if ('ta' in value) return base64ToTypedArray(value.ta.b, typedArrayConstructors[value.ta.k]); if ('ab' in value) @@ -300,4 +313,7 @@ function innerSerialize(value: any, handleSerializer: (value: any) => HandleOrVa return { o, id }; } + + if (typeof value === 'function' && value.name.startsWith(kFunctionBindingPrefix)) + return { fn: value.name }; } diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 3ab60c474dfe3..d24168c61f007 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -87,14 +87,15 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression) invocation. * * If the function passed to the - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a [Promise], - * then [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) would wait for - * the promise to resolve and return its value. + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a + * [Promise], then + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) would wait + * for the promise to resolve and return its value. * * If the function passed to the - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a * non-[Serializable] value, then - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) resolves to + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) resolves to * `undefined`. Playwright also supports transferring some additional values that are not serializable by `JSON`: * `-0`, `NaN`, `Infinity`, `-Infinity`. * @@ -118,7 +119,7 @@ export interface Page { * ``` * * [ElementHandle](https://playwright.dev/docs/api/class-elementhandle) instances can be passed as an argument to the - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate): + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate): * * ```js * const bodyHandle = await page.evaluateHandle('document.body'); @@ -131,21 +132,23 @@ export interface Page { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression). + * @param options */ - evaluate(pageFunction: PageFunction, arg: Arg): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise; /** * Returns the value of the * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression) invocation. * * If the function passed to the - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a [Promise], - * then [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) would wait for - * the promise to resolve and return its value. + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a + * [Promise], then + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) would wait + * for the promise to resolve and return its value. * * If the function passed to the - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a * non-[Serializable] value, then - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) resolves to + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) resolves to * `undefined`. Playwright also supports transferring some additional values that are not serializable by `JSON`: * `-0`, `NaN`, `Infinity`, `-Infinity`. * @@ -169,7 +172,7 @@ export interface Page { * ``` * * [ElementHandle](https://playwright.dev/docs/api/class-elementhandle) instances can be passed as an argument to the - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate): + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate): * * ```js * const bodyHandle = await page.evaluateHandle('document.body'); @@ -182,8 +185,9 @@ export interface Page { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression). + * @param options */ - evaluate(pageFunction: PageFunction, arg?: any): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise; /** * Returns the value of the @@ -191,16 +195,17 @@ export interface Page { * [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * The only difference between - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) and - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) is that - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) returns - * [JSHandle](https://playwright.dev/docs/api/class-jshandle). + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) and + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * is that + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * returns [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * If the function passed to the - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) returns - * a [Promise], then - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) would - * wait for the promise to resolve and return its value. + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * returns a [Promise], then + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * would wait for the promise to resolve and return its value. * * **Usage** * @@ -216,7 +221,7 @@ export interface Page { * ``` * * [JSHandle](https://playwright.dev/docs/api/class-jshandle) instances can be passed as an argument to the - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle): + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle): * * ```js * const aHandle = await page.evaluateHandle(() => document.body); @@ -228,24 +233,26 @@ export interface Page { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression). + * @param options */ - evaluateHandle(pageFunction: PageFunction, arg: Arg): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; /** * Returns the value of the * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression) invocation as a * [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * The only difference between - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) and - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) is that - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) returns - * [JSHandle](https://playwright.dev/docs/api/class-jshandle). + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) and + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * is that + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * returns [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * If the function passed to the - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) returns - * a [Promise], then - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) would - * wait for the promise to resolve and return its value. + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * returns a [Promise], then + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * would wait for the promise to resolve and return its value. * * **Usage** * @@ -261,7 +268,7 @@ export interface Page { * ``` * * [JSHandle](https://playwright.dev/docs/api/class-jshandle) instances can be passed as an argument to the - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle): + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle): * * ```js * const aHandle = await page.evaluateHandle(() => document.body); @@ -273,8 +280,9 @@ export interface Page { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression). + * @param options */ - evaluateHandle(pageFunction: PageFunction, arg?: any): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; /** * Adds a script which would be evaluated in one of the following scenarios: @@ -5786,14 +5794,15 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). * * If the function passed to the - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns a - * [Promise], then [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) - * would wait for the promise to resolve and return its value. + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns + * a [Promise], then + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) would + * wait for the promise to resolve and return its value. * * If the function passed to the - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns a - * non-[Serializable] value, then - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns + * a non-[Serializable] value, then + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns * `undefined`. Playwright also supports transferring some additional values that are not serializable by `JSON`: * `-0`, `NaN`, `Infinity`, `-Infinity`. * @@ -5813,7 +5822,7 @@ export interface Frame { * ``` * * [ElementHandle](https://playwright.dev/docs/api/class-elementhandle) instances can be passed as an argument to the - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate): + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate): * * ```js * const bodyHandle = await frame.evaluateHandle('document.body'); @@ -5826,21 +5835,23 @@ export interface Frame { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). + * @param options */ - evaluate(pageFunction: PageFunction, arg: Arg): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). * * If the function passed to the - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns a - * [Promise], then [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) - * would wait for the promise to resolve and return its value. + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns + * a [Promise], then + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) would + * wait for the promise to resolve and return its value. * * If the function passed to the - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns a - * non-[Serializable] value, then - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns + * a non-[Serializable] value, then + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns * `undefined`. Playwright also supports transferring some additional values that are not serializable by `JSON`: * `-0`, `NaN`, `Infinity`, `-Infinity`. * @@ -5860,7 +5871,7 @@ export interface Frame { * ``` * * [ElementHandle](https://playwright.dev/docs/api/class-elementhandle) instances can be passed as an argument to the - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate): + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate): * * ```js * const bodyHandle = await frame.evaluateHandle('document.body'); @@ -5873,8 +5884,9 @@ export interface Frame { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). + * @param options */ - evaluate(pageFunction: PageFunction, arg?: any): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise; /** * Returns the return value of @@ -5882,15 +5894,16 @@ export interface Frame { * [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * The only difference between - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) and - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) is - * that [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) and + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) + * is that + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) * returns [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * If the function, passed to the - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle), + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle), * returns a [Promise], then - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) * would wait for the promise to resolve and return its value. * * **Usage** @@ -5907,7 +5920,7 @@ export interface Frame { * ``` * * [JSHandle](https://playwright.dev/docs/api/class-jshandle) instances can be passed as an argument to the - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle): + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle): * * ```js * const aHandle = await frame.evaluateHandle(() => document.body); @@ -5921,23 +5934,25 @@ export interface Frame { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression). + * @param options */ - evaluateHandle(pageFunction: PageFunction, arg: Arg): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression) as a * [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * The only difference between - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) and - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) is - * that [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) and + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) + * is that + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) * returns [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * If the function, passed to the - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle), + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle), * returns a [Promise], then - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) * would wait for the promise to resolve and return its value. * * **Usage** @@ -5954,7 +5969,7 @@ export interface Frame { * ``` * * [JSHandle](https://playwright.dev/docs/api/class-jshandle) instances can be passed as an argument to the - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle): + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle): * * ```js * const aHandle = await frame.evaluateHandle(() => document.body); @@ -5968,8 +5983,9 @@ export interface Frame { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression). + * @param options */ - evaluateHandle(pageFunction: PageFunction, arg?: any): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; /** * **NOTE** Use locator-based [frame.locator(selector[, options])](https://playwright.dev/docs/api/class-frame#frame-locator) @@ -11726,7 +11742,8 @@ export interface Worker { /** * JSHandle represents an in-page JavaScript object. JSHandles can be created with the - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) method. + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * method. * * ```js * const windowHandle = await page.evaluateHandle(() => window); @@ -11739,8 +11756,8 @@ export interface Worker { * * JSHandle instances can be used as an argument in * [page.$eval(selector, pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-eval-on-selector), - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) and - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) and + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) * methods. */ export interface JSHandle { @@ -11764,8 +11781,9 @@ export interface JSHandle { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). + * @param options */ - evaluate(pageFunction: PageFunctionOn, arg: Arg): Promise; + evaluate(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean }): Promise; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). @@ -11786,8 +11804,9 @@ export interface JSHandle { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). + * @param options */ - evaluate(pageFunction: PageFunctionOn, arg?: any): Promise; + evaluate(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean }): Promise; /** * Returns the return value of @@ -11803,13 +11822,15 @@ export interface JSHandle { * If the function passed to the `jsHandle.evaluateHandle` returns a [Promise], then `jsHandle.evaluateHandle` would * wait for the promise to resolve and return its value. * - * See [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) for - * more details. + * See + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * for more details. * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression). + * @param options */ - evaluateHandle(pageFunction: PageFunctionOn, arg: Arg): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression) @@ -11824,13 +11845,15 @@ export interface JSHandle { * If the function passed to the `jsHandle.evaluateHandle` returns a [Promise], then `jsHandle.evaluateHandle` would * wait for the promise to resolve and return its value. * - * See [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) for - * more details. + * See + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * for more details. * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression). + * @param options */ - evaluateHandle(pageFunction: PageFunctionOn, arg?: any): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; /** * Returns a JSON representation of the object. If the object has a `toJSON` function, it **will not be called**. @@ -11895,7 +11918,8 @@ export interface JSHandle { * * ElementHandle instances can be used as an argument in * [page.$eval(selector, pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-eval-on-selector) - * and [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) methods. + * and [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) + * methods. * * The difference between the [Locator](https://playwright.dev/docs/api/class-locator) and ElementHandle is that the * ElementHandle points to a particular element, while [Locator](https://playwright.dev/docs/api/class-locator) @@ -13837,7 +13861,7 @@ export interface Locator { * [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal }): Promise; + evaluate(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean }): Promise; /** * Execute JavaScript code in the page, taking the matching element as an argument, and return a * [JSHandle](https://playwright.dev/docs/api/class-jshandle) with the result. @@ -13863,14 +13887,15 @@ export interface Locator { * If [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle-option-expression) throws * or rejects, this method throws. * - * See [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) for - * more details. + * See + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * for more details. * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean }): Promise>; /** * Execute JavaScript code in the page, taking all matching elements as an argument. * diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index e2f2856b53558..f6ed9d166f192 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -116,6 +116,16 @@ export abstract class ChannelOwner | undefined { + let parent: ChannelOwner | undefined = this._parent; + while (parent) { + if (parent._type === type) + return parent; + parent = parent._parent; + } + return undefined; + } + _dispose(reason: 'gc' | undefined) { // Clean up from parent and connection. if (this._parent) diff --git a/packages/playwright-core/src/client/channels.d.ts b/packages/playwright-core/src/client/channels.d.ts index e24253c85e32f..ee239a9c51efc 100644 --- a/packages/playwright-core/src/client/channels.d.ts +++ b/packages/playwright-core/src/client/channels.d.ts @@ -4048,9 +4048,10 @@ export type PageEmulateMediaOptions = { export type PageEmulateMediaResult = void; export type PageExposeBindingParams = { name: string, + noGlobal?: boolean, }; export type PageExposeBindingOptions = { - + noGlobal?: boolean, }; export type PageExposeBindingResult = { disposable: DisposableChannel, diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index ad25c267dd8e3..2b315dd685642 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -27,13 +27,14 @@ import { addSourceUrlToScript } from './clientHelper'; import { ElementHandle, convertInputFiles, convertSelectOptionValues } from './elementHandle'; import { AbortError, PlaywrightError } from './errors'; import { Events } from './events'; -import { JSHandle, assertMaxArguments, parseResult, serializeArgument } from './jsHandle'; +import { JSHandle, assertEvaluateOptions, assertMaxArguments, parseResult, serializeArgument, serializeArgumentWithCallbacks } from './jsHandle'; import { FrameLocator, Locator, testIdAttributeName } from './locator'; import * as network from './network'; import { kLifecycleEvents } from './types'; import { Waiter } from './waiter'; import { TimeoutSettings, kNoTimeout } from './timeoutSettings'; +import type { EvaluateOptions } from './jsHandle'; import type { LocatorOptions } from './locator'; import type { Page } from './page'; import type { DropPayload, FilePayload, LifecycleEvent, SelectOption, SelectOptionOptions, StrictOptions, TimeoutOptions, WaitForFunctionOptions } from './types'; @@ -95,6 +96,10 @@ export class Frame extends ChannelOwner implements api.Fr this._channel.on('navigated', event => { this._url = event.url; this._name = event.name; + // Main frame navigation destroys all execution contexts in the page, + // erase the bindings backing the functions passed to evaluate(). + if (!this._parentFrame && event.newDocument && this._page) + this._page._eraseEvaluateCallbacks(); this._eventEmitter.emit('navigated', event); if (!event.error && this._page) { this._page.emit(Events.Page.FrameNavigated, this); @@ -201,15 +206,19 @@ export class Frame extends ChannelOwner implements api.Fr return ElementHandle.from((await this._channel.frameElement({}, kNoTimeout)).element); } - async evaluateHandle(pageFunction: structs.PageFunction, arg?: Arg): Promise> { - assertMaxArguments(arguments.length, 2); - const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }, kNoTimeout); + async evaluateHandle(pageFunction: structs.PageFunction, arg?: Arg, options?: EvaluateOptions): Promise> { + assertMaxArguments(arguments.length, 3); + assertEvaluateOptions(options); + const serializedArg = options?.exposeFunctions ? await serializeArgumentWithCallbacks(this, this._page, arg) : serializeArgument(arg); + const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg }, kNoTimeout); return JSHandle.from(result.handle) as any as structs.SmartHandle; } - async evaluate(pageFunction: structs.PageFunction, arg?: Arg): Promise { - assertMaxArguments(arguments.length, 2); - const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }, kNoTimeout); + async evaluate(pageFunction: structs.PageFunction, arg?: Arg, options?: EvaluateOptions): Promise { + assertMaxArguments(arguments.length, 3); + assertEvaluateOptions(options); + const serializedArg = options?.exposeFunctions ? await serializeArgumentWithCallbacks(this, this._page, arg) : serializeArgument(arg); + const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg }, kNoTimeout); return parseResult(result.value); } diff --git a/packages/playwright-core/src/client/jsHandle.ts b/packages/playwright-core/src/client/jsHandle.ts index aa9841c2e4075..c0f20cc97965e 100644 --- a/packages/playwright-core/src/client/jsHandle.ts +++ b/packages/playwright-core/src/client/jsHandle.ts @@ -14,7 +14,9 @@ * limitations under the License. */ +import { kFunctionBindingPrefix } from '@isomorphic/utilityScriptSerializers'; import { parseSerializedValue, serializeValue } from '@protocol/serializers'; +import { createGuid } from '@utils/crypto'; import { ChannelOwner } from './channelOwner'; import { isTargetClosedError } from './errors'; import { kNoTimeout } from './timeoutSettings'; @@ -22,6 +24,7 @@ import { kNoTimeout } from './timeoutSettings'; import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; import type * as channels from './channels'; +import type { Page } from './page'; export class JSHandle extends ChannelOwner implements api.JSHandle { @@ -37,13 +40,17 @@ export class JSHandle extends ChannelOwner im this._channel.on('previewUpdated', ({ preview }) => this._preview = preview); } - async evaluate(pageFunction: structs.PageFunctionOn, arg?: Arg): Promise { - const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }, kNoTimeout); + async evaluate(pageFunction: structs.PageFunctionOn, arg?: Arg, options?: EvaluateOptions): Promise { + assertEvaluateOptions(options); + const serializedArg = options?.exposeFunctions ? await serializeArgumentWithCallbacks(this, this._parentOfType('Page') as Page | undefined, arg) : serializeArgument(arg); + const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg }, kNoTimeout); return parseResult(result.value); } - async evaluateHandle(pageFunction: structs.PageFunctionOn, arg?: Arg): Promise> { - const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializeArgument(arg) }, kNoTimeout); + async evaluateHandle(pageFunction: structs.PageFunctionOn, arg?: Arg, options?: EvaluateOptions): Promise> { + assertEvaluateOptions(options); + const serializedArg = options?.exposeFunctions ? await serializeArgumentWithCallbacks(this, this._parentOfType('Page') as Page | undefined, arg) : serializeArgument(arg); + const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg }, kNoTimeout); return JSHandle.from(result.handle) as any as structs.SmartHandle; } @@ -88,7 +95,7 @@ export class JSHandle extends ChannelOwner im // This function takes care of converting all JSHandles to their channels, // so that generic channel serializer converts them to guids. -export function serializeArgument(arg: any): channels.SerializedArgument { +export function serializeArgument(arg: any, registerCallback?: (callback: Function) => string): channels.SerializedArgument { const handles: channels.Channel[] = []; const pushHandle = (channel: channels.Channel): number => { handles.push(channel); @@ -97,11 +104,30 @@ export function serializeArgument(arg: any): channels.SerializedArgument { const value = serializeValue(arg, value => { if (value instanceof JSHandle) return { h: pushHandle(value._channel) }; + if (typeof value === 'function' && registerCallback) + return { fn: registerCallback(value as Function) }; return { fallThrough: value }; }); return { value, handles }; } +export type EvaluateOptions = { exposeFunctions?: boolean }; + +export async function serializeArgumentWithCallbacks(owner: ChannelOwner, page: Page | undefined, arg: any): Promise { + return await owner._wrapApiCall(async () => { + const exposePromises: Promise[] = []; + const serialized = serializeArgument(arg, callback => { + if (!page) + throw new Error('Passing a function is not supported as an argument here'); + const name = kFunctionBindingPrefix + createGuid(); + exposePromises.push(page._exposeEvaluateCallback(name, callback)); + return name; + }); + await Promise.all(exposePromises); + return serialized; + }, { internal: true }); +} + export function parseResult(value: channels.SerializedValue): any { return parseSerializedValue(value, undefined); } @@ -110,3 +136,8 @@ export function assertMaxArguments(count: number, max: number): asserts count { if (count > max) throw new Error('Too many arguments. If you need to pass more than 1 argument to the function wrap them in an object.'); } + +export function assertEvaluateOptions(options: any) { + if (options !== undefined && (typeof options !== 'object' || options === null || Array.isArray(options))) + throw new Error('Too many arguments. If you need to pass more than 1 argument to the function wrap them in an object.'); +} diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 342799550bc40..078234fd9f110 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -27,6 +27,7 @@ import { DisposableStub } from './disposable'; import { kNoTimeout } from './timeoutSettings'; import type { ExpectResult, Frame } from './frame'; +import type { EvaluateOptions } from './jsHandle'; import type { DropPayload, FilePayload, FrameExpectParams, Rect, SelectOption, SelectOptionOptions, TimeoutOptions } from './types'; import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; @@ -134,16 +135,16 @@ export class Locator implements api.Locator { await this._frame._drop(this._selector, payload, { strict: true, ...options }); } - async evaluate(pageFunction: structs.PageFunctionOn, arg?: Arg, options?: TimeoutOptions): Promise { - return await this._withElement(h => h.evaluate(pageFunction, arg), { title: 'Evaluate', timeout: options?.timeout, signal: options?.signal }); + async evaluate(pageFunction: structs.PageFunctionOn, arg?: Arg, options?: TimeoutOptions & EvaluateOptions): Promise { + return await this._withElement(h => h.evaluate(pageFunction, arg, options), { title: 'Evaluate', timeout: options?.timeout, signal: options?.signal }); } async evaluateAll(pageFunction: structs.PageFunctionOn, arg?: Arg): Promise { return await this._frame.$$eval(this._selector, pageFunction, arg); } - async evaluateHandle(pageFunction: structs.PageFunctionOn, arg?: Arg, options?: TimeoutOptions): Promise> { - return await this._withElement(h => h.evaluateHandle(pageFunction, arg), { title: 'Evaluate', timeout: options?.timeout, signal: options?.signal }); + async evaluateHandle(pageFunction: structs.PageFunctionOn, arg?: Arg, options?: TimeoutOptions & EvaluateOptions): Promise> { + return await this._withElement(h => h.evaluateHandle(pageFunction, arg, options), { title: 'Evaluate', timeout: options?.timeout, signal: options?.signal }); } async fill(value: string, options: channels.ElementHandleFillOptions & TimeoutOptions = {}): Promise { diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index f7f2ba405427b..90a199a17f76d 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -50,6 +50,7 @@ import { TimeoutSettings, kNoTimeout } from './timeoutSettings'; import { mkdirIfNeeded } from './fileUtils'; import { ConsoleMessage } from './consoleMessage'; import type { BrowserContext } from './browserContext'; +import type { EvaluateOptions } from './jsHandle'; import type { Clock } from './clock'; import type { APIRequestContext } from './fetch'; import type { WaitForNavigationOptions } from './frame'; @@ -117,6 +118,7 @@ export class Page extends ChannelOwner implements api.Page private _harRouters: HarRouter[] = []; private _locatorHandlers = new Map any, times: number | undefined }>(); + private _evaluateCallbacks: { name: string, disposable: DisposableObject }[] = []; static from(page: channels.PageChannel): Page { return (page as any)._object; @@ -334,9 +336,9 @@ export class Page extends ChannelOwner implements api.Page return await this._mainFrame.dispatchEvent(selector, type, eventInit, options); } - async evaluateHandle(pageFunction: structs.PageFunction, arg?: Arg): Promise> { - assertMaxArguments(arguments.length, 2); - return await this._mainFrame.evaluateHandle(pageFunction, arg); + async evaluateHandle(pageFunction: structs.PageFunction, arg?: Arg, options?: EvaluateOptions): Promise> { + assertMaxArguments(arguments.length, 3); + return await this._mainFrame.evaluateHandle(pageFunction, arg, options); } async $eval(selector: string, pageFunction: structs.PageFunctionOn, arg?: Arg): Promise { @@ -374,6 +376,20 @@ export class Page extends ChannelOwner implements api.Page return DisposableObject.from(result.disposable); } + async _exposeEvaluateCallback(name: string, callback: Function) { + this._bindings.set(name, (source, ...args) => callback(...args)); + const result = await this._channel.exposeBinding({ name, noGlobal: true }, kNoTimeout); + this._evaluateCallbacks.push({ name, disposable: DisposableObject.from(result.disposable) }); + } + + _eraseEvaluateCallbacks() { + for (const { name, disposable } of this._evaluateCallbacks) { + this._bindings.delete(name); + disposable.dispose().catch(() => {}); + } + this._evaluateCallbacks = []; + } + async setExtraHTTPHeaders(headers: Headers) { validateHeaders(headers); await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) }, kNoTimeout); @@ -528,9 +544,9 @@ export class Page extends ChannelOwner implements api.Page return this._viewportSize || null; } - async evaluate(pageFunction: structs.PageFunction, arg?: Arg): Promise { - assertMaxArguments(arguments.length, 2); - return await this._mainFrame.evaluate(pageFunction, arg); + async evaluate(pageFunction: structs.PageFunction, arg?: Arg, options?: EvaluateOptions): Promise { + assertMaxArguments(arguments.length, 3); + return await this._mainFrame.evaluate(pageFunction, arg, options); } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { diff --git a/packages/playwright-core/src/server/channels.d.ts b/packages/playwright-core/src/server/channels.d.ts index 62c59f0116e2e..b66eb1753d7ef 100644 --- a/packages/playwright-core/src/server/channels.d.ts +++ b/packages/playwright-core/src/server/channels.d.ts @@ -4049,9 +4049,10 @@ export type PageEmulateMediaOptions = { export type PageEmulateMediaResult = void; export type PageExposeBindingParams = { name: string, + noGlobal?: boolean, }; export type PageExposeBindingOptions = { - + noGlobal?: boolean, }; export type PageExposeBindingResult = { disposable: DisposableChannel, diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index b9e4ba875092a..ede502ef9bbc9 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -143,7 +143,7 @@ export class PageDispatcher extends Dispatcher { return this.frameManager.frames(); } - async exposeBinding(progress: Progress, name: string, playwrightBinding: frames.FunctionWithSource): Promise { + async exposeBinding(progress: Progress, name: string, playwrightBinding: frames.FunctionWithSource, noGlobal?: boolean): Promise { if (this._pageBindings.has(name)) throw new Error(`Function "${name}" has been already registered`); if (this.browserContext._pageBindings.has(name)) throw new Error(`Function "${name}" has been already registered in the browser context`); await progress.race(this.browserContext.exposePlaywrightBindingIfNeeded()); - const binding = new PageBinding(this, name, playwrightBinding); + const binding = new PageBinding(this, name, playwrightBinding, noGlobal); this._pageBindings.set(name, binding); try { await progress.race(this.delegate.addInitScript(binding.initScript)); @@ -1046,7 +1046,6 @@ export class Worker extends SdkObject { } export class PageBinding extends DisposableObject { - private static kController = '__playwright__binding__controller__'; static kBindingName = '__playwright__binding__'; static createInitScript(browserContext: BrowserContext): InitScript { @@ -1054,7 +1053,7 @@ export class PageBinding extends DisposableObject { (() => { const module = {}; ${rawBindingsControllerSource.source} - const property = '${PageBinding.kController}'; + const property = '${kBindingsControllerProperty}'; if (!globalThis[property]) globalThis[property] = new (module.exports.BindingsController())(globalThis, '${PageBinding.kBindingName}'); })(); @@ -1067,12 +1066,12 @@ export class PageBinding extends DisposableObject { readonly cleanupScript: string; forClient?: unknown; - constructor(parent: BrowserContext | Page, name: string, playwrightFunction: frames.FunctionWithSource) { + constructor(parent: BrowserContext | Page, name: string, playwrightFunction: frames.FunctionWithSource, noGlobal?: boolean) { super(parent); this.name = name; this.playwrightFunction = playwrightFunction; - this.initScript = new InitScript(parent, `globalThis['${PageBinding.kController}'].addBinding(${JSON.stringify(name)})`); - this.cleanupScript = `globalThis['${PageBinding.kController}'].removeBinding(${JSON.stringify(name)})`; + this.initScript = new InitScript(parent, `globalThis['${kBindingsControllerProperty}'].addBinding(${JSON.stringify(name)}, ${!!noGlobal})`); + this.cleanupScript = `globalThis['${kBindingsControllerProperty}'].removeBinding(${JSON.stringify(name)})`; } static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) { @@ -1086,9 +1085,9 @@ export class PageBinding extends DisposableObject { throw new Error(`serializedArgs is not an array. This can happen when Array.prototype.toJSON is defined incorrectly`); const args = serializedArgs.map(a => parseEvaluationResultValue(a)); const result = await binding.playwrightFunction({ frame: context.frame, page, context: page.browserContext }, ...args); - context.evaluateExpressionHandle(`arg => globalThis['${PageBinding.kController}'].deliverBindingResult(arg)`, { isFunction: true }, { name, seq, result }).catch(e => debugLogger.log('error', e)); + context.evaluateExpressionHandle(`arg => globalThis['${kBindingsControllerProperty}'].deliverBindingResult(arg)`, { isFunction: true }, { name, seq, result }).catch(e => debugLogger.log('error', e)); } catch (error) { - context.evaluateExpressionHandle(`arg => globalThis['${PageBinding.kController}'].deliverBindingResult(arg)`, { isFunction: true }, { name, seq, error }).catch(e => debugLogger.log('error', e)); + context.evaluateExpressionHandle(`arg => globalThis['${kBindingsControllerProperty}'].deliverBindingResult(arg)`, { isFunction: true }, { name, seq, error }).catch(e => debugLogger.log('error', e)); } } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 3ab60c474dfe3..d24168c61f007 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -87,14 +87,15 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression) invocation. * * If the function passed to the - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a [Promise], - * then [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) would wait for - * the promise to resolve and return its value. + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a + * [Promise], then + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) would wait + * for the promise to resolve and return its value. * * If the function passed to the - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a * non-[Serializable] value, then - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) resolves to + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) resolves to * `undefined`. Playwright also supports transferring some additional values that are not serializable by `JSON`: * `-0`, `NaN`, `Infinity`, `-Infinity`. * @@ -118,7 +119,7 @@ export interface Page { * ``` * * [ElementHandle](https://playwright.dev/docs/api/class-elementhandle) instances can be passed as an argument to the - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate): + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate): * * ```js * const bodyHandle = await page.evaluateHandle('document.body'); @@ -131,21 +132,23 @@ export interface Page { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression). + * @param options */ - evaluate(pageFunction: PageFunction, arg: Arg): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise; /** * Returns the value of the * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression) invocation. * * If the function passed to the - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a [Promise], - * then [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) would wait for - * the promise to resolve and return its value. + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a + * [Promise], then + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) would wait + * for the promise to resolve and return its value. * * If the function passed to the - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) returns a * non-[Serializable] value, then - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) resolves to + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) resolves to * `undefined`. Playwright also supports transferring some additional values that are not serializable by `JSON`: * `-0`, `NaN`, `Infinity`, `-Infinity`. * @@ -169,7 +172,7 @@ export interface Page { * ``` * * [ElementHandle](https://playwright.dev/docs/api/class-elementhandle) instances can be passed as an argument to the - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate): + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate): * * ```js * const bodyHandle = await page.evaluateHandle('document.body'); @@ -182,8 +185,9 @@ export interface Page { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression). + * @param options */ - evaluate(pageFunction: PageFunction, arg?: any): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise; /** * Returns the value of the @@ -191,16 +195,17 @@ export interface Page { * [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * The only difference between - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) and - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) is that - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) returns - * [JSHandle](https://playwright.dev/docs/api/class-jshandle). + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) and + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * is that + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * returns [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * If the function passed to the - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) returns - * a [Promise], then - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) would - * wait for the promise to resolve and return its value. + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * returns a [Promise], then + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * would wait for the promise to resolve and return its value. * * **Usage** * @@ -216,7 +221,7 @@ export interface Page { * ``` * * [JSHandle](https://playwright.dev/docs/api/class-jshandle) instances can be passed as an argument to the - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle): + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle): * * ```js * const aHandle = await page.evaluateHandle(() => document.body); @@ -228,24 +233,26 @@ export interface Page { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression). + * @param options */ - evaluateHandle(pageFunction: PageFunction, arg: Arg): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; /** * Returns the value of the * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression) invocation as a * [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * The only difference between - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) and - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) is that - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) returns - * [JSHandle](https://playwright.dev/docs/api/class-jshandle). + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) and + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * is that + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * returns [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * If the function passed to the - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) returns - * a [Promise], then - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) would - * wait for the promise to resolve and return its value. + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * returns a [Promise], then + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * would wait for the promise to resolve and return its value. * * **Usage** * @@ -261,7 +268,7 @@ export interface Page { * ``` * * [JSHandle](https://playwright.dev/docs/api/class-jshandle) instances can be passed as an argument to the - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle): + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle): * * ```js * const aHandle = await page.evaluateHandle(() => document.body); @@ -273,8 +280,9 @@ export interface Page { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression). + * @param options */ - evaluateHandle(pageFunction: PageFunction, arg?: any): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; /** * Adds a script which would be evaluated in one of the following scenarios: @@ -5786,14 +5794,15 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). * * If the function passed to the - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns a - * [Promise], then [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) - * would wait for the promise to resolve and return its value. + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns + * a [Promise], then + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) would + * wait for the promise to resolve and return its value. * * If the function passed to the - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns a - * non-[Serializable] value, then - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns + * a non-[Serializable] value, then + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns * `undefined`. Playwright also supports transferring some additional values that are not serializable by `JSON`: * `-0`, `NaN`, `Infinity`, `-Infinity`. * @@ -5813,7 +5822,7 @@ export interface Frame { * ``` * * [ElementHandle](https://playwright.dev/docs/api/class-elementhandle) instances can be passed as an argument to the - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate): + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate): * * ```js * const bodyHandle = await frame.evaluateHandle('document.body'); @@ -5826,21 +5835,23 @@ export interface Frame { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). + * @param options */ - evaluate(pageFunction: PageFunction, arg: Arg): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). * * If the function passed to the - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns a - * [Promise], then [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) - * would wait for the promise to resolve and return its value. + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns + * a [Promise], then + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) would + * wait for the promise to resolve and return its value. * * If the function passed to the - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns a - * non-[Serializable] value, then - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns + * a non-[Serializable] value, then + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) returns * `undefined`. Playwright also supports transferring some additional values that are not serializable by `JSON`: * `-0`, `NaN`, `Infinity`, `-Infinity`. * @@ -5860,7 +5871,7 @@ export interface Frame { * ``` * * [ElementHandle](https://playwright.dev/docs/api/class-elementhandle) instances can be passed as an argument to the - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate): + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate): * * ```js * const bodyHandle = await frame.evaluateHandle('document.body'); @@ -5873,8 +5884,9 @@ export interface Frame { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). + * @param options */ - evaluate(pageFunction: PageFunction, arg?: any): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise; /** * Returns the return value of @@ -5882,15 +5894,16 @@ export interface Frame { * [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * The only difference between - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) and - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) is - * that [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) and + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) + * is that + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) * returns [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * If the function, passed to the - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle), + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle), * returns a [Promise], then - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) * would wait for the promise to resolve and return its value. * * **Usage** @@ -5907,7 +5920,7 @@ export interface Frame { * ``` * * [JSHandle](https://playwright.dev/docs/api/class-jshandle) instances can be passed as an argument to the - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle): + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle): * * ```js * const aHandle = await frame.evaluateHandle(() => document.body); @@ -5921,23 +5934,25 @@ export interface Frame { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression). + * @param options */ - evaluateHandle(pageFunction: PageFunction, arg: Arg): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression) as a * [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * The only difference between - * [frame.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate) and - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) is - * that [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) + * [frame.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate) and + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) + * is that + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) * returns [JSHandle](https://playwright.dev/docs/api/class-jshandle). * * If the function, passed to the - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle), + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle), * returns a [Promise], then - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle) * would wait for the promise to resolve and return its value. * * **Usage** @@ -5954,7 +5969,7 @@ export interface Frame { * ``` * * [JSHandle](https://playwright.dev/docs/api/class-jshandle) instances can be passed as an argument to the - * [frame.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle): + * [frame.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle): * * ```js * const aHandle = await frame.evaluateHandle(() => document.body); @@ -5968,8 +5983,9 @@ export interface Frame { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression). + * @param options */ - evaluateHandle(pageFunction: PageFunction, arg?: any): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; /** * **NOTE** Use locator-based [frame.locator(selector[, options])](https://playwright.dev/docs/api/class-frame#frame-locator) @@ -11726,7 +11742,8 @@ export interface Worker { /** * JSHandle represents an in-page JavaScript object. JSHandles can be created with the - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) method. + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * method. * * ```js * const windowHandle = await page.evaluateHandle(() => window); @@ -11739,8 +11756,8 @@ export interface Worker { * * JSHandle instances can be used as an argument in * [page.$eval(selector, pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-eval-on-selector), - * [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) and - * [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) and + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) * methods. */ export interface JSHandle { @@ -11764,8 +11781,9 @@ export interface JSHandle { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). + * @param options */ - evaluate(pageFunction: PageFunctionOn, arg: Arg): Promise; + evaluate(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean }): Promise; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). @@ -11786,8 +11804,9 @@ export interface JSHandle { * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). + * @param options */ - evaluate(pageFunction: PageFunctionOn, arg?: any): Promise; + evaluate(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean }): Promise; /** * Returns the return value of @@ -11803,13 +11822,15 @@ export interface JSHandle { * If the function passed to the `jsHandle.evaluateHandle` returns a [Promise], then `jsHandle.evaluateHandle` would * wait for the promise to resolve and return its value. * - * See [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) for - * more details. + * See + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * for more details. * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression). + * @param options */ - evaluateHandle(pageFunction: PageFunctionOn, arg: Arg): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression) @@ -11824,13 +11845,15 @@ export interface JSHandle { * If the function passed to the `jsHandle.evaluateHandle` returns a [Promise], then `jsHandle.evaluateHandle` would * wait for the promise to resolve and return its value. * - * See [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) for - * more details. + * See + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * for more details. * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression). + * @param options */ - evaluateHandle(pageFunction: PageFunctionOn, arg?: any): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; /** * Returns a JSON representation of the object. If the object has a `toJSON` function, it **will not be called**. @@ -11895,7 +11918,8 @@ export interface JSHandle { * * ElementHandle instances can be used as an argument in * [page.$eval(selector, pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-eval-on-selector) - * and [page.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate) methods. + * and [page.evaluate(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate) + * methods. * * The difference between the [Locator](https://playwright.dev/docs/api/class-locator) and ElementHandle is that the * ElementHandle points to a particular element, while [Locator](https://playwright.dev/docs/api/class-locator) @@ -13837,7 +13861,7 @@ export interface Locator { * [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal }): Promise; + evaluate(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean }): Promise; /** * Execute JavaScript code in the page, taking the matching element as an argument, and return a * [JSHandle](https://playwright.dev/docs/api/class-jshandle) with the result. @@ -13863,14 +13887,15 @@ export interface Locator { * If [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle-option-expression) throws * or rejects, this method throws. * - * See [page.evaluateHandle(pageFunction[, arg])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) for - * more details. + * See + * [page.evaluateHandle(pageFunction[, arg, options])](https://playwright.dev/docs/api/class-page#page-evaluate-handle) + * for more details. * @param pageFunction Function to be evaluated in the page context. * @param arg Optional argument to pass to * [`pageFunction`](https://playwright.dev/docs/api/class-locator#locator-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean }): Promise>; /** * Execute JavaScript code in the page, taking all matching elements as an argument. * diff --git a/packages/playwright-ct-core/src/injected/serializers.ts b/packages/playwright-ct-core/src/injected/serializers.ts index be083e27db72f..3d9fee5e7659a 100644 --- a/packages/playwright-ct-core/src/injected/serializers.ts +++ b/packages/playwright-ct-core/src/injected/serializers.ts @@ -16,37 +16,15 @@ import { isImportRef } from './importRegistry'; -type FunctionRef = { - __pw_type: 'function'; - ordinal: number; -}; - -function isFunctionRef(value: any): value is FunctionRef { - return value && typeof value === 'object' && value.__pw_type === 'function'; -} - -export function wrapObject(value: any, callbacks: Function[]): any { - return transformObject(value, (v: any) => { - if (typeof v === 'function') { - const ordinal = callbacks.length; - callbacks.push(v as Function); - const result: FunctionRef = { - __pw_type: 'function', - ordinal, - }; - return { result }; - } - }); +// Validates that the component tree does not reference components defined in +// the test file. Functions in the tree are passed to evaluate() as arguments +// and materialize as callbacks in the page. +export function validateComponent(value: any) { + transformObject(value, () => undefined); } export async function unwrapObject(value: any): Promise { return transformObjectAsync(value, async (v: any) => { - if (isFunctionRef(v)) { - const result = (...args: any[]) => { - window.__ctDispatchFunction(v.ordinal, args); - }; - return { result }; - } if (isImportRef(v)) return { result: await window.__pwRegistry.resolveImportRef(v) }; }); diff --git a/packages/playwright-ct-core/src/mount.ts b/packages/playwright-ct-core/src/mount.ts index 302439f46c390..4486d17b79263 100644 --- a/packages/playwright-ct-core/src/mount.ts +++ b/packages/playwright-ct-core/src/mount.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { wrapObject } from './injected/serializers'; +import { validateComponent } from './injected/serializers'; import { Router } from './router'; import type { config, FullConfigInternal } from 'playwright/lib/common'; @@ -24,8 +24,6 @@ import type { Component, JsxComponent, MountOptions, ObjectComponentOptions } fr import type { Fixtures, Locator, Page, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions } from 'playwright/test'; import type { Page as PageImpl } from 'playwright-core/lib/client/page'; -let boundCallbacksForMount: Function[] = []; - interface MountResult extends Locator { unmount: () => Promise; update: (options: ObjectComponentOptions | JsxComponent) => Promise; @@ -52,9 +50,6 @@ export const fixtures: Fixtures if (!process.env.PLAYWRIGHT_TEST_BASE_URL) throw new Error('Component testing could not determine the base URL of your component under test. Ensure you have supplied a template playwright/index.html or have set the PLAYWRIGHT_TEST_BASE_URL environment variable.'); await (page as PageImpl)._wrapApiCall(async () => { - await page.exposeFunction('__ctDispatchFunction', (ordinal: number, args: any[]) => { - boundCallbacksForMount[ordinal](...args); - }); await page.goto(process.env.PLAYWRIGHT_TEST_BASE_URL!); }, { internal: true }); await use(page); @@ -80,7 +75,6 @@ export const fixtures: Fixtures } }); }); - boundCallbacksForMount = []; }, router: async ({ context, baseURL }, use) => { @@ -95,17 +89,19 @@ function isJsxComponent(component: any): component is JsxComponent { } async function innerUpdate(page: Page, componentRef: JsxComponent | ImportRef, options: ObjectComponentOptions = {}): Promise { - const component = wrapObject(createComponent(componentRef, options), boundCallbacksForMount); + const component = createComponent(componentRef, options); + validateComponent(component); await page.evaluate(async ({ component }) => { component = await window.__pwUnwrapObject(component); const rootElement = document.getElementById('root')!; return await window.playwrightUpdate(rootElement, component); - }, { component }); + }, { component }, { exposeFunctions: true }); } async function innerMount(page: Page, componentRef: JsxComponent | ImportRef, options: ObjectComponentOptions & MountOptions = {}): Promise { - const component = wrapObject(createComponent(componentRef, options), boundCallbacksForMount); + const component = createComponent(componentRef, options); + validateComponent(component); // WebKit does not wait for deferred scripts. await page.waitForFunction(() => !!window.playwrightMount); @@ -122,7 +118,7 @@ async function innerMount(page: Page, componentRef: JsxComponent | ImportRef, op await window.playwrightMount(component, rootElement, hooksConfig); return '#root >> internal:control=component'; - }, { component, hooksConfig: options.hooksConfig }); + }, { component, hooksConfig: options.hooksConfig }, { exposeFunctions: true }); return selector; } diff --git a/packages/playwright-ct-core/types/component.d.ts b/packages/playwright-ct-core/types/component.d.ts index 2f488b885e6c5..d3dfa9f5669f3 100644 --- a/packages/playwright-ct-core/types/component.d.ts +++ b/packages/playwright-ct-core/types/component.d.ts @@ -47,8 +47,6 @@ declare global { __pw_hooks_after_mount?: (( params: { hooksConfig?: HooksConfig; [key: string]: any } ) => Promise)[]; - // Can't start with __pw due to core reuse bindings logic for __pw*. - __ctDispatchFunction: (ordinal: number, args: any[]) => void; __pwUnwrapObject: (value: any) => Promise; __pwTransformObject: (value: any, mapping: (v: any) => { result: any } | undefined) => any; } diff --git a/packages/protocol/spec/page.yml b/packages/protocol/spec/page.yml index 722870100e7cf..0d132d013a939 100644 --- a/packages/protocol/spec/page.yml +++ b/packages/protocol/spec/page.yml @@ -107,6 +107,7 @@ Page: group: configuration parameters: name: string + noGlobal: boolean? returns: disposable: Disposable diff --git a/packages/protocol/spec/serialized.yml b/packages/protocol/spec/serialized.yml index 1457d5f8914d3..5a1ddb22d0431 100644 --- a/packages/protocol/spec/serialized.yml +++ b/packages/protocol/spec/serialized.yml @@ -79,6 +79,9 @@ SerializedValue: v: SerializedValue # An index in the handles array from SerializedArgument. h: int? + # Name of a page-side callback that routes back to a client-side function. + # Used when passing a function as (part of) an evaluate argument. + fn: string? # Index of the object in value-type for circular reference resolution. id: int? # Ref to the object in value-type for circular reference resolution. diff --git a/packages/protocol/src/serializers.ts b/packages/protocol/src/serializers.ts index 020421c646381..399de41e35575 100644 --- a/packages/protocol/src/serializers.ts +++ b/packages/protocol/src/serializers.ts @@ -81,10 +81,15 @@ function innerParseSerializedValue(value: SerializedValue, handles: any[] | unde throw new Error('Unexpected handle'); return handles[value.h]; } + if (value.fn !== undefined) { + const dummy = () => {}; + Object.defineProperty(dummy, 'name', { value: value.fn }); + return dummy; + } throw new Error(`Attempting to deserialize unexpected value${accessChainToDisplayString(accessChain)}: ${value}`); } -export type HandleOrValue = { h: number } | { fallThrough: any }; +export type HandleOrValue = { h: number } | { fn: string } | { fallThrough: any }; type VisitorInfo = { visited: Map; lastId: number; diff --git a/packages/protocol/src/structs.d.ts b/packages/protocol/src/structs.d.ts index 7d96e75d8d538..30ee9c10c1149 100644 --- a/packages/protocol/src/structs.d.ts +++ b/packages/protocol/src/structs.d.ts @@ -284,6 +284,7 @@ export type SerializedValue = { v: SerializedValue, }[], h?: number, + fn?: string, id?: number, ref?: number, }; diff --git a/packages/protocol/src/validator.ts b/packages/protocol/src/validator.ts index d18c0b9138ef0..5c080081f7e09 100644 --- a/packages/protocol/src/validator.ts +++ b/packages/protocol/src/validator.ts @@ -2318,6 +2318,7 @@ scheme.PageEmulateMediaParams = tObject({ scheme.PageEmulateMediaResult = tOptional(tObject({})); scheme.PageExposeBindingParams = tObject({ name: tString, + noGlobal: tOptional(tBoolean), }); scheme.PageExposeBindingResult = tObject({ disposable: tChannel(['Disposable']), @@ -2985,6 +2986,7 @@ scheme.SerializedValue = tObject({ v: tType('SerializedValue'), }))), h: tOptional(tInt), + fn: tOptional(tString), id: tOptional(tInt), ref: tOptional(tInt), }); diff --git a/tests/page/page-evaluate-callback.spec.ts b/tests/page/page-evaluate-callback.spec.ts new file mode 100644 index 0000000000000..6b58dfbc508ee --- /dev/null +++ b/tests/page/page-evaluate-callback.spec.ts @@ -0,0 +1,193 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * 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 { attachFrame } from '../config/utils'; +import { test as it, expect } from './pageTest'; + +it('should throw without the exposeFunctions option', async ({ page }) => { + await expect(page.evaluate(({ cb }) => (cb as any)(), { cb: () => {} })) + .rejects.toThrow(/Attempting to serialize unexpected value at position "cb": \(\) => {}/); +}); + +it('should call a function passed as an argument', async ({ page }) => { + const received: number[] = []; + await page.evaluate(async ({ cb }) => { + await cb(1); + await cb(2); + }, { cb: async (n: number) => { received.push(n); } }, { exposeFunctions: true }); + expect(received).toEqual([1, 2]); +}); + +it('should accept a function as the whole argument', async ({ page }) => { + const received: string[] = []; + await page.evaluate(async cb => { + await cb('a'); + await cb('b'); + }, async (s: string) => { received.push(s); }, { exposeFunctions: true }); + expect(received).toEqual(['a', 'b']); +}); + +it('should pass arguments to the callback', async ({ page }) => { + const args = await new Promise(resolve => { + page.evaluate(({ cb }) => cb(1, 'two', { three: 3 }, [4]), { + cb: (...a: any[]) => resolve(a), + }, { exposeFunctions: true }).catch(() => {}); + }); + expect(args).toEqual([1, 'two', { three: 3 }, [4]]); +}); + +it('should return the callback result to the page', async ({ page }) => { + const doubled = await page.evaluate(async ({ cb }) => await cb(21), { + cb: async (n: number) => n * 2, + }, { exposeFunctions: true }); + expect(doubled).toBe(42); +}); + +it('should await an async callback result', async ({ page }) => { + const value = await page.evaluate(async ({ cb }) => await cb(20), { + cb: async (n: number) => { await new Promise(f => setTimeout(f, 10)); return n + 1; }, + }, { exposeFunctions: true }); + expect(value).toBe(21); +}); + +it('should propagate callback errors to the page', async ({ page }) => { + const message = await page.evaluate(async ({ cb }) => { + try { + await cb(); + return 'no error'; + } catch (e) { + return (e as Error).message; + } + }, { cb: async () => { throw new Error('boom'); } }, { exposeFunctions: true }); + expect(message).toContain('boom'); +}); + +it('should work with a fire-and-forget setTimeout callback', async ({ page }) => { + const value = await new Promise(resolve => { + page.evaluate(({ cb }) => { setTimeout(() => cb(5), 0); }, { + cb: (n: number) => resolve(n), + }, { exposeFunctions: true }).catch(() => {}); + }); + expect(value).toBe(5); +}); + +it('should support multiple callbacks', async ({ page }) => { + const result = await page.evaluate(async ({ add, mul }) => { + return (await add(2, 3)) + (await mul(2, 3)); + }, { + add: async (a: number, b: number) => a + b, + mul: async (a: number, b: number) => a * b, + }, { exposeFunctions: true }); + expect(result).toBe(11); +}); + +it('should work with evaluateHandle', async ({ page }) => { + const received: number[] = []; + const handle = await page.evaluateHandle(async ({ cb }) => { + await cb(7); + return { done: true }; + }, { cb: async (n: number) => { received.push(n); } }, { exposeFunctions: true }); + expect(await handle.jsonValue()).toEqual({ done: true }); + expect(received).toEqual([7]); +}); + +it('should work in a child frame', async ({ page, server }) => { + await page.goto(server.EMPTY_PAGE); + const frame = await attachFrame(page, 'frame1', server.EMPTY_PAGE); + const received: number[] = []; + await frame.evaluate(async ({ cb }) => { await cb(42); }, { cb: async (n: number) => { received.push(n); } }, { exposeFunctions: true }); + expect(received).toEqual([42]); +}); + +it('should work with jsHandle.evaluate', async ({ page }) => { + const handle = await page.evaluateHandle(() => window); + const received: number[] = []; + await handle.evaluate(async (win, { cb }) => { await cb(99); }, { cb: async (n: number) => { received.push(n); } }, { exposeFunctions: true }); + expect(received).toEqual([99]); +}); + +it('should work with locator.evaluate', async ({ page }) => { + await page.setContent('
hello
'); + const received: string[] = []; + await page.locator('#target').evaluate(async (element, { cb }) => { await cb(element.id); }, { cb: async (s: string) => { received.push(s); } }, { exposeFunctions: true }); + expect(received).toEqual(['target']); +}); + +it('should return the callback result with locator.evaluate', async ({ page }) => { + await page.setContent('
7
'); + const result = await page.locator('#target').evaluate(async (element, { double }) => { + return await double(+element.textContent!); + }, { double: async (n: number) => n * 2 }, { exposeFunctions: true }); + expect(result).toBe(14); +}); + +it('should propagate callback errors with locator.evaluate', async ({ page }) => { + await page.setContent('
'); + const message = await page.locator('#target').evaluate(async (element, { cb }) => { + try { + await cb(); + return 'no error'; + } catch (e) { + return (e as Error).message; + } + }, { cb: async () => { throw new Error('boom'); } }, { exposeFunctions: true }); + expect(message).toContain('boom'); +}); + +it('should work with locator.evaluateHandle', async ({ page }) => { + await page.setContent('
hello
'); + const received: string[] = []; + const handle = await page.locator('#target').evaluateHandle(async (element, { cb }) => { + await cb(element.id); + return element; + }, { cb: async (s: string) => { received.push(s); } }, { exposeFunctions: true }); + expect(received).toEqual(['target']); + expect(await handle.evaluate(element => element.id)).toBe('target'); +}); + +it('should work with locator.evaluate inside an iframe', async ({ page, server }) => { + await page.goto(server.EMPTY_PAGE); + const frame = await attachFrame(page, 'frame1', server.EMPTY_PAGE); + await frame.evaluate(() => { document.body.innerHTML = '
in-frame
'; }); + const received: (string | null)[] = []; + await page.frameLocator('#frame1').locator('#target').evaluate(async (element, { cb }) => { + await cb(element.textContent); + }, { cb: async (text: string | null) => { received.push(text); } }, { exposeFunctions: true }); + expect(received).toEqual(['in-frame']); +}); + +it('should survive a navigation and keep working', async ({ page, server }) => { + const received: number[] = []; + await page.evaluate(async ({ cb }) => { await cb(1); }, { cb: async (n: number) => { received.push(n); } }, { exposeFunctions: true }); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(async ({ cb }) => { await cb(2); }, { cb: async (n: number) => { received.push(n); } }, { exposeFunctions: true }); + expect(received).toEqual([1, 2]); +}); + +it('should not register the callback on the global object', async ({ page }) => { + const result = await page.evaluate(async ({ cb }) => { + await cb(); + return Object.getOwnPropertyNames(globalThis).filter(name => name.startsWith('__pw_fn_')); + }, { cb: async () => {} }, { exposeFunctions: true }); + expect(result).toEqual([]); +}); + +it('should scope the page-side callback to the execution context', async ({ page, server }) => { + await page.evaluate(({ cb }) => { (window as any).__cb = cb; }, { cb: () => {} }, { exposeFunctions: true }); + expect(await page.evaluate(() => typeof (window as any).__cb)).toBe('function'); + await page.goto(server.EMPTY_PAGE); + expect(await page.evaluate(() => typeof (window as any).__cb)).toBe('undefined'); +}); diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 302fd7e31294e..81ac22c943262 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -38,11 +38,11 @@ type ZodSchema = ZodTypeAny | z3.ZodTypeAny; type InferZodSchema = T extends z3.ZodTypeAny ? z3.infer : T extends ZodTypeAny ? z.infer : never; export interface Page { - evaluate(pageFunction: PageFunction, arg: Arg): Promise; - evaluate(pageFunction: PageFunction, arg?: any): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise; - evaluateHandle(pageFunction: PageFunction, arg: Arg): Promise>; - evaluateHandle(pageFunction: PageFunction, arg?: any): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; addInitScript(script: PageFunction | { path?: string, content?: string }, arg?: Arg): Promise; @@ -85,11 +85,11 @@ export interface Page { } export interface Frame { - evaluate(pageFunction: PageFunction, arg: Arg): Promise; - evaluate(pageFunction: PageFunction, arg?: any): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise; - evaluateHandle(pageFunction: PageFunction, arg: Arg): Promise>; - evaluateHandle(pageFunction: PageFunction, arg?: any): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; $(selector: K, options?: { strict: boolean }): Promise | null>; $(selector: string, options?: { strict: boolean }): Promise | null>; @@ -156,11 +156,11 @@ export interface Worker { } export interface JSHandle { - evaluate(pageFunction: PageFunctionOn, arg: Arg): Promise; - evaluate(pageFunction: PageFunctionOn, arg?: any): Promise; + evaluate(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean }): Promise; + evaluate(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean }): Promise; - evaluateHandle(pageFunction: PageFunctionOn, arg: Arg): Promise>; - evaluateHandle(pageFunction: PageFunctionOn, arg?: any): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; jsonValue(): Promise; asElement(): T extends Node ? ElementHandle : null; @@ -190,8 +190,8 @@ export interface ElementHandle extends JSHandle { } export interface Locator { - evaluate(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal }): Promise; - evaluateHandle(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal }): Promise>; + evaluate(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean }): Promise; + evaluateHandle(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean }): Promise>; evaluateAll(pageFunction: PageFunctionOn, arg?: Arg): Promise; waitForFunction(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal }): Promise; elementHandle(options?: { timeout?: number }): Promise>;