From 0210f83604c7fbc49f62e513e779be34565ecd49 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 14 Sep 2026 10:58:36 -0700 Subject: [PATCH 1/3] feat(evaluate): expose extended serialization for component testing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- 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/utilityScript.ts | 12 +- packages/playwright-client/types/types.d.ts | 28 ++--- .../playwright-core/src/client/channels.d.ts | 8 ++ packages/playwright-core/src/client/frame.ts | 12 +- .../playwright-core/src/client/jsHandle.ts | 28 +++-- .../playwright-core/src/client/locator.ts | 4 +- packages/playwright-core/src/client/page.ts | 4 +- .../playwright-core/src/server/channels.d.ts | 8 ++ .../dispatchers/elementHandlerDispatcher.ts | 4 +- .../src/server/dispatchers/frameDispatcher.ts | 4 +- .../server/dispatchers/jsHandleDispatcher.ts | 10 +- .../src/server/dispatchers/pageDispatcher.ts | 2 +- packages/playwright-core/src/server/dom.ts | 10 +- packages/playwright-core/src/server/frames.ts | 8 +- .../playwright-core/src/server/javascript.ts | 14 +-- packages/playwright-core/types/structs.d.ts | 4 + packages/playwright-core/types/types.d.ts | 28 ++--- packages/playwright/src/index.ts | 2 +- packages/protocol/spec/frame.yml | 2 + packages/protocol/spec/handles.yml | 2 + packages/protocol/spec/serialized.yml | 12 ++ packages/protocol/src/serializers.ts | 36 +++++- packages/protocol/src/structs.d.ts | 5 + packages/protocol/src/validator.ts | 9 ++ .../page/page-evaluate-serialization.spec.ts | 104 ++++++++++++++++++ .../playwright-test/playwright.mount.spec.ts | 68 ++++++++++++ utils/generate_types/overrides.d.ts | 28 ++--- utils/generate_types/test/test.ts | 13 ++- 33 files changed, 399 insertions(+), 100 deletions(-) create mode 100644 tests/page/page-evaluate-serialization.spec.ts create mode 100644 tests/playwright-test/playwright.mount.spec.ts diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index e5ec33f0c188d..68ebd90946e8f 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -754,6 +754,9 @@ Optional argument to pass to [`param: expression`]. ### option: Frame.evaluate.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 +### option: Frame.evaluate.serialization = %%-js-evaluate-serialization-%% +* since: v1.64 + ### option: Frame.evaluate.world = %%-js-evaluate-world-%% * since: v1.64 @@ -872,6 +875,9 @@ Optional argument to pass to [`param: expression`]. ### option: Frame.evaluateHandle.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 +### option: Frame.evaluateHandle.serialization = %%-js-evaluate-serialization-%% +* since: v1.64 + ## 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 5a883bf8892d6..0f0b446bdc53a 100644 --- a/docs/src/api/class-jshandle.md +++ b/docs/src/api/class-jshandle.md @@ -99,6 +99,9 @@ Optional argument to pass to [`param: expression`]. ### option: JSHandle.evaluate.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 +### option: JSHandle.evaluate.serialization = %%-js-evaluate-serialization-%% +* since: v1.64 + ## async method: JSHandle.evaluateHandle * since: v1.8 - returns: <[JSHandle]> @@ -129,6 +132,9 @@ Optional argument to pass to [`param: expression`]. ### option: JSHandle.evaluateHandle.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 +### option: JSHandle.evaluateHandle.serialization = %%-js-evaluate-serialization-%% +* since: v1.64 + ## 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 945cc8884c48f..7fce10d00515c 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -1181,6 +1181,9 @@ Optional argument to pass to [`param: expression`]. ### option: Locator.evaluate.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 +### option: Locator.evaluate.serialization = %%-js-evaluate-serialization-%% +* since: v1.64 + ### option: Locator.evaluate.world = %%-js-evaluate-world-%% * since: v1.64 @@ -1289,6 +1292,9 @@ Optional argument to pass to [`param: expression`]. ### option: Locator.evaluateHandle.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 +### option: Locator.evaluateHandle.serialization = %%-js-evaluate-serialization-%% +* since: v1.64 + ### 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 5466537a8ca63..83e1c7066d817 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -1605,6 +1605,9 @@ Optional argument to pass to [`param: expression`]. ### option: Page.evaluate.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 +### option: Page.evaluate.serialization = %%-js-evaluate-serialization-%% +* since: v1.64 + ### option: Page.evaluate.world = %%-js-evaluate-world-%% * since: v1.64 @@ -1720,6 +1723,9 @@ Optional argument to pass to [`param: expression`]. ### option: Page.evaluateHandle.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 +### option: Page.evaluateHandle.serialization = %%-js-evaluate-serialization-%% +* since: v1.64 + ## async method: Page.exposeBinding * since: v1.8 - returns: <[Disposable]> diff --git a/docs/src/api/params.md b/docs/src/api/params.md index e5f02b313ab93..24bb5a0502fa0 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-serialization +* langs: js +- `serialization` <[Serialization]<"default"|"extended">> + +When set to `"extended"`, preserves additional built-in types in the evaluation argument and return value. Currently supports [Map] and [Set], including nested collections and circular references. Defaults to `"default"`. + ## js-evaluate-expose-functions * langs: js - `exposeFunctions` <[boolean]> diff --git a/packages/injected/src/utilityScript.ts b/packages/injected/src/utilityScript.ts index 5c54d9ed98814..a39085159f6a9 100644 --- a/packages/injected/src/utilityScript.ts +++ b/packages/injected/src/utilityScript.ts @@ -63,7 +63,7 @@ export class UtilityScript { (global as any).builtins = this.builtins; } - evaluate(isFunction: boolean | undefined, returnByValue: boolean, expression: string, argCount: number, ...argsAndHandles: any[]) { + evaluate(isFunction: boolean | undefined, returnByValue: boolean, extendedSerialization: boolean | undefined, expression: string, argCount: number, ...argsAndHandles: any[]) { const args = argsAndHandles.slice(0, argCount); const handles = argsAndHandles.slice(argCount); const parameters = []; @@ -80,20 +80,20 @@ export class UtilityScript { if (typeof result === 'function') result = result(...parameters); } - return returnByValue ? this._promiseAwareJsonValueNoThrow(result) : result; + return returnByValue ? this._promiseAwareJsonValueNoThrow(result, extendedSerialization) : result; } - jsonValue(returnByValue: true, value: any) { + jsonValue(returnByValue: true, value: any, extendedSerialization?: boolean) { // Special handling of undefined to work-around multi-step returnByValue handling in WebKit. if (value === undefined) return undefined; - return serializeAsCallArgument(value, (value: any) => ({ fallThrough: value })); + return serializeAsCallArgument(value, (value: any) => ({ fallThrough: value }), { extendedSerialization }); } - private _promiseAwareJsonValueNoThrow(value: any) { + private _promiseAwareJsonValueNoThrow(value: any, extendedSerialization?: boolean) { const safeJson = (value: any) => { try { - return this.jsonValue(true, value); + return this.jsonValue(true, value, extendedSerialization); } catch (e) { return undefined; } diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 1aaccdadf3550..1723fb9df3eac 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -134,7 +134,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; /** * Returns the value of the * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression) invocation. @@ -187,7 +187,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; /** * Returns the value of the @@ -235,7 +235,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; /** * Returns the value of the * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression) invocation as a @@ -282,7 +282,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; /** * Adds a script which would be evaluated in one of the following scenarios: @@ -5963,7 +5963,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). @@ -6012,7 +6012,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; /** * Returns the return value of @@ -6062,7 +6062,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression) as a @@ -6111,7 +6111,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; /** * **NOTE** Use locator-based [frame.locator(selector[, options])](https://playwright.dev/docs/api/class-frame#frame-locator) @@ -12035,7 +12035,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean }): Promise; + evaluate(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). @@ -12058,7 +12058,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean }): Promise; + evaluate(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise; /** * Returns the return value of @@ -12082,7 +12082,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression) @@ -12105,7 +12105,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; /** * Returns a JSON representation of the object. If the object has a `toJSON` function, it **will not be called**. @@ -14300,7 +14300,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, exposeFunctions?: boolean, world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): 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. @@ -14334,7 +14334,7 @@ export interface Locator { * [`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, exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; /** * Execute JavaScript code in the page, taking all matching elements as an argument. * diff --git a/packages/playwright-core/src/client/channels.d.ts b/packages/playwright-core/src/client/channels.d.ts index a645cd61a81e3..4419751fc4bbb 100644 --- a/packages/playwright-core/src/client/channels.d.ts +++ b/packages/playwright-core/src/client/channels.d.ts @@ -2479,10 +2479,12 @@ export type FrameEvaluateExpressionParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, + extendedSerialization?: boolean, world?: 'main' | 'utility', }; export type FrameEvaluateExpressionOptions = { isFunction?: boolean, + extendedSerialization?: boolean, world?: 'main' | 'utility', }; export type FrameEvaluateExpressionResult = { @@ -2492,9 +2494,11 @@ export type FrameEvaluateExpressionHandleParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, + extendedSerialization?: boolean, }; export type FrameEvaluateExpressionHandleOptions = { isFunction?: boolean, + extendedSerialization?: boolean, }; export type FrameEvaluateExpressionHandleResult = { handle: JSHandleChannel, @@ -2938,10 +2942,12 @@ export type JSHandleEvaluateExpressionParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, + extendedSerialization?: boolean, world?: 'main' | 'utility', }; export type JSHandleEvaluateExpressionOptions = { isFunction?: boolean, + extendedSerialization?: boolean, world?: 'main' | 'utility', }; export type JSHandleEvaluateExpressionResult = { @@ -2951,9 +2957,11 @@ export type JSHandleEvaluateExpressionHandleParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, + extendedSerialization?: boolean, }; export type JSHandleEvaluateExpressionHandleOptions = { isFunction?: boolean, + extendedSerialization?: boolean, }; export type JSHandleEvaluateExpressionHandleResult = { handle: JSHandleChannel, diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index b0b6f1d998e33..77664b2938424 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -35,7 +35,7 @@ import { kLifecycleEvents } from './types'; import { Waiter } from './waiter'; import { TimeoutSettings, kNoTimeout } from './timeoutSettings'; -import type { EvaluateOptions, ExposeFunctionsOptions, WorldOptions } from './jsHandle'; +import type { EvaluateOptions, EvaluateHandleOptions, WorldOptions } from './jsHandle'; import type { LocatorOptions } from './locator'; import type { Page } from './page'; import type { DropPayload, FilePayload, LifecycleEvent, SelectOption, SelectOptionOptions, StrictOptions, TimeoutOptions, WaitForFunctionOptions } from './types'; @@ -207,19 +207,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, options?: ExposeFunctionsOptions): Promise> { + async evaluateHandle(pageFunction: structs.PageFunction, arg?: Arg, options?: EvaluateHandleOptions): 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); + const serializedArg = options?.exposeFunctions ? await serializeArgumentWithCallbacks(this, this._page, arg, options) : serializeArgument(arg, undefined, options); + const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg, extendedSerialization: options?.serialization === 'extended' }, kNoTimeout); return JSHandle.from(result.handle) as any as structs.SmartHandle; } 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, world: options?.world }, kNoTimeout); + const serializedArg = options?.exposeFunctions ? await serializeArgumentWithCallbacks(this, this._page, arg, options) : serializeArgument(arg, undefined, options); + const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg, world: options?.world, extendedSerialization: options?.serialization === 'extended' }, kNoTimeout); return parseResult(result.value); } diff --git a/packages/playwright-core/src/client/jsHandle.ts b/packages/playwright-core/src/client/jsHandle.ts index 2415cfefab474..48dc8d41a9b98 100644 --- a/packages/playwright-core/src/client/jsHandle.ts +++ b/packages/playwright-core/src/client/jsHandle.ts @@ -40,22 +40,22 @@ export class JSHandle extends ChannelOwner im this._channel.on('previewUpdated', ({ preview }) => this._preview = preview); } - async evaluate(pageFunction: structs.PageFunctionOn, arg?: Arg, options?: ExposeFunctionsOptions): Promise { + async evaluate(pageFunction: structs.PageFunctionOn, arg?: Arg, options?: EvaluateHandleOptions): Promise { assertEvaluateOptions(options); - return await this._evaluate(pageFunction, arg, { exposeFunctions: options?.exposeFunctions }); + return await this._evaluate(pageFunction, arg, { exposeFunctions: options?.exposeFunctions, serialization: options?.serialization }); } 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, world: options?.world }, kNoTimeout); + const serializedArg = options?.exposeFunctions ? await serializeArgumentWithCallbacks(this, this._parentOfType('Page') as Page | undefined, arg, options) : serializeArgument(arg, undefined, options); + const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg, world: options?.world, extendedSerialization: options?.serialization === 'extended' }, kNoTimeout); return parseResult(result.value); } - async evaluateHandle(pageFunction: structs.PageFunctionOn, arg?: Arg, options?: ExposeFunctionsOptions): Promise> { + async evaluateHandle(pageFunction: structs.PageFunctionOn, arg?: Arg, options?: EvaluateHandleOptions): 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); + const serializedArg = options?.exposeFunctions ? await serializeArgumentWithCallbacks(this, this._parentOfType('Page') as Page | undefined, arg, options) : serializeArgument(arg, undefined, options); + const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg, extendedSerialization: options?.serialization === 'extended' }, kNoTimeout); return JSHandle.from(result.handle) as any as structs.SmartHandle; } @@ -100,7 +100,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, registerCallback?: (callback: Function) => string): channels.SerializedArgument { +export function serializeArgument(arg: any, registerCallback?: (callback: Function) => string, options?: SerializationOptions): channels.SerializedArgument { const handles: channels.Channel[] = []; const pushHandle = (channel: channels.Channel): number => { handles.push(channel); @@ -112,15 +112,17 @@ export function serializeArgument(arg: any, registerCallback?: (callback: Functi if (typeof value === 'function' && registerCallback) return { fn: registerCallback(value as Function) }; return { fallThrough: value }; - }); + }, { extendedSerialization: options?.serialization === 'extended' }); return { value, handles }; } export type WorldOptions = { world?: 'main' | 'utility' }; export type ExposeFunctionsOptions = { exposeFunctions?: boolean }; -export type EvaluateOptions = ExposeFunctionsOptions & WorldOptions; +export type SerializationOptions = { serialization?: 'default' | 'extended' }; +export type EvaluateHandleOptions = ExposeFunctionsOptions & SerializationOptions; +export type EvaluateOptions = EvaluateHandleOptions & WorldOptions; -export async function serializeArgumentWithCallbacks(owner: ChannelOwner, page: Page | undefined, arg: any): Promise { +export async function serializeArgumentWithCallbacks(owner: ChannelOwner, page: Page | undefined, arg: any, options?: SerializationOptions): Promise { return await owner._wrapApiCall(async () => { const exposePromises: Promise[] = []; const serialized = serializeArgument(arg, callback => { @@ -129,7 +131,7 @@ export async function serializeArgumentWithCallbacks(owner: ChannelOwner, p const name = kFunctionBindingPrefix + createGuid(); exposePromises.push(page._exposeEvaluateCallback(name, callback)); return name; - }); + }, options); await Promise.all(exposePromises); return serialized; }, { internal: true }); @@ -147,4 +149,6 @@ export function assertMaxArguments(count: number, max: number): asserts count { 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.'); + if (options?.serialization !== undefined && options.serialization !== 'default' && options.serialization !== 'extended') + throw new Error('serialization: expected one of (default|extended)'); } diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index d9c4bb21192f7..6ff220960ef2d 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -28,7 +28,7 @@ import { DisposableStub } from './disposable'; import { kNoTimeout } from './timeoutSettings'; import type { ExpectResult, Frame } from './frame'; -import type { EvaluateOptions, ExposeFunctionsOptions, WorldOptions } from './jsHandle'; +import type { EvaluateOptions, EvaluateHandleOptions, WorldOptions } 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'; @@ -145,7 +145,7 @@ export class Locator implements api.Locator { return await this._frame.$$eval(this._selector, pageFunction, arg, options); } - async evaluateHandle(pageFunction: structs.PageFunctionOn, arg?: Arg, options?: TimeoutOptions & ExposeFunctionsOptions): Promise> { + async evaluateHandle(pageFunction: structs.PageFunctionOn, arg?: Arg, options?: TimeoutOptions & EvaluateHandleOptions): Promise> { return await this._withElement(h => h.evaluateHandle(pageFunction, arg, options), { title: 'Evaluate', timeout: options?.timeout, signal: options?.signal }); } diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 381b833de3822..0c6451de1aaf5 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -50,7 +50,7 @@ import { TimeoutSettings, kNoTimeout } from './timeoutSettings'; import { mkdirIfNeeded } from './fileUtils'; import { ConsoleMessage } from './consoleMessage'; import type { BrowserContext } from './browserContext'; -import type { EvaluateOptions, ExposeFunctionsOptions, WorldOptions } from './jsHandle'; +import type { EvaluateOptions, EvaluateHandleOptions, ExposeFunctionsOptions, WorldOptions } from './jsHandle'; import type { Clock } from './clock'; import type { APIRequestContext } from './fetch'; import type { WaitForNavigationOptions } from './frame'; @@ -339,7 +339,7 @@ export class Page extends ChannelOwner implements api.Page return await this._mainFrame.dispatchEvent(selector, type, eventInit, options); } - async evaluateHandle(pageFunction: structs.PageFunction, arg?: Arg, options?: ExposeFunctionsOptions): Promise> { + async evaluateHandle(pageFunction: structs.PageFunction, arg?: Arg, options?: EvaluateHandleOptions): Promise> { assertMaxArguments(arguments.length, 3); return await this._mainFrame.evaluateHandle(pageFunction, arg, options); } diff --git a/packages/playwright-core/src/server/channels.d.ts b/packages/playwright-core/src/server/channels.d.ts index f2fc853c42368..5e60915b6d8d8 100644 --- a/packages/playwright-core/src/server/channels.d.ts +++ b/packages/playwright-core/src/server/channels.d.ts @@ -2480,10 +2480,12 @@ export type FrameEvaluateExpressionParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, + extendedSerialization?: boolean, world?: 'main' | 'utility', }; export type FrameEvaluateExpressionOptions = { isFunction?: boolean, + extendedSerialization?: boolean, world?: 'main' | 'utility', }; export type FrameEvaluateExpressionResult = { @@ -2493,9 +2495,11 @@ export type FrameEvaluateExpressionHandleParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, + extendedSerialization?: boolean, }; export type FrameEvaluateExpressionHandleOptions = { isFunction?: boolean, + extendedSerialization?: boolean, }; export type FrameEvaluateExpressionHandleResult = { handle: JSHandleChannel, @@ -2939,10 +2943,12 @@ export type JSHandleEvaluateExpressionParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, + extendedSerialization?: boolean, world?: 'main' | 'utility', }; export type JSHandleEvaluateExpressionOptions = { isFunction?: boolean, + extendedSerialization?: boolean, world?: 'main' | 'utility', }; export type JSHandleEvaluateExpressionResult = { @@ -2952,9 +2958,11 @@ export type JSHandleEvaluateExpressionHandleParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, + extendedSerialization?: boolean, }; export type JSHandleEvaluateExpressionHandleOptions = { isFunction?: boolean, + extendedSerialization?: boolean, }; export type JSHandleEvaluateExpressionHandleResult = { handle: JSHandleChannel, diff --git a/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts b/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts index 1522e049a4d17..ee964792261db 100644 --- a/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts @@ -56,8 +56,8 @@ export class ElementHandleDispatcher extends JSHandleDispatcher } override async evaluateExpression(params: channels.JSHandleEvaluateExpressionParams, progress: Progress): Promise { - const value = await this._elementHandle.evaluateExpression(progress, params.expression, { isFunction: params.isFunction, world: params.world }, parseArgument(params.arg)); - return { value: serializeResult(value) }; + const value = await this._elementHandle.evaluateExpression(progress, params.expression, { isFunction: params.isFunction, world: params.world, extendedSerialization: params.extendedSerialization }, parseArgument(params.arg)); + return { value: serializeResult(value, params) }; } async ownerFrame(params: channels.ElementHandleOwnerFrameParams, progress: Progress): Promise { diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index 412d23c39f094..4c436e2ab237a 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -82,11 +82,11 @@ export class FrameDispatcher extends Dispatcher { - return { value: serializeResult(await this._frame.evaluateExpression(progress, params.expression, { isFunction: params.isFunction, world: params.world }, parseArgument(params.arg))) }; + return { value: serializeResult(await this._frame.evaluateExpression(progress, params.expression, { isFunction: params.isFunction, world: params.world, extendedSerialization: params.extendedSerialization }, parseArgument(params.arg)), params) }; } async evaluateExpressionHandle(params: channels.FrameEvaluateExpressionHandleParams, progress: Progress): Promise { - return { handle: ElementHandleDispatcher.fromJSOrElementHandle(this, await this._frame.evaluateExpressionHandle(progress, params.expression, { isFunction: params.isFunction }, parseArgument(params.arg))) }; + return { handle: ElementHandleDispatcher.fromJSOrElementHandle(this, await this._frame.evaluateExpressionHandle(progress, params.expression, { isFunction: params.isFunction, extendedSerialization: params.extendedSerialization }, parseArgument(params.arg))) }; } async waitForSelector(params: channels.FrameWaitForSelectorParams, progress: Progress): Promise { diff --git a/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts b/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts index c2aa99a892d0c..9957c5c591df8 100644 --- a/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts @@ -45,12 +45,12 @@ export class JSHandleDispatcher { if (params.world) throw new Error(`Only element handles can be evaluated in the "${params.world}" world`); - const jsHandle = await this._object.evaluateExpression(progress, params.expression, { isFunction: params.isFunction }, parseArgument(params.arg)); - return { value: serializeResult(jsHandle) }; + const jsHandle = await this._object.evaluateExpression(progress, params.expression, { isFunction: params.isFunction, extendedSerialization: params.extendedSerialization }, parseArgument(params.arg)); + return { value: serializeResult(jsHandle, params) }; } async evaluateExpressionHandle(params: channels.JSHandleEvaluateExpressionHandleParams, progress: Progress): Promise { - const jsHandle = await this._object.evaluateExpressionHandle(progress, params.expression, { isFunction: params.isFunction }, parseArgument(params.arg)); + const jsHandle = await this._object.evaluateExpressionHandle(progress, params.expression, { isFunction: params.isFunction, extendedSerialization: params.extendedSerialization }, parseArgument(params.arg)); // If "jsHandle" is an ElementHandle, it belongs to the same frame as "this". return { handle: ElementHandleDispatcher.fromJSOrElementHandle(this.parentScope() as FrameDispatcher, jsHandle) }; } @@ -91,6 +91,6 @@ export function parseValue(v: channels.SerializedValue): any { return parseSerializedValue(v, []); } -export function serializeResult(arg: any): channels.SerializedValue { - return serializeValue(arg, value => ({ fallThrough: value })); +export function serializeResult(arg: any, options?: { extendedSerialization?: boolean }): channels.SerializedValue { + return serializeValue(arg, value => ({ fallThrough: value }), options); } diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 4b351777a85a5..9066ef864f622 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -598,7 +598,7 @@ export class BindingCallDispatcher extends Dispatcher serializeResult(arg)), }); this._promise = new Promise((resolve, reject) => { this._resolve = resolve; diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 39f725084b890..33cd0a4bf6f0e 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -73,11 +73,11 @@ export class FrameExecutionContext extends js.ExecutionContext { return js.evaluate(this, false /* returnByValue */, pageFunction, arg); } - async evaluateExpression(expression: string, options: { isFunction?: boolean }, arg?: any): Promise { + async evaluateExpression(expression: string, options: { isFunction?: boolean, extendedSerialization?: boolean }, arg?: any): Promise { return js.evaluateExpression(this, expression, { ...options, returnByValue: true }, arg); } - async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean }, arg?: any): Promise> { + async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, extendedSerialization?: boolean }, arg?: any): Promise> { return js.evaluateExpression(this, expression, { ...options, returnByValue: false }, arg); } @@ -869,13 +869,13 @@ export class ElementHandle extends js.JSHandle { return this._frame.selectors.queryAll(selector, this); } - override async evaluateExpression(progress: Progress, expression: string, options: { isFunction?: boolean, world?: types.World }, arg: any): Promise { + override async evaluateExpression(progress: Progress, expression: string, options: { isFunction?: boolean, world?: types.World, extendedSerialization?: boolean }, arg: any): Promise { return await progress.race(this.internalEvaluateExpression(expression, options, arg)); } - override async internalEvaluateExpression(expression: string, options: { isFunction?: boolean, world?: types.World }, arg: any): Promise { + override async internalEvaluateExpression(expression: string, options: { isFunction?: boolean, world?: types.World, extendedSerialization?: boolean }, arg: any): Promise { const context = options.world ? await this._frame.context(options.world) : this._context; - return await js.evaluateExpression(context, expression, { isFunction: options.isFunction, returnByValue: true }, this, arg); + return await js.evaluateExpression(context, expression, { isFunction: options.isFunction, extendedSerialization: options.extendedSerialization, returnByValue: true }, this, arg); } async evalOnSelector(progress: Progress, selector: string, strict: boolean, expression: string, options: { isFunction?: boolean, world?: types.World }, arg: any): Promise { diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 2c20749b71933..657b8bdf433f2 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -808,21 +808,21 @@ export class Frame extends SdkObject { return this.context('utility'); } - async evaluateExpression(progress: Progress, expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise { + async evaluateExpression(progress: Progress, expression: string, options: { isFunction?: boolean, world?: types.World, extendedSerialization?: boolean } = {}, arg?: any): Promise { return await progress.race(this._evaluateExpression(expression, options, arg)); } - private async _evaluateExpression(expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise { + private async _evaluateExpression(expression: string, options: { isFunction?: boolean, world?: types.World, extendedSerialization?: boolean } = {}, arg?: any): Promise { const context = await this.context(options.world ?? 'main'); const value = await context.evaluateExpression(expression, options, arg); return value; } - async evaluateExpressionHandle(progress: Progress, expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise> { + async evaluateExpressionHandle(progress: Progress, expression: string, options: { isFunction?: boolean, world?: types.World, extendedSerialization?: boolean } = {}, arg?: any): Promise> { return await progress.race(this._evaluateExpressionHandle(expression, options, arg)); } - private async _evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, world?: types.World } = {}, arg?: any): Promise> { + private async _evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, world?: types.World, extendedSerialization?: boolean } = {}, arg?: any): Promise> { const context = await this.context(options.world ?? 'main'); const value = await context.evaluateExpressionHandle(expression, options, arg); return value; diff --git a/packages/playwright-core/src/server/javascript.ts b/packages/playwright-core/src/server/javascript.ts index adaae310f1bf2..8467bbd45503d 100644 --- a/packages/playwright-core/src/server/javascript.ts +++ b/packages/playwright-core/src/server/javascript.ts @@ -143,11 +143,11 @@ export class JSHandle extends SdkObject { (globalThis as any).leakedJSHandles.set(this, new Error('Leaked JSHandle')); } - async evaluateExpression(progress: Progress, expression: string, options: { isFunction?: boolean }, arg: any) { + async evaluateExpression(progress: Progress, expression: string, options: { isFunction?: boolean, extendedSerialization?: boolean }, arg: any) { return await progress.race(this.internalEvaluateExpression(expression, options, arg)); } - async evaluateExpressionHandle(progress: Progress, expression: string, options: { isFunction?: boolean }, arg: any): Promise> { + async evaluateExpressionHandle(progress: Progress, expression: string, options: { isFunction?: boolean, extendedSerialization?: boolean }, arg: any): Promise> { return await progress.race(this._evaluateExpressionHandle(expression, options, arg)); } @@ -171,11 +171,11 @@ export class JSHandle extends SdkObject { return evaluate(this._context, false /* returnByValue */, pageFunction, this, arg); } - async internalEvaluateExpression(expression: string, options: { isFunction?: boolean }, arg: any) { + async internalEvaluateExpression(expression: string, options: { isFunction?: boolean, extendedSerialization?: boolean }, arg: any) { return await evaluateExpression(this._context, expression, { ...options, returnByValue: true }, this, arg); } - private async _evaluateExpressionHandle(expression: string, options: { isFunction?: boolean }, arg: any): Promise> { + private async _evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, extendedSerialization?: boolean }, arg: any): Promise> { return await evaluateExpression(this._context, expression, { ...options, returnByValue: false }, this, arg); } @@ -254,7 +254,7 @@ export async function evaluate(context: ExecutionContext, returnByValue: boolean return evaluateExpression(context, String(pageFunction), { returnByValue, isFunction: typeof pageFunction === 'function' }, ...args); } -export async function evaluateExpression(context: ExecutionContext, expression: string, options: { returnByValue?: boolean, isFunction?: boolean }, ...args: any[]): Promise { +export async function evaluateExpression(context: ExecutionContext, expression: string, options: { returnByValue?: boolean, isFunction?: boolean, extendedSerialization?: boolean }, ...args: any[]): Promise { expression = normalizeEvaluationExpression(expression, options.isFunction); const handles: (Promise)[] = []; const toDispose: Promise[] = []; @@ -276,7 +276,7 @@ export async function evaluateExpression(context: ExecutionContext, expression: return { h: pushHandle(adopted) }; } return { fallThrough: handle }; - })); + }, options)); const utilityScriptObjects: JSHandle[] = []; for (const handle of await Promise.all(handles)) { @@ -286,7 +286,7 @@ export async function evaluateExpression(context: ExecutionContext, expression: } // See UtilityScript for arguments. - const utilityScriptValues = [options.isFunction, options.returnByValue, expression, args.length, ...args]; + const utilityScriptValues = [options.isFunction, options.returnByValue, options.extendedSerialization, expression, args.length, ...args]; const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`; try { diff --git a/packages/playwright-core/types/structs.d.ts b/packages/playwright-core/types/structs.d.ts index 3b671e99ac17b..a5b6c922a7a5a 100644 --- a/packages/playwright-core/types/structs.d.ts +++ b/packages/playwright-core/types/structs.d.ts @@ -27,6 +27,8 @@ export type EvaluationArgument = {}; export type NoHandles = Arg extends JSHandle ? never : + Arg extends Map ? Map, NoHandles> : + Arg extends Set ? Set> : Arg extends (...args: infer T) => PromiseLike ? (...args: T) => Promise> : Arg extends (...args: infer T) => infer R ? (...args: T) => NoHandles : Arg extends object ? { [Key in keyof Arg]: NoHandles } : @@ -34,6 +36,8 @@ export type NoHandles = export type Unboxed = Arg extends ElementHandle ? T : Arg extends JSHandle ? T : + Arg extends Map ? Map, Unboxed> : + Arg extends Set ? Set> : Arg extends (...args: infer T) => PromiseLike ? (...args: T) => Promise> : Arg extends (...args: infer T) => infer R ? (...args: T) => Unboxed : Arg extends NoHandles ? Arg : diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 1aaccdadf3550..1723fb9df3eac 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -134,7 +134,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; /** * Returns the value of the * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression) invocation. @@ -187,7 +187,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; /** * Returns the value of the @@ -235,7 +235,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; /** * Returns the value of the * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression) invocation as a @@ -282,7 +282,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; /** * Adds a script which would be evaluated in one of the following scenarios: @@ -5963,7 +5963,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). @@ -6012,7 +6012,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; /** * Returns the return value of @@ -6062,7 +6062,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression) as a @@ -6111,7 +6111,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; /** * **NOTE** Use locator-based [frame.locator(selector[, options])](https://playwright.dev/docs/api/class-frame#frame-locator) @@ -12035,7 +12035,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean }): Promise; + evaluate(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). @@ -12058,7 +12058,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean }): Promise; + evaluate(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise; /** * Returns the return value of @@ -12082,7 +12082,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression) @@ -12105,7 +12105,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; /** * Returns a JSON representation of the object. If the object has a `toJSON` function, it **will not be called**. @@ -14300,7 +14300,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, exposeFunctions?: boolean, world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): 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. @@ -14334,7 +14334,7 @@ export interface Locator { * [`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, exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; /** * Execute JavaScript code in the page, taking all matching elements as an argument. * diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 778a39cc98df6..ba3af8a2bfd3d 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -508,7 +508,7 @@ const playwrightFixtures: Fixtures { if (!baseURL) throw new Error('mount() requires `baseURL` to point at the component gallery. Set it in your Playwright config.'); diff --git a/packages/protocol/spec/frame.yml b/packages/protocol/spec/frame.yml index 439ebfd99a73f..f69f7606c4b9f 100644 --- a/packages/protocol/spec/frame.yml +++ b/packages/protocol/spec/frame.yml @@ -337,6 +337,7 @@ Frame: expression: string isFunction: boolean? arg: SerializedArgument + extendedSerialization: boolean? world: type: enum? literals: @@ -354,6 +355,7 @@ Frame: expression: string isFunction: boolean? arg: SerializedArgument + extendedSerialization: boolean? returns: handle: JSHandle flags: diff --git a/packages/protocol/spec/handles.yml b/packages/protocol/spec/handles.yml index 00e581fe657f9..c1fd5cb98943b 100644 --- a/packages/protocol/spec/handles.yml +++ b/packages/protocol/spec/handles.yml @@ -29,6 +29,7 @@ JSHandle: expression: string isFunction: boolean? arg: SerializedArgument + extendedSerialization: boolean? world: type: enum? literals: @@ -46,6 +47,7 @@ JSHandle: expression: string isFunction: boolean? arg: SerializedArgument + extendedSerialization: boolean? returns: handle: JSHandle flags: diff --git a/packages/protocol/spec/serialized.yml b/packages/protocol/spec/serialized.yml index e40ee4803ce45..0b82544a8daeb 100644 --- a/packages/protocol/spec/serialized.yml +++ b/packages/protocol/spec/serialized.yml @@ -77,6 +77,18 @@ SerializedValue: properties: k: string v: SerializedValue + # Map entries. + me: + type: array? + items: + type: object + properties: + k: SerializedValue + v: SerializedValue + # Set entries. + se: + type: array? + items: 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. diff --git a/packages/protocol/src/serializers.ts b/packages/protocol/src/serializers.ts index 0b245c1b8f24c..c45dff898af7c 100644 --- a/packages/protocol/src/serializers.ts +++ b/packages/protocol/src/serializers.ts @@ -111,6 +111,20 @@ function innerParseSerializedValue(value: SerializedValue, handles: any[] | unde result[k] = innerParseSerializedValue(v, handles, refs, [...accessChain, k]); return result; } + if (value.me !== undefined) { + const result = new Map(); + refs.set(value.id!, result); + for (const { k, v } of value.me) + result.set(innerParseSerializedValue(k, handles, refs, accessChain), innerParseSerializedValue(v, handles, refs, accessChain)); + return result; + } + if (value.se !== undefined) { + const result = new Set(); + refs.set(value.id!, result); + for (const item of value.se) + result.add(innerParseSerializedValue(item, handles, refs, accessChain)); + return result; + } if (value.h !== undefined) { if (handles === undefined) throw new Error('Unexpected handle'); @@ -128,10 +142,11 @@ export type HandleOrValue = { h: number } | { fn: string } | { fallThrough: any type VisitorInfo = { visited: Map; lastId: number; + extendedSerialization?: boolean; }; -export function serializeValue(value: any, handleSerializer: (value: any) => HandleOrValue): SerializedValue { - return innerSerializeValue(value, handleSerializer, { lastId: 0, visited: new Map() }, []); +export function serializeValue(value: any, handleSerializer: (value: any) => HandleOrValue, options: { extendedSerialization?: boolean } = {}): SerializedValue { + return innerSerializeValue(value, handleSerializer, { lastId: 0, visited: new Map(), extendedSerialization: options.extendedSerialization }, []); } export function serializePlainValue(arg: any): SerializedValue { @@ -184,6 +199,23 @@ function innerSerializeValue(value: any, handleSerializer: (value: any) => Handl if (id) return { ref: id }; + if (visitorInfo.extendedSerialization && value instanceof Map) { + const me: { k: SerializedValue, v: SerializedValue }[] = []; + const id = ++visitorInfo.lastId; + visitorInfo.visited.set(value, id); + for (const [k, v] of value) + me.push({ k: innerSerializeValue(k, handleSerializer, visitorInfo, accessChain), v: innerSerializeValue(v, handleSerializer, visitorInfo, accessChain) }); + return { me, id }; + } + if (visitorInfo.extendedSerialization && value instanceof Set) { + const se: SerializedValue[] = []; + const id = ++visitorInfo.lastId; + visitorInfo.visited.set(value, id); + for (const item of value) + se.push(innerSerializeValue(item, handleSerializer, visitorInfo, accessChain)); + return { se, id }; + } + if (Array.isArray(value)) { const a = []; const id = ++visitorInfo.lastId; diff --git a/packages/protocol/src/structs.d.ts b/packages/protocol/src/structs.d.ts index 084e3c13e74fb..1d230fda89c07 100644 --- a/packages/protocol/src/structs.d.ts +++ b/packages/protocol/src/structs.d.ts @@ -297,6 +297,11 @@ export type SerializedValue = { k: string, v: SerializedValue, }[], + me?: { + k: SerializedValue, + v: SerializedValue, + }[], + se?: SerializedValue[], h?: number, fn?: string, id?: number, diff --git a/packages/protocol/src/validator.ts b/packages/protocol/src/validator.ts index 21e7cbe6036e3..408f65c4d443b 100644 --- a/packages/protocol/src/validator.ts +++ b/packages/protocol/src/validator.ts @@ -1349,6 +1349,7 @@ scheme.FrameEvaluateExpressionParams = tObject({ expression: tString, isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), + extendedSerialization: tOptional(tBoolean), world: tOptional(tEnum(['main', 'utility'])), }); scheme.FrameEvaluateExpressionResult = tObject({ @@ -1358,6 +1359,7 @@ scheme.FrameEvaluateExpressionHandleParams = tObject({ expression: tString, isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), + extendedSerialization: tOptional(tBoolean), }); scheme.FrameEvaluateExpressionHandleResult = tObject({ handle: tChannel(['ElementHandle', 'JSHandle']), @@ -1639,6 +1641,7 @@ scheme.JSHandleEvaluateExpressionParams = tObject({ expression: tString, isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), + extendedSerialization: tOptional(tBoolean), world: tOptional(tEnum(['main', 'utility'])), }); scheme.ElementHandleEvaluateExpressionParams = tType('JSHandleEvaluateExpressionParams'); @@ -1650,6 +1653,7 @@ scheme.JSHandleEvaluateExpressionHandleParams = tObject({ expression: tString, isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), + extendedSerialization: tOptional(tBoolean), }); scheme.ElementHandleEvaluateExpressionHandleParams = tType('JSHandleEvaluateExpressionHandleParams'); scheme.JSHandleEvaluateExpressionHandleResult = tObject({ @@ -2998,6 +3002,11 @@ scheme.SerializedValue = tObject({ k: tString, v: tType('SerializedValue'), }))), + me: tOptional(tArray(tObject({ + k: tType('SerializedValue'), + v: tType('SerializedValue'), + }))), + se: tOptional(tArray(tType('SerializedValue'))), h: tOptional(tInt), fn: tOptional(tString), id: tOptional(tInt), diff --git a/tests/page/page-evaluate-serialization.spec.ts b/tests/page/page-evaluate-serialization.spec.ts new file mode 100644 index 0000000000000..9d37afae9d27b --- /dev/null +++ b/tests/page/page-evaluate-serialization.spec.ts @@ -0,0 +1,104 @@ +/** + * 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 { test, expect } from './pageTest'; + +for (const target of ['page', 'frame'] as const) { + for (const world of ['main', 'utility'] as const) { + test(`should use extended serialization in ${target}.evaluate in ${world} world`, async ({ page }) => { + const evaluator = target === 'page' ? page : page.mainFrame(); + const value = { map: new Map([['key', new Set([1, 2])]]), empty: new Map(), set: new Set() }; + const result = await evaluator.evaluate(async value => { + if (!(value.map instanceof Map) || !(value.map.get('key') instanceof Set)) + throw new Error('Expected Map and Set arguments'); + value.map.get('key')!.add(3); + return value; + }, value, { serialization: 'extended', world }); + expect(result).toEqual({ map: new Map([['key', new Set([1, 2, 3])]]), empty: new Map(), set: new Set() }); + expect(await evaluator.evaluate(() => new Map([['key', 1]]), undefined, { serialization: 'default' })).toEqual({}); + }); + } + + test(`should use extended serialization in ${target}.evaluateHandle`, async ({ page }) => { + const evaluator = target === 'page' ? page : page.mainFrame(); + const value = new Map([['key', new Set([1, 2])]]); + const handle = await evaluator.evaluateHandle(value => value, value, { serialization: 'extended' }); + expect(await handle.evaluate(value => value instanceof Map && value.get('key') instanceof Set)).toBe(true); + expect(await handle.evaluate(value => value, undefined, { serialization: 'extended' })).toEqual(value); + expect(await handle.evaluate(value => value)).toEqual({}); + await handle.dispose(); + }); +} + +test('should preserve cycles and shared references with extended serialization', async ({ page }) => { + const key = { name: 'key' }; + const map = new Map(); + const set = new Set([key, map]); + map.set(key, set); + map.set(map, map); + set.add(set); + const result = await page.evaluate(value => value, { key, map, set }, { serialization: 'extended' }); + expect(result.map.get(result.key)).toBe(result.set); + expect(result.map.get(result.map)).toBe(result.map); + expect(result.set.has(result.set)).toBe(true); + expect(result.set.has(result.key)).toBe(true); + expect(result.set.has(result.map)).toBe(true); +}); + +test('should use extended serialization with handle evaluation arguments', async ({ page }) => { + const handle = await page.evaluateHandle(() => 42); + const value = new Map([['key', new Set([1, 2])]]); + expect(await handle.evaluate((n, value) => ({ n, value }), value, { serialization: 'extended' })).toEqual({ n: 42, value }); + const result = await handle.evaluateHandle((n, value) => ({ n, value }), value, { serialization: 'extended' }); + expect(await result.evaluate(value => value, undefined, { serialization: 'extended' })).toEqual({ n: 42, value }); + await result.dispose(); + await handle.dispose(); +}); + +test('should use extended serialization with element and locator evaluation', async ({ page }) => { + await page.setContent('
hello
'); + const value = new Map([['key', new Set([1, 2])]]); + const locator = page.locator('div'); + const element = await locator.elementHandle(); + expect(await element!.evaluate((e, value) => ({ text: e.textContent, value }), value, { serialization: 'extended' })).toEqual({ text: 'hello', value }); + for (const world of ['main', 'utility'] as const) + expect(await locator.evaluate((e, value) => ({ text: e.textContent, value }), value, { serialization: 'extended', world })).toEqual({ text: 'hello', value }); + const handle = await locator.evaluateHandle((e, value) => ({ text: e.textContent, value }), value, { serialization: 'extended' }); + expect(await handle.evaluate(value => value, undefined, { serialization: 'extended' })).toEqual({ text: 'hello', value }); + await handle.dispose(); + await element!.dispose(); +}); + +test('should support handles inside Map and Set arguments', async ({ page }) => { + const handle = await page.evaluateHandle(() => document.body); + const value = new Map([[handle, new Set([handle])]]); + expect(await page.evaluate(value => value.get(document.body)?.has(document.body), value, { serialization: 'extended' })).toBe(true); + await handle.dispose(); +}); + +test('should expose callbacks inside Map and Set arguments', async ({ page }) => { + const callback = async (value: number) => value * 2; + const result = await page.evaluate(async value => { + const [callback] = value.get('callbacks')!; + return new Map([['result', await callback(21)]]); + }, new Map([['callbacks', new Set([callback])]]), { serialization: 'extended', exposeFunctions: true }); + expect(result).toEqual(new Map([['result', 42]])); +}); + +test('should reject an unsupported serialization mode', async ({ page }) => { + // @ts-expect-error Invalid serialization mode. + await expect(page.evaluate(() => 42, undefined, { serialization: 'invalid' })).rejects.toThrow('serialization: expected one of (default|extended)'); +}); diff --git a/tests/playwright-test/playwright.mount.spec.ts b/tests/playwright-test/playwright.mount.spec.ts new file mode 100644 index 0000000000000..0927f92bad1b4 --- /dev/null +++ b/tests/playwright-test/playwright.mount.spec.ts @@ -0,0 +1,68 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * 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 { test, expect } from './playwright-test-fixtures'; + +test('mount and update should preserve Map and Set props with callbacks', async ({ runInlineTest, server }) => { + server.setRoute('/gallery.html', (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(` +
+ + `); + }); + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { use: { baseURL: '${server.PREFIX}/gallery.html' } }; + `, + 'mount.spec.ts': ` + import { test, expect } from '@playwright/test'; + + test('collections', async ({ mount }) => { + const received: string[] = []; + const callback = async (text: string) => { received.push(text); }; + const component = await mount('collections', { + values: new Map([['numbers', new Set([1, 2, 2])]]), + callbacks: new Map([['change', new Set([callback])]]), + }); + await expect(component).toHaveText('collections: 1, 2'); + expect(received).toEqual(['collections: 1, 2']); + + await component.update({ + values: new Map([['numbers', new Set([3, 4])]]), + callbacks: new Map([['change', new Set([callback])]]), + }); + await expect(component).toHaveText('collections: 3, 4'); + expect(received).toEqual(['collections: 1, 2', 'collections: 3, 4']); + }); + `, + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 001d1785a40e3..9783c4b090221 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, options?: { exposeFunctions?: boolean, world?: 'main'|'utility' }): Promise; - evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; - evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; - evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; addInitScript(script: PageFunction | { path?: string, content?: string }, arg?: Arg, options?: { exposeFunctions?: boolean }): Promise; @@ -85,11 +85,11 @@ export interface Page { } export interface Frame { - evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, world?: 'main'|'utility' }): Promise; - evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; - evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; - evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): 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, options?: { exposeFunctions?: boolean }): Promise; - evaluate(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean }): Promise; + evaluate(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise; + evaluate(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise; - evaluateHandle(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean }): Promise>; - evaluateHandle(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; jsonValue(): Promise; asElement(): T extends Node ? ElementHandle : null; @@ -194,8 +194,8 @@ export interface ElementHandle extends JSHandle { } export interface Locator { - evaluate(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean, world?: 'main'|'utility' }): Promise; - evaluateHandle(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean }): Promise>; + evaluate(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; + evaluateHandle(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; evaluateAll(pageFunction: PageFunctionOn, arg?: Arg, options?: { world?: 'main'|'utility' }): Promise; waitForFunction(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal }): Promise; elementHandle(options?: { timeout?: number, signal?: AbortSignal }): Promise>; diff --git a/utils/generate_types/test/test.ts b/utils/generate_types/test/test.ts index 3d098fc0f4931..e919c6c8baa7d 100644 --- a/utils/generate_types/test/test.ts +++ b/utils/generate_types/test/test.ts @@ -336,6 +336,18 @@ playwright.chromium.launch().then(async browser => { await browser.close(); })(); +// Extended serialization unboxes handles inside collections. +(async () => { + const body = await page.evaluateHandle(() => document.body); + const input = { map: new Map([[body, new Set([body])]]) }; + const value = await page.evaluate(({ map }) => { + const elements = map.get(document.body)!; + const assertion: AssertType, typeof elements> = true; + return new Set([...elements].map(element => element.tagName)); + }, input, { serialization: 'extended' }); + const assertion: AssertType, typeof value> = true; +})(); + // test $eval and $$eval (async () => { const browser = await playwright.firefox.launch(); @@ -1049,4 +1061,3 @@ import { Geolocation, HTTPCredentials, } from 'playwright'; - From 43dbd3cd0a17c607eb13f79c8f3ebac2eaa48799 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 14 Sep 2026 12:01:13 -0700 Subject: [PATCH 2/3] test(evaluate): keep basic extended serialization coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../page/page-evaluate-serialization.spec.ts | 90 ++++--------------- 1 file changed, 18 insertions(+), 72 deletions(-) diff --git a/tests/page/page-evaluate-serialization.spec.ts b/tests/page/page-evaluate-serialization.spec.ts index 9d37afae9d27b..be420f964c825 100644 --- a/tests/page/page-evaluate-serialization.spec.ts +++ b/tests/page/page-evaluate-serialization.spec.ts @@ -16,86 +16,32 @@ import { test, expect } from './pageTest'; -for (const target of ['page', 'frame'] as const) { - for (const world of ['main', 'utility'] as const) { - test(`should use extended serialization in ${target}.evaluate in ${world} world`, async ({ page }) => { - const evaluator = target === 'page' ? page : page.mainFrame(); - const value = { map: new Map([['key', new Set([1, 2])]]), empty: new Map(), set: new Set() }; - const result = await evaluator.evaluate(async value => { - if (!(value.map instanceof Map) || !(value.map.get('key') instanceof Set)) - throw new Error('Expected Map and Set arguments'); - value.map.get('key')!.add(3); - return value; - }, value, { serialization: 'extended', world }); - expect(result).toEqual({ map: new Map([['key', new Set([1, 2, 3])]]), empty: new Map(), set: new Set() }); - expect(await evaluator.evaluate(() => new Map([['key', 1]]), undefined, { serialization: 'default' })).toEqual({}); - }); - } - - test(`should use extended serialization in ${target}.evaluateHandle`, async ({ page }) => { - const evaluator = target === 'page' ? page : page.mainFrame(); - const value = new Map([['key', new Set([1, 2])]]); - const handle = await evaluator.evaluateHandle(value => value, value, { serialization: 'extended' }); - expect(await handle.evaluate(value => value instanceof Map && value.get('key') instanceof Set)).toBe(true); - expect(await handle.evaluate(value => value, undefined, { serialization: 'extended' })).toEqual(value); - expect(await handle.evaluate(value => value)).toEqual({}); - await handle.dispose(); - }); -} - -test('should preserve cycles and shared references with extended serialization', async ({ page }) => { - const key = { name: 'key' }; - const map = new Map(); - const set = new Set([key, map]); - map.set(key, set); - map.set(map, map); - set.add(set); - const result = await page.evaluate(value => value, { key, map, set }, { serialization: 'extended' }); - expect(result.map.get(result.key)).toBe(result.set); - expect(result.map.get(result.map)).toBe(result.map); - expect(result.set.has(result.set)).toBe(true); - expect(result.set.has(result.key)).toBe(true); - expect(result.set.has(result.map)).toBe(true); +test('should use extended serialization in evaluate', async ({ page }) => { + const value = new Map([['key', new Set([1, 2])]]); + const result = await page.evaluate(value => { + if (!(value instanceof Map) || !(value.get('key') instanceof Set)) + throw new Error('Expected Map and Set arguments'); + value.get('key')!.add(3); + return value; + }, value, { serialization: 'extended' }); + expect(result).toEqual(new Map([['key', new Set([1, 2, 3])]])); + expect(await page.evaluate(value => value, value, { serialization: 'default' })).toEqual({}); }); -test('should use extended serialization with handle evaluation arguments', async ({ page }) => { - const handle = await page.evaluateHandle(() => 42); +test('should use extended serialization with handles', async ({ page }) => { const value = new Map([['key', new Set([1, 2])]]); - expect(await handle.evaluate((n, value) => ({ n, value }), value, { serialization: 'extended' })).toEqual({ n: 42, value }); - const result = await handle.evaluateHandle((n, value) => ({ n, value }), value, { serialization: 'extended' }); - expect(await result.evaluate(value => value, undefined, { serialization: 'extended' })).toEqual({ n: 42, value }); - await result.dispose(); + const handle = await page.evaluateHandle(value => value, value, { serialization: 'extended' }); + expect(await handle.evaluate(value => value instanceof Map && value.get('key') instanceof Set)).toBe(true); + expect(await handle.evaluate((value, set) => ({ value, set }), new Set([3]), { serialization: 'extended' })).toEqual({ value, set: new Set([3]) }); + expect(await handle.evaluate(value => value)).toEqual({}); await handle.dispose(); }); -test('should use extended serialization with element and locator evaluation', async ({ page }) => { +test('should use extended serialization with locators', async ({ page }) => { await page.setContent('
hello
'); const value = new Map([['key', new Set([1, 2])]]); - const locator = page.locator('div'); - const element = await locator.elementHandle(); - expect(await element!.evaluate((e, value) => ({ text: e.textContent, value }), value, { serialization: 'extended' })).toEqual({ text: 'hello', value }); - for (const world of ['main', 'utility'] as const) - expect(await locator.evaluate((e, value) => ({ text: e.textContent, value }), value, { serialization: 'extended', world })).toEqual({ text: 'hello', value }); - const handle = await locator.evaluateHandle((e, value) => ({ text: e.textContent, value }), value, { serialization: 'extended' }); - expect(await handle.evaluate(value => value, undefined, { serialization: 'extended' })).toEqual({ text: 'hello', value }); - await handle.dispose(); - await element!.dispose(); -}); - -test('should support handles inside Map and Set arguments', async ({ page }) => { - const handle = await page.evaluateHandle(() => document.body); - const value = new Map([[handle, new Set([handle])]]); - expect(await page.evaluate(value => value.get(document.body)?.has(document.body), value, { serialization: 'extended' })).toBe(true); - await handle.dispose(); -}); - -test('should expose callbacks inside Map and Set arguments', async ({ page }) => { - const callback = async (value: number) => value * 2; - const result = await page.evaluate(async value => { - const [callback] = value.get('callbacks')!; - return new Map([['result', await callback(21)]]); - }, new Map([['callbacks', new Set([callback])]]), { serialization: 'extended', exposeFunctions: true }); - expect(result).toEqual(new Map([['result', 42]])); + const result = await page.locator('div').evaluate((element, value) => ({ text: element.textContent, value }), value, { serialization: 'extended' }); + expect(result).toEqual({ text: 'hello', value }); }); test('should reject an unsupported serialization mode', async ({ page }) => { From 6a92fdbabf477e81e914efd2ac64b6f6aa4f489a Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 15 Sep 2026 15:00:12 -0700 Subject: [PATCH 3/3] feat(evaluate): opt in to serialization by built-in type Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/src/api/class-frame.md | 4 +- docs/src/api/class-jshandle.md | 4 +- docs/src/api/class-locator.md | 4 +- docs/src/api/class-page.md | 4 +- docs/src/api/params.md | 6 +- packages/injected/src/storageScript.ts | 2 +- packages/injected/src/utilityScript.ts | 12 ++-- .../isomorphic/utilityScriptSerializers.ts | 58 +++++++++---------- packages/playwright-client/types/types.d.ts | 28 ++++----- .../playwright-core/src/client/channels.d.ts | 16 ++--- packages/playwright-core/src/client/frame.ts | 4 +- .../playwright-core/src/client/jsHandle.ts | 14 ++--- .../playwright-core/src/server/channels.d.ts | 16 ++--- .../dispatchers/elementHandlerDispatcher.ts | 2 +- .../src/server/dispatchers/frameDispatcher.ts | 4 +- .../server/dispatchers/jsHandleDispatcher.ts | 6 +- packages/playwright-core/src/server/dom.ts | 10 ++-- packages/playwright-core/src/server/frames.ts | 8 +-- .../playwright-core/src/server/javascript.ts | 12 ++-- packages/playwright-core/types/types.d.ts | 28 ++++----- packages/playwright/src/index.ts | 2 +- packages/protocol/spec/frame.yml | 16 ++++- packages/protocol/spec/handles.yml | 16 ++++- packages/protocol/src/serializers.ts | 10 ++-- packages/protocol/src/validator.ts | 8 +-- .../page/page-evaluate-serialization.spec.ts | 35 +++++++---- utils/generate_types/overrides.d.ts | 28 ++++----- utils/generate_types/test/test.ts | 4 +- 28 files changed, 198 insertions(+), 163 deletions(-) diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index 68ebd90946e8f..791640f1b86ca 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -754,7 +754,7 @@ Optional argument to pass to [`param: expression`]. ### option: Frame.evaluate.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 -### option: Frame.evaluate.serialization = %%-js-evaluate-serialization-%% +### option: Frame.evaluate.serialize = %%-js-evaluate-serialize-%% * since: v1.64 ### option: Frame.evaluate.world = %%-js-evaluate-world-%% @@ -875,7 +875,7 @@ Optional argument to pass to [`param: expression`]. ### option: Frame.evaluateHandle.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 -### option: Frame.evaluateHandle.serialization = %%-js-evaluate-serialization-%% +### option: Frame.evaluateHandle.serialize = %%-js-evaluate-serialize-%% * since: v1.64 ## async method: Frame.fill diff --git a/docs/src/api/class-jshandle.md b/docs/src/api/class-jshandle.md index 0f0b446bdc53a..9910841d11874 100644 --- a/docs/src/api/class-jshandle.md +++ b/docs/src/api/class-jshandle.md @@ -99,7 +99,7 @@ Optional argument to pass to [`param: expression`]. ### option: JSHandle.evaluate.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 -### option: JSHandle.evaluate.serialization = %%-js-evaluate-serialization-%% +### option: JSHandle.evaluate.serialize = %%-js-evaluate-serialize-%% * since: v1.64 ## async method: JSHandle.evaluateHandle @@ -132,7 +132,7 @@ Optional argument to pass to [`param: expression`]. ### option: JSHandle.evaluateHandle.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 -### option: JSHandle.evaluateHandle.serialization = %%-js-evaluate-serialization-%% +### option: JSHandle.evaluateHandle.serialize = %%-js-evaluate-serialize-%% * since: v1.64 ## async method: JSHandle.getProperties diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index 7fce10d00515c..cf6ac55c65b55 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -1181,7 +1181,7 @@ Optional argument to pass to [`param: expression`]. ### option: Locator.evaluate.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 -### option: Locator.evaluate.serialization = %%-js-evaluate-serialization-%% +### option: Locator.evaluate.serialize = %%-js-evaluate-serialize-%% * since: v1.64 ### option: Locator.evaluate.world = %%-js-evaluate-world-%% @@ -1292,7 +1292,7 @@ Optional argument to pass to [`param: expression`]. ### option: Locator.evaluateHandle.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 -### option: Locator.evaluateHandle.serialization = %%-js-evaluate-serialization-%% +### option: Locator.evaluateHandle.serialize = %%-js-evaluate-serialize-%% * since: v1.64 ### option: Locator.evaluateHandle.timeout diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 83e1c7066d817..750f8efc87ba0 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -1605,7 +1605,7 @@ Optional argument to pass to [`param: expression`]. ### option: Page.evaluate.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 -### option: Page.evaluate.serialization = %%-js-evaluate-serialization-%% +### option: Page.evaluate.serialize = %%-js-evaluate-serialize-%% * since: v1.64 ### option: Page.evaluate.world = %%-js-evaluate-world-%% @@ -1723,7 +1723,7 @@ Optional argument to pass to [`param: expression`]. ### option: Page.evaluateHandle.exposeFunctions = %%-js-evaluate-expose-functions-%% * since: v1.62 -### option: Page.evaluateHandle.serialization = %%-js-evaluate-serialization-%% +### option: Page.evaluateHandle.serialize = %%-js-evaluate-serialize-%% * since: v1.64 ## async method: Page.exposeBinding diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 24bb5a0502fa0..8cb88d79eb570 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -584,11 +584,11 @@ to a function, the function is automatically invoked. Function to be evaluated in the page context. -## js-evaluate-serialization +## js-evaluate-serialize * langs: js -- `serialization` <[Serialization]<"default"|"extended">> +- `serialize` <[Array]<[SerializationType]<"Map"|"Set">>> -When set to `"extended"`, preserves additional built-in types in the evaluation argument and return value. Currently supports [Map] and [Set], including nested collections and circular references. Defaults to `"default"`. +Additional built-in types to preserve in the evaluation argument and return value, including nested collections and circular references. Supports `"Map"` and `"Set"`. For example, `serialize: ['Map', 'Set']` preserves both [Map] and [Set] instances. Defaults to an empty array, in which case these types are serialized as plain objects. ## js-evaluate-expose-functions * langs: js diff --git a/packages/injected/src/storageScript.ts b/packages/injected/src/storageScript.ts index f7e4d3780923f..bab7f1895a23a 100644 --- a/packages/injected/src/storageScript.ts +++ b/packages/injected/src/storageScript.ts @@ -77,7 +77,7 @@ export class StorageScript { trivial = false; return { fallThrough: v }; - }, { extendedSerialization: true }); + }, { serialize: ['Map', 'Set'] }); if (trivial) return { trivial: value }; return { encoded }; diff --git a/packages/injected/src/utilityScript.ts b/packages/injected/src/utilityScript.ts index a39085159f6a9..e4cfe21b7ba73 100644 --- a/packages/injected/src/utilityScript.ts +++ b/packages/injected/src/utilityScript.ts @@ -63,7 +63,7 @@ export class UtilityScript { (global as any).builtins = this.builtins; } - evaluate(isFunction: boolean | undefined, returnByValue: boolean, extendedSerialization: boolean | undefined, expression: string, argCount: number, ...argsAndHandles: any[]) { + evaluate(isFunction: boolean | undefined, returnByValue: boolean, serialize: ('Map' | 'Set')[] | undefined, expression: string, argCount: number, ...argsAndHandles: any[]) { const args = argsAndHandles.slice(0, argCount); const handles = argsAndHandles.slice(argCount); const parameters = []; @@ -80,20 +80,20 @@ export class UtilityScript { if (typeof result === 'function') result = result(...parameters); } - return returnByValue ? this._promiseAwareJsonValueNoThrow(result, extendedSerialization) : result; + return returnByValue ? this._promiseAwareJsonValueNoThrow(result, serialize) : result; } - jsonValue(returnByValue: true, value: any, extendedSerialization?: boolean) { + jsonValue(returnByValue: true, value: any, serialize?: ('Map' | 'Set')[]) { // Special handling of undefined to work-around multi-step returnByValue handling in WebKit. if (value === undefined) return undefined; - return serializeAsCallArgument(value, (value: any) => ({ fallThrough: value }), { extendedSerialization }); + return serializeAsCallArgument(value, (value: any) => ({ fallThrough: value }), { serialize }); } - private _promiseAwareJsonValueNoThrow(value: any, extendedSerialization?: boolean) { + private _promiseAwareJsonValueNoThrow(value: any, serialize?: ('Map' | 'Set')[]) { const safeJson = (value: any) => { try { - return this.jsonValue(true, value, extendedSerialization); + return this.jsonValue(true, value, serialize); } catch (e) { return undefined; } diff --git a/packages/isomorphic/utilityScriptSerializers.ts b/packages/isomorphic/utilityScriptSerializers.ts index 7132bc86266a0..6027e77e1f5a7 100644 --- a/packages/isomorphic/utilityScriptSerializers.ts +++ b/packages/isomorphic/utilityScriptSerializers.ts @@ -46,7 +46,7 @@ type HandleOrValue = { h: number } | { fn: string } | { fallThrough: any }; type VisitorInfo = { visited: Map; lastId: number; - extendedSerialization?: boolean; + serialize?: ('Map' | 'Set')[]; }; function isRegExp(obj: any): obj is RegExp { @@ -231,8 +231,8 @@ export function parseEvaluationResultValue(value: SerializedValue, handles: any[ return value; } -export function serializeAsCallArgument(value: any, handleSerializer: (value: any) => HandleOrValue, options: { extendedSerialization?: boolean } = {}): SerializedValue { - return serialize(value, handleSerializer, { visited: new Map(), lastId: 0, extendedSerialization: options.extendedSerialization }); +export function serializeAsCallArgument(value: any, handleSerializer: (value: any) => HandleOrValue, options: { serialize?: ('Map' | 'Set')[] } = {}): SerializedValue { + return serialize(value, handleSerializer, { visited: new Map(), lastId: 0, serialize: options.serialize }); } function serialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue { @@ -308,35 +308,33 @@ function innerSerialize(value: any, handleSerializer: (value: any) => HandleOrVa if (id) return { ref: id }; - if (visitorInfo.extendedSerialization) { - if (isMap(value)) { - const m: { k: SerializedValue, v: SerializedValue }[] = []; - const id = ++visitorInfo.lastId; - visitorInfo.visited.set(value, id); - const iterator = value.entries(); - const next = new Map().entries().next; - while (true) { - const entry = next.call(iterator); - if (entry.done) - break; - m.push({ k: serialize(entry.value[0], handleSerializer, visitorInfo), v: serialize(entry.value[1], handleSerializer, visitorInfo) }); - } - return { m, id }; + if (visitorInfo.serialize?.includes('Map') && isMap(value)) { + const m: { k: SerializedValue, v: SerializedValue }[] = []; + const id = ++visitorInfo.lastId; + visitorInfo.visited.set(value, id); + const iterator = value.entries(); + const next = new Map().entries().next; + while (true) { + const entry = next.call(iterator); + if (entry.done) + break; + m.push({ k: serialize(entry.value[0], handleSerializer, visitorInfo), v: serialize(entry.value[1], handleSerializer, visitorInfo) }); } - if (isSet(value)) { - const s: SerializedValue[] = []; - const id = ++visitorInfo.lastId; - visitorInfo.visited.set(value, id); - const iterator = value.values(); - const next = new Set().values().next; - while (true) { - const entry = next.call(iterator); - if (entry.done) - break; - s.push(serialize(entry.value, handleSerializer, visitorInfo)); - } - return { s, id }; + return { m, id }; + } + if (visitorInfo.serialize?.includes('Set') && isSet(value)) { + const s: SerializedValue[] = []; + const id = ++visitorInfo.lastId; + visitorInfo.visited.set(value, id); + const iterator = value.values(); + const next = new Set().values().next; + while (true) { + const entry = next.call(iterator); + if (entry.done) + break; + s.push(serialize(entry.value, handleSerializer, visitorInfo)); } + return { s, id }; } if (Array.isArray(value)) { diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 1723fb9df3eac..52d6c19acdea3 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -134,7 +134,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[], world?: 'main'|'utility' }): Promise; /** * Returns the value of the * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression) invocation. @@ -187,7 +187,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[], world?: 'main'|'utility' }): Promise; /** * Returns the value of the @@ -235,7 +235,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; /** * Returns the value of the * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression) invocation as a @@ -282,7 +282,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; /** * Adds a script which would be evaluated in one of the following scenarios: @@ -5963,7 +5963,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[], world?: 'main'|'utility' }): Promise; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). @@ -6012,7 +6012,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[], world?: 'main'|'utility' }): Promise; /** * Returns the return value of @@ -6062,7 +6062,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression) as a @@ -6111,7 +6111,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; /** * **NOTE** Use locator-based [frame.locator(selector[, options])](https://playwright.dev/docs/api/class-frame#frame-locator) @@ -12035,7 +12035,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise; + evaluate(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). @@ -12058,7 +12058,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise; + evaluate(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise; /** * Returns the return value of @@ -12082,7 +12082,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression) @@ -12105,7 +12105,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; /** * Returns a JSON representation of the object. If the object has a `toJSON` function, it **will not be called**. @@ -14300,7 +14300,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, exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean, serialize?: ('Map'|'Set')[], world?: 'main'|'utility' }): 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. @@ -14334,7 +14334,7 @@ export interface Locator { * [`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, exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; /** * Execute JavaScript code in the page, taking all matching elements as an argument. * diff --git a/packages/playwright-core/src/client/channels.d.ts b/packages/playwright-core/src/client/channels.d.ts index 4419751fc4bbb..44359aaba96cd 100644 --- a/packages/playwright-core/src/client/channels.d.ts +++ b/packages/playwright-core/src/client/channels.d.ts @@ -2479,12 +2479,12 @@ export type FrameEvaluateExpressionParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], world?: 'main' | 'utility', }; export type FrameEvaluateExpressionOptions = { isFunction?: boolean, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], world?: 'main' | 'utility', }; export type FrameEvaluateExpressionResult = { @@ -2494,11 +2494,11 @@ export type FrameEvaluateExpressionHandleParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], }; export type FrameEvaluateExpressionHandleOptions = { isFunction?: boolean, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], }; export type FrameEvaluateExpressionHandleResult = { handle: JSHandleChannel, @@ -2942,12 +2942,12 @@ export type JSHandleEvaluateExpressionParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], world?: 'main' | 'utility', }; export type JSHandleEvaluateExpressionOptions = { isFunction?: boolean, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], world?: 'main' | 'utility', }; export type JSHandleEvaluateExpressionResult = { @@ -2957,11 +2957,11 @@ export type JSHandleEvaluateExpressionHandleParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], }; export type JSHandleEvaluateExpressionHandleOptions = { isFunction?: boolean, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], }; export type JSHandleEvaluateExpressionHandleResult = { handle: JSHandleChannel, diff --git a/packages/playwright-core/src/client/frame.ts b/packages/playwright-core/src/client/frame.ts index 77664b2938424..bcaef87659c4c 100644 --- a/packages/playwright-core/src/client/frame.ts +++ b/packages/playwright-core/src/client/frame.ts @@ -211,7 +211,7 @@ export class Frame extends ChannelOwner implements api.Fr assertMaxArguments(arguments.length, 3); assertEvaluateOptions(options); const serializedArg = options?.exposeFunctions ? await serializeArgumentWithCallbacks(this, this._page, arg, options) : serializeArgument(arg, undefined, options); - const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg, extendedSerialization: options?.serialization === 'extended' }, kNoTimeout); + const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg, serialize: options?.serialize }, kNoTimeout); return JSHandle.from(result.handle) as any as structs.SmartHandle; } @@ -219,7 +219,7 @@ export class Frame extends ChannelOwner implements api.Fr assertMaxArguments(arguments.length, 3); assertEvaluateOptions(options); const serializedArg = options?.exposeFunctions ? await serializeArgumentWithCallbacks(this, this._page, arg, options) : serializeArgument(arg, undefined, options); - const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg, world: options?.world, extendedSerialization: options?.serialization === 'extended' }, kNoTimeout); + const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg, world: options?.world, serialize: options?.serialize }, kNoTimeout); return parseResult(result.value); } diff --git a/packages/playwright-core/src/client/jsHandle.ts b/packages/playwright-core/src/client/jsHandle.ts index 48dc8d41a9b98..362652d908f45 100644 --- a/packages/playwright-core/src/client/jsHandle.ts +++ b/packages/playwright-core/src/client/jsHandle.ts @@ -42,20 +42,20 @@ export class JSHandle extends ChannelOwner im async evaluate(pageFunction: structs.PageFunctionOn, arg?: Arg, options?: EvaluateHandleOptions): Promise { assertEvaluateOptions(options); - return await this._evaluate(pageFunction, arg, { exposeFunctions: options?.exposeFunctions, serialization: options?.serialization }); + return await this._evaluate(pageFunction, arg, { exposeFunctions: options?.exposeFunctions, serialize: options?.serialize }); } 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, options) : serializeArgument(arg, undefined, options); - const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg, world: options?.world, extendedSerialization: options?.serialization === 'extended' }, kNoTimeout); + const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg, world: options?.world, serialize: options?.serialize }, kNoTimeout); return parseResult(result.value); } async evaluateHandle(pageFunction: structs.PageFunctionOn, arg?: Arg, options?: EvaluateHandleOptions): Promise> { assertEvaluateOptions(options); const serializedArg = options?.exposeFunctions ? await serializeArgumentWithCallbacks(this, this._parentOfType('Page') as Page | undefined, arg, options) : serializeArgument(arg, undefined, options); - const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg, extendedSerialization: options?.serialization === 'extended' }, kNoTimeout); + const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: serializedArg, serialize: options?.serialize }, kNoTimeout); return JSHandle.from(result.handle) as any as structs.SmartHandle; } @@ -112,13 +112,13 @@ export function serializeArgument(arg: any, registerCallback?: (callback: Functi if (typeof value === 'function' && registerCallback) return { fn: registerCallback(value as Function) }; return { fallThrough: value }; - }, { extendedSerialization: options?.serialization === 'extended' }); + }, options); return { value, handles }; } export type WorldOptions = { world?: 'main' | 'utility' }; export type ExposeFunctionsOptions = { exposeFunctions?: boolean }; -export type SerializationOptions = { serialization?: 'default' | 'extended' }; +export type SerializationOptions = { serialize?: ('Map' | 'Set')[] }; export type EvaluateHandleOptions = ExposeFunctionsOptions & SerializationOptions; export type EvaluateOptions = EvaluateHandleOptions & WorldOptions; @@ -149,6 +149,6 @@ export function assertMaxArguments(count: number, max: number): asserts count { 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.'); - if (options?.serialization !== undefined && options.serialization !== 'default' && options.serialization !== 'extended') - throw new Error('serialization: expected one of (default|extended)'); + if (options?.serialize !== undefined && (!Array.isArray(options.serialize) || options.serialize.some((type: unknown) => type !== 'Map' && type !== 'Set'))) + throw new Error('serialize: expected an array of (Map|Set)'); } diff --git a/packages/playwright-core/src/server/channels.d.ts b/packages/playwright-core/src/server/channels.d.ts index 5e60915b6d8d8..73afab004d6e2 100644 --- a/packages/playwright-core/src/server/channels.d.ts +++ b/packages/playwright-core/src/server/channels.d.ts @@ -2480,12 +2480,12 @@ export type FrameEvaluateExpressionParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], world?: 'main' | 'utility', }; export type FrameEvaluateExpressionOptions = { isFunction?: boolean, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], world?: 'main' | 'utility', }; export type FrameEvaluateExpressionResult = { @@ -2495,11 +2495,11 @@ export type FrameEvaluateExpressionHandleParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], }; export type FrameEvaluateExpressionHandleOptions = { isFunction?: boolean, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], }; export type FrameEvaluateExpressionHandleResult = { handle: JSHandleChannel, @@ -2943,12 +2943,12 @@ export type JSHandleEvaluateExpressionParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], world?: 'main' | 'utility', }; export type JSHandleEvaluateExpressionOptions = { isFunction?: boolean, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], world?: 'main' | 'utility', }; export type JSHandleEvaluateExpressionResult = { @@ -2958,11 +2958,11 @@ export type JSHandleEvaluateExpressionHandleParams = { expression: string, isFunction?: boolean, arg: SerializedArgument, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], }; export type JSHandleEvaluateExpressionHandleOptions = { isFunction?: boolean, - extendedSerialization?: boolean, + serialize?: ('Map' | 'Set')[], }; export type JSHandleEvaluateExpressionHandleResult = { handle: JSHandleChannel, diff --git a/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts b/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts index ee964792261db..a410f90e778a6 100644 --- a/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/elementHandlerDispatcher.ts @@ -56,7 +56,7 @@ export class ElementHandleDispatcher extends JSHandleDispatcher } override async evaluateExpression(params: channels.JSHandleEvaluateExpressionParams, progress: Progress): Promise { - const value = await this._elementHandle.evaluateExpression(progress, params.expression, { isFunction: params.isFunction, world: params.world, extendedSerialization: params.extendedSerialization }, parseArgument(params.arg)); + const value = await this._elementHandle.evaluateExpression(progress, params.expression, { isFunction: params.isFunction, world: params.world, serialize: params.serialize }, parseArgument(params.arg)); return { value: serializeResult(value, params) }; } diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index 4c436e2ab237a..7733df73fb44e 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -82,11 +82,11 @@ export class FrameDispatcher extends Dispatcher { - return { value: serializeResult(await this._frame.evaluateExpression(progress, params.expression, { isFunction: params.isFunction, world: params.world, extendedSerialization: params.extendedSerialization }, parseArgument(params.arg)), params) }; + return { value: serializeResult(await this._frame.evaluateExpression(progress, params.expression, { isFunction: params.isFunction, world: params.world, serialize: params.serialize }, parseArgument(params.arg)), params) }; } async evaluateExpressionHandle(params: channels.FrameEvaluateExpressionHandleParams, progress: Progress): Promise { - return { handle: ElementHandleDispatcher.fromJSOrElementHandle(this, await this._frame.evaluateExpressionHandle(progress, params.expression, { isFunction: params.isFunction, extendedSerialization: params.extendedSerialization }, parseArgument(params.arg))) }; + return { handle: ElementHandleDispatcher.fromJSOrElementHandle(this, await this._frame.evaluateExpressionHandle(progress, params.expression, { isFunction: params.isFunction, serialize: params.serialize }, parseArgument(params.arg))) }; } async waitForSelector(params: channels.FrameWaitForSelectorParams, progress: Progress): Promise { diff --git a/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts b/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts index 9957c5c591df8..6c489f87904e3 100644 --- a/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/jsHandleDispatcher.ts @@ -45,12 +45,12 @@ export class JSHandleDispatcher { if (params.world) throw new Error(`Only element handles can be evaluated in the "${params.world}" world`); - const jsHandle = await this._object.evaluateExpression(progress, params.expression, { isFunction: params.isFunction, extendedSerialization: params.extendedSerialization }, parseArgument(params.arg)); + const jsHandle = await this._object.evaluateExpression(progress, params.expression, { isFunction: params.isFunction, serialize: params.serialize }, parseArgument(params.arg)); return { value: serializeResult(jsHandle, params) }; } async evaluateExpressionHandle(params: channels.JSHandleEvaluateExpressionHandleParams, progress: Progress): Promise { - const jsHandle = await this._object.evaluateExpressionHandle(progress, params.expression, { isFunction: params.isFunction, extendedSerialization: params.extendedSerialization }, parseArgument(params.arg)); + const jsHandle = await this._object.evaluateExpressionHandle(progress, params.expression, { isFunction: params.isFunction, serialize: params.serialize }, parseArgument(params.arg)); // If "jsHandle" is an ElementHandle, it belongs to the same frame as "this". return { handle: ElementHandleDispatcher.fromJSOrElementHandle(this.parentScope() as FrameDispatcher, jsHandle) }; } @@ -91,6 +91,6 @@ export function parseValue(v: channels.SerializedValue): any { return parseSerializedValue(v, []); } -export function serializeResult(arg: any, options?: { extendedSerialization?: boolean }): channels.SerializedValue { +export function serializeResult(arg: any, options?: { serialize?: ('Map' | 'Set')[] }): channels.SerializedValue { return serializeValue(arg, value => ({ fallThrough: value }), options); } diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 33cd0a4bf6f0e..c90aa05331964 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -73,11 +73,11 @@ export class FrameExecutionContext extends js.ExecutionContext { return js.evaluate(this, false /* returnByValue */, pageFunction, arg); } - async evaluateExpression(expression: string, options: { isFunction?: boolean, extendedSerialization?: boolean }, arg?: any): Promise { + async evaluateExpression(expression: string, options: { isFunction?: boolean, serialize?: ('Map' | 'Set')[] }, arg?: any): Promise { return js.evaluateExpression(this, expression, { ...options, returnByValue: true }, arg); } - async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, extendedSerialization?: boolean }, arg?: any): Promise> { + async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, serialize?: ('Map' | 'Set')[] }, arg?: any): Promise> { return js.evaluateExpression(this, expression, { ...options, returnByValue: false }, arg); } @@ -869,13 +869,13 @@ export class ElementHandle extends js.JSHandle { return this._frame.selectors.queryAll(selector, this); } - override async evaluateExpression(progress: Progress, expression: string, options: { isFunction?: boolean, world?: types.World, extendedSerialization?: boolean }, arg: any): Promise { + override async evaluateExpression(progress: Progress, expression: string, options: { isFunction?: boolean, world?: types.World, serialize?: ('Map' | 'Set')[] }, arg: any): Promise { return await progress.race(this.internalEvaluateExpression(expression, options, arg)); } - override async internalEvaluateExpression(expression: string, options: { isFunction?: boolean, world?: types.World, extendedSerialization?: boolean }, arg: any): Promise { + override async internalEvaluateExpression(expression: string, options: { isFunction?: boolean, world?: types.World, serialize?: ('Map' | 'Set')[] }, arg: any): Promise { const context = options.world ? await this._frame.context(options.world) : this._context; - return await js.evaluateExpression(context, expression, { isFunction: options.isFunction, extendedSerialization: options.extendedSerialization, returnByValue: true }, this, arg); + return await js.evaluateExpression(context, expression, { isFunction: options.isFunction, serialize: options.serialize, returnByValue: true }, this, arg); } async evalOnSelector(progress: Progress, selector: string, strict: boolean, expression: string, options: { isFunction?: boolean, world?: types.World }, arg: any): Promise { diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 657b8bdf433f2..fb2d901d3ba77 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -808,21 +808,21 @@ export class Frame extends SdkObject { return this.context('utility'); } - async evaluateExpression(progress: Progress, expression: string, options: { isFunction?: boolean, world?: types.World, extendedSerialization?: boolean } = {}, arg?: any): Promise { + async evaluateExpression(progress: Progress, expression: string, options: { isFunction?: boolean, world?: types.World, serialize?: ('Map' | 'Set')[] } = {}, arg?: any): Promise { return await progress.race(this._evaluateExpression(expression, options, arg)); } - private async _evaluateExpression(expression: string, options: { isFunction?: boolean, world?: types.World, extendedSerialization?: boolean } = {}, arg?: any): Promise { + private async _evaluateExpression(expression: string, options: { isFunction?: boolean, world?: types.World, serialize?: ('Map' | 'Set')[] } = {}, arg?: any): Promise { const context = await this.context(options.world ?? 'main'); const value = await context.evaluateExpression(expression, options, arg); return value; } - async evaluateExpressionHandle(progress: Progress, expression: string, options: { isFunction?: boolean, world?: types.World, extendedSerialization?: boolean } = {}, arg?: any): Promise> { + async evaluateExpressionHandle(progress: Progress, expression: string, options: { isFunction?: boolean, world?: types.World, serialize?: ('Map' | 'Set')[] } = {}, arg?: any): Promise> { return await progress.race(this._evaluateExpressionHandle(expression, options, arg)); } - private async _evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, world?: types.World, extendedSerialization?: boolean } = {}, arg?: any): Promise> { + private async _evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, world?: types.World, serialize?: ('Map' | 'Set')[] } = {}, arg?: any): Promise> { const context = await this.context(options.world ?? 'main'); const value = await context.evaluateExpressionHandle(expression, options, arg); return value; diff --git a/packages/playwright-core/src/server/javascript.ts b/packages/playwright-core/src/server/javascript.ts index 8467bbd45503d..420d4ec669c0f 100644 --- a/packages/playwright-core/src/server/javascript.ts +++ b/packages/playwright-core/src/server/javascript.ts @@ -143,11 +143,11 @@ export class JSHandle extends SdkObject { (globalThis as any).leakedJSHandles.set(this, new Error('Leaked JSHandle')); } - async evaluateExpression(progress: Progress, expression: string, options: { isFunction?: boolean, extendedSerialization?: boolean }, arg: any) { + async evaluateExpression(progress: Progress, expression: string, options: { isFunction?: boolean, serialize?: ('Map' | 'Set')[] }, arg: any) { return await progress.race(this.internalEvaluateExpression(expression, options, arg)); } - async evaluateExpressionHandle(progress: Progress, expression: string, options: { isFunction?: boolean, extendedSerialization?: boolean }, arg: any): Promise> { + async evaluateExpressionHandle(progress: Progress, expression: string, options: { isFunction?: boolean, serialize?: ('Map' | 'Set')[] }, arg: any): Promise> { return await progress.race(this._evaluateExpressionHandle(expression, options, arg)); } @@ -171,11 +171,11 @@ export class JSHandle extends SdkObject { return evaluate(this._context, false /* returnByValue */, pageFunction, this, arg); } - async internalEvaluateExpression(expression: string, options: { isFunction?: boolean, extendedSerialization?: boolean }, arg: any) { + async internalEvaluateExpression(expression: string, options: { isFunction?: boolean, serialize?: ('Map' | 'Set')[] }, arg: any) { return await evaluateExpression(this._context, expression, { ...options, returnByValue: true }, this, arg); } - private async _evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, extendedSerialization?: boolean }, arg: any): Promise> { + private async _evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, serialize?: ('Map' | 'Set')[] }, arg: any): Promise> { return await evaluateExpression(this._context, expression, { ...options, returnByValue: false }, this, arg); } @@ -254,7 +254,7 @@ export async function evaluate(context: ExecutionContext, returnByValue: boolean return evaluateExpression(context, String(pageFunction), { returnByValue, isFunction: typeof pageFunction === 'function' }, ...args); } -export async function evaluateExpression(context: ExecutionContext, expression: string, options: { returnByValue?: boolean, isFunction?: boolean, extendedSerialization?: boolean }, ...args: any[]): Promise { +export async function evaluateExpression(context: ExecutionContext, expression: string, options: { returnByValue?: boolean, isFunction?: boolean, serialize?: ('Map' | 'Set')[] }, ...args: any[]): Promise { expression = normalizeEvaluationExpression(expression, options.isFunction); const handles: (Promise)[] = []; const toDispose: Promise[] = []; @@ -286,7 +286,7 @@ export async function evaluateExpression(context: ExecutionContext, expression: } // See UtilityScript for arguments. - const utilityScriptValues = [options.isFunction, options.returnByValue, options.extendedSerialization, expression, args.length, ...args]; + const utilityScriptValues = [options.isFunction, options.returnByValue, options.serialize, expression, args.length, ...args]; const script = `(utilityScript, ...args) => utilityScript.evaluate(...args)`; try { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 1723fb9df3eac..52d6c19acdea3 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -134,7 +134,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[], world?: 'main'|'utility' }): Promise; /** * Returns the value of the * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression) invocation. @@ -187,7 +187,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[], world?: 'main'|'utility' }): Promise; /** * Returns the value of the @@ -235,7 +235,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; /** * Returns the value of the * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression) invocation as a @@ -282,7 +282,7 @@ export interface Page { * [`pageFunction`](https://playwright.dev/docs/api/class-page#page-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; /** * Adds a script which would be evaluated in one of the following scenarios: @@ -5963,7 +5963,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[], world?: 'main'|'utility' }): Promise; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). @@ -6012,7 +6012,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[], world?: 'main'|'utility' }): Promise; /** * Returns the return value of @@ -6062,7 +6062,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression) as a @@ -6111,7 +6111,7 @@ export interface Frame { * [`pageFunction`](https://playwright.dev/docs/api/class-frame#frame-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; /** * **NOTE** Use locator-based [frame.locator(selector[, options])](https://playwright.dev/docs/api/class-frame#frame-locator) @@ -12035,7 +12035,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise; + evaluate(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). @@ -12058,7 +12058,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-option-expression). * @param options */ - evaluate(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise; + evaluate(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise; /** * Returns the return value of @@ -12082,7 +12082,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; /** * Returns the return value of * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression) @@ -12105,7 +12105,7 @@ export interface JSHandle { * [`pageFunction`](https://playwright.dev/docs/api/class-elementhandle#element-handle-evaluate-handle-option-expression). * @param options */ - evaluateHandle(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; /** * Returns a JSON representation of the object. If the object has a `toJSON` function, it **will not be called**. @@ -14300,7 +14300,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, exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean, serialize?: ('Map'|'Set')[], world?: 'main'|'utility' }): 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. @@ -14334,7 +14334,7 @@ export interface Locator { * [`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, exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; /** * Execute JavaScript code in the page, taking all matching elements as an argument. * diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index ba3af8a2bfd3d..9eb21ca724346 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -508,7 +508,7 @@ const playwrightFixtures: Fixtures { if (!baseURL) throw new Error('mount() requires `baseURL` to point at the component gallery. Set it in your Playwright config.'); diff --git a/packages/protocol/spec/frame.yml b/packages/protocol/spec/frame.yml index f69f7606c4b9f..3b34d5207eae8 100644 --- a/packages/protocol/spec/frame.yml +++ b/packages/protocol/spec/frame.yml @@ -337,7 +337,13 @@ Frame: expression: string isFunction: boolean? arg: SerializedArgument - extendedSerialization: boolean? + serialize: + type: array? + items: + type: enum + literals: + - Map + - Set world: type: enum? literals: @@ -355,7 +361,13 @@ Frame: expression: string isFunction: boolean? arg: SerializedArgument - extendedSerialization: boolean? + serialize: + type: array? + items: + type: enum + literals: + - Map + - Set returns: handle: JSHandle flags: diff --git a/packages/protocol/spec/handles.yml b/packages/protocol/spec/handles.yml index c1fd5cb98943b..15e17c3ecf6fc 100644 --- a/packages/protocol/spec/handles.yml +++ b/packages/protocol/spec/handles.yml @@ -29,7 +29,13 @@ JSHandle: expression: string isFunction: boolean? arg: SerializedArgument - extendedSerialization: boolean? + serialize: + type: array? + items: + type: enum + literals: + - Map + - Set world: type: enum? literals: @@ -47,7 +53,13 @@ JSHandle: expression: string isFunction: boolean? arg: SerializedArgument - extendedSerialization: boolean? + serialize: + type: array? + items: + type: enum + literals: + - Map + - Set returns: handle: JSHandle flags: diff --git a/packages/protocol/src/serializers.ts b/packages/protocol/src/serializers.ts index c45dff898af7c..ef59330e6eac8 100644 --- a/packages/protocol/src/serializers.ts +++ b/packages/protocol/src/serializers.ts @@ -142,11 +142,11 @@ export type HandleOrValue = { h: number } | { fn: string } | { fallThrough: any type VisitorInfo = { visited: Map; lastId: number; - extendedSerialization?: boolean; + serialize?: ('Map' | 'Set')[]; }; -export function serializeValue(value: any, handleSerializer: (value: any) => HandleOrValue, options: { extendedSerialization?: boolean } = {}): SerializedValue { - return innerSerializeValue(value, handleSerializer, { lastId: 0, visited: new Map(), extendedSerialization: options.extendedSerialization }, []); +export function serializeValue(value: any, handleSerializer: (value: any) => HandleOrValue, options: { serialize?: ('Map' | 'Set')[] } = {}): SerializedValue { + return innerSerializeValue(value, handleSerializer, { lastId: 0, visited: new Map(), serialize: options.serialize }, []); } export function serializePlainValue(arg: any): SerializedValue { @@ -199,7 +199,7 @@ function innerSerializeValue(value: any, handleSerializer: (value: any) => Handl if (id) return { ref: id }; - if (visitorInfo.extendedSerialization && value instanceof Map) { + if (visitorInfo.serialize?.includes('Map') && value instanceof Map) { const me: { k: SerializedValue, v: SerializedValue }[] = []; const id = ++visitorInfo.lastId; visitorInfo.visited.set(value, id); @@ -207,7 +207,7 @@ function innerSerializeValue(value: any, handleSerializer: (value: any) => Handl me.push({ k: innerSerializeValue(k, handleSerializer, visitorInfo, accessChain), v: innerSerializeValue(v, handleSerializer, visitorInfo, accessChain) }); return { me, id }; } - if (visitorInfo.extendedSerialization && value instanceof Set) { + if (visitorInfo.serialize?.includes('Set') && value instanceof Set) { const se: SerializedValue[] = []; const id = ++visitorInfo.lastId; visitorInfo.visited.set(value, id); diff --git a/packages/protocol/src/validator.ts b/packages/protocol/src/validator.ts index 408f65c4d443b..bfd12b33258ab 100644 --- a/packages/protocol/src/validator.ts +++ b/packages/protocol/src/validator.ts @@ -1349,7 +1349,7 @@ scheme.FrameEvaluateExpressionParams = tObject({ expression: tString, isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), - extendedSerialization: tOptional(tBoolean), + serialize: tOptional(tArray(tEnum(['Map', 'Set']))), world: tOptional(tEnum(['main', 'utility'])), }); scheme.FrameEvaluateExpressionResult = tObject({ @@ -1359,7 +1359,7 @@ scheme.FrameEvaluateExpressionHandleParams = tObject({ expression: tString, isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), - extendedSerialization: tOptional(tBoolean), + serialize: tOptional(tArray(tEnum(['Map', 'Set']))), }); scheme.FrameEvaluateExpressionHandleResult = tObject({ handle: tChannel(['ElementHandle', 'JSHandle']), @@ -1641,7 +1641,7 @@ scheme.JSHandleEvaluateExpressionParams = tObject({ expression: tString, isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), - extendedSerialization: tOptional(tBoolean), + serialize: tOptional(tArray(tEnum(['Map', 'Set']))), world: tOptional(tEnum(['main', 'utility'])), }); scheme.ElementHandleEvaluateExpressionParams = tType('JSHandleEvaluateExpressionParams'); @@ -1653,7 +1653,7 @@ scheme.JSHandleEvaluateExpressionHandleParams = tObject({ expression: tString, isFunction: tOptional(tBoolean), arg: tType('SerializedArgument'), - extendedSerialization: tOptional(tBoolean), + serialize: tOptional(tArray(tEnum(['Map', 'Set']))), }); scheme.ElementHandleEvaluateExpressionHandleParams = tType('JSHandleEvaluateExpressionHandleParams'); scheme.JSHandleEvaluateExpressionHandleResult = tObject({ diff --git a/tests/page/page-evaluate-serialization.spec.ts b/tests/page/page-evaluate-serialization.spec.ts index be420f964c825..1c1f7e63b9b6b 100644 --- a/tests/page/page-evaluate-serialization.spec.ts +++ b/tests/page/page-evaluate-serialization.spec.ts @@ -16,35 +16,48 @@ import { test, expect } from './pageTest'; -test('should use extended serialization in evaluate', async ({ page }) => { +test('should serialize selected types in evaluate', async ({ page }) => { const value = new Map([['key', new Set([1, 2])]]); const result = await page.evaluate(value => { if (!(value instanceof Map) || !(value.get('key') instanceof Set)) throw new Error('Expected Map and Set arguments'); value.get('key')!.add(3); return value; - }, value, { serialization: 'extended' }); + }, value, { serialize: ['Map', 'Set'] }); expect(result).toEqual(new Map([['key', new Set([1, 2, 3])]])); - expect(await page.evaluate(value => value, value, { serialization: 'default' })).toEqual({}); + expect(await page.evaluate(value => value, value, { serialize: [] })).toEqual({}); + const collections = { map: new Map([['key', 1]]), set: new Set([2]) }; + for (const type of ['Map', 'Set'] as const) { + const result = await page.evaluate(value => ({ + map: value.map instanceof Map, + set: value.set instanceof Set, + value, + created: { map: new Map([['key', 1]]), set: new Set([2]) }, + }), collections, { serialize: [type] }); + const expected = { map: type === 'Map' ? collections.map : {}, set: type === 'Set' ? collections.set : {} }; + expect(result).toEqual({ map: type === 'Map', set: type === 'Set', value: expected, created: expected }); + } }); -test('should use extended serialization with handles', async ({ page }) => { +test('should serialize Map and Set with handles', async ({ page }) => { const value = new Map([['key', new Set([1, 2])]]); - const handle = await page.evaluateHandle(value => value, value, { serialization: 'extended' }); + const handle = await page.evaluateHandle(value => value, value, { serialize: ['Map', 'Set'] }); expect(await handle.evaluate(value => value instanceof Map && value.get('key') instanceof Set)).toBe(true); - expect(await handle.evaluate((value, set) => ({ value, set }), new Set([3]), { serialization: 'extended' })).toEqual({ value, set: new Set([3]) }); + expect(await handle.evaluate((value, set) => ({ value, set }), new Set([3]), { serialize: ['Map', 'Set'] })).toEqual({ value, set: new Set([3]) }); expect(await handle.evaluate(value => value)).toEqual({}); await handle.dispose(); }); -test('should use extended serialization with locators', async ({ page }) => { +test('should serialize Map and Set with locators', async ({ page }) => { await page.setContent('
hello
'); const value = new Map([['key', new Set([1, 2])]]); - const result = await page.locator('div').evaluate((element, value) => ({ text: element.textContent, value }), value, { serialization: 'extended' }); + const result = await page.locator('div').evaluate((element, value) => ({ text: element.textContent, value }), value, { serialize: ['Map', 'Set'] }); expect(result).toEqual({ text: 'hello', value }); }); -test('should reject an unsupported serialization mode', async ({ page }) => { - // @ts-expect-error Invalid serialization mode. - await expect(page.evaluate(() => 42, undefined, { serialization: 'invalid' })).rejects.toThrow('serialization: expected one of (default|extended)'); +test('should reject invalid serialization types', async ({ page }) => { + // @ts-expect-error Invalid serialization type. + await expect(page.evaluate(() => 42, undefined, { serialize: ['Blob'] })).rejects.toThrow('serialize: expected an array of (Map|Set)'); + // @ts-expect-error Serialization types must be an array. + await expect(page.evaluate(() => 42, undefined, { serialize: 'Map' })).rejects.toThrow('serialize: expected an array of (Map|Set)'); }); diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 9783c4b090221..dd853e7c3f64a 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, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; - evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[], world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[], world?: 'main'|'utility' }): Promise; - evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; - evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; addInitScript(script: PageFunction | { path?: string, content?: string }, arg?: Arg, options?: { exposeFunctions?: boolean }): Promise; @@ -85,11 +85,11 @@ export interface Page { } export interface Frame { - evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; - evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[], world?: 'main'|'utility' }): Promise; + evaluate(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[], world?: 'main'|'utility' }): Promise; - evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; - evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; + evaluateHandle(pageFunction: PageFunction, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): 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, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise; - evaluate(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise; + evaluate(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise; + evaluate(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise; - evaluateHandle(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; - evaluateHandle(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg: Arg, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; + evaluateHandle(pageFunction: PageFunctionOn, arg?: any, options?: { exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; jsonValue(): Promise; asElement(): T extends Node ? ElementHandle : null; @@ -194,8 +194,8 @@ export interface ElementHandle extends JSHandle { } export interface Locator { - evaluate(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean, serialization?: 'default'|'extended', world?: 'main'|'utility' }): Promise; - evaluateHandle(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean, serialization?: 'default'|'extended' }): Promise>; + evaluate(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean, serialize?: ('Map'|'Set')[], world?: 'main'|'utility' }): Promise; + evaluateHandle(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal, exposeFunctions?: boolean, serialize?: ('Map'|'Set')[] }): Promise>; evaluateAll(pageFunction: PageFunctionOn, arg?: Arg, options?: { world?: 'main'|'utility' }): Promise; waitForFunction(pageFunction: PageFunctionOn, arg?: Arg, options?: { timeout?: number, signal?: AbortSignal }): Promise; elementHandle(options?: { timeout?: number, signal?: AbortSignal }): Promise>; diff --git a/utils/generate_types/test/test.ts b/utils/generate_types/test/test.ts index e919c6c8baa7d..68069e258bb14 100644 --- a/utils/generate_types/test/test.ts +++ b/utils/generate_types/test/test.ts @@ -336,7 +336,7 @@ playwright.chromium.launch().then(async browser => { await browser.close(); })(); -// Extended serialization unboxes handles inside collections. +// Collection serialization unboxes handles inside collections. (async () => { const body = await page.evaluateHandle(() => document.body); const input = { map: new Map([[body, new Set([body])]]) }; @@ -344,7 +344,7 @@ playwright.chromium.launch().then(async browser => { const elements = map.get(document.body)!; const assertion: AssertType, typeof elements> = true; return new Set([...elements].map(element => element.tagName)); - }, input, { serialization: 'extended' }); + }, input, { serialize: ['Map', 'Set'] }); const assertion: AssertType, typeof value> = true; })();