From b73bbbd4febd660140d84e31ac3ed933a3b0acb4 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 15 Jul 2026 17:45:49 -0700 Subject: [PATCH] feat(evaluate): allow passing functions as evaluate arguments When evaluate is called with the exposeFunctions option, functions passed as (part of) the argument are exposed as page bindings with unguessable names and materialize as functions in the page. The page-side function calls the binding and returns a promise of the result. The bindings are erased upon main frame navigation, scoping the callbacks to the execution context they were passed to. Without the option, passing a function throws as before. Works for page, frame, jsHandle and locator evaluate. Component testing now mounts components with callback props through this mechanism instead of the ordinal-based __ctDispatchFunction dispatch. --- docs/src/api/class-frame.md | 6 + docs/src/api/class-jshandle.md | 6 + docs/src/api/class-locator.md | 6 + docs/src/api/class-page.md | 6 + docs/src/api/params.md | 6 + packages/injected/src/bindingsController.ts | 36 ++-- .../isomorphic/utilityScriptSerializers.ts | 16 ++ packages/playwright-client/types/types.d.ts | 185 +++++++++-------- .../src/client/channelOwner.ts | 10 + .../playwright-core/src/client/channels.d.ts | 3 +- packages/playwright-core/src/client/frame.ts | 23 ++- .../playwright-core/src/client/jsHandle.ts | 41 +++- .../playwright-core/src/client/locator.ts | 9 +- packages/playwright-core/src/client/page.ts | 28 ++- .../playwright-core/src/server/channels.d.ts | 3 +- .../src/server/dispatchers/pageDispatcher.ts | 2 +- packages/playwright-core/src/server/page.ts | 19 +- packages/playwright-core/types/types.d.ts | 185 +++++++++-------- .../src/injected/serializers.ts | 32 +-- packages/playwright-ct-core/src/mount.ts | 18 +- .../playwright-ct-core/types/component.d.ts | 2 - packages/protocol/spec/page.yml | 1 + packages/protocol/spec/serialized.yml | 3 + packages/protocol/src/serializers.ts | 7 +- packages/protocol/src/structs.d.ts | 1 + packages/protocol/src/validator.ts | 2 + tests/page/page-evaluate-callback.spec.ts | 193 ++++++++++++++++++ utils/generate_types/overrides.d.ts | 28 +-- 28 files changed, 611 insertions(+), 266 deletions(-) create mode 100644 tests/page/page-evaluate-callback.spec.ts 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>;