From ee203b782ccc82ec688a7973a0c5ffc87aa636df Mon Sep 17 00:00:00 2001 From: Sander Date: Mon, 28 Aug 2023 23:32:41 +0200 Subject: [PATCH] feat(evaluate): serialize map and set (#26730) closes: https://github.com/microsoft/playwright/issues/24040 --- .../src/protocol/serializers.ts | 16 ++++++++++++++++ .../playwright-core/src/protocol/validator.ts | 2 ++ .../isomorphic/utilityScriptSerializers.ts | 19 +++++++++++++++++++ packages/protocol/src/channels.ts | 2 ++ packages/protocol/src/protocol.yml | 4 ++++ tests/page/page-evaluate.spec.ts | 11 ++++++++--- 6 files changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/playwright-core/src/protocol/serializers.ts b/packages/playwright-core/src/protocol/serializers.ts index d43747237f..3e4134d62c 100644 --- a/packages/playwright-core/src/protocol/serializers.ts +++ b/packages/playwright-core/src/protocol/serializers.ts @@ -75,6 +75,10 @@ function innerParseSerializedValue(value: SerializedValue, handles: any[] | unde return BigInt(value.bi); if (value.r !== undefined) return new RegExp(value.r.p, value.r.f); + if (value.m !== undefined) + return new Map(innerParseSerializedValue(value.m, handles, refs)); + if (value.se !== undefined) + return new Set(innerParseSerializedValue(value.se, handles, refs)); if (value.a !== undefined) { const result: any[] = []; @@ -145,6 +149,10 @@ function innerSerializeValue(value: any, handleSerializer: (value: any) => Handl } return { s: `${error.name}: ${error.message}\n${error.stack}` }; } + if (isMap(value)) + return { m: innerSerializeValue(Array.from(value), handleSerializer, visitorInfo) }; + if (isSet(value)) + return { se: innerSerializeValue(Array.from(value), handleSerializer, visitorInfo) }; if (isDate(value)) return { d: value.toJSON() }; if (isURL(value)) @@ -175,6 +183,14 @@ function innerSerializeValue(value: any, handleSerializer: (value: any) => Handl throw new Error('Unexpected value'); } +function isMap(obj: any): obj is Map { + return obj instanceof Map || Object.prototype.toString.call(obj) === '[object Map]'; +} + +function isSet(obj: any): obj is Set { + return obj instanceof Set || Object.prototype.toString.call(obj) === '[object Set]'; +} + function isRegExp(obj: any): obj is RegExp { return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]'; } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 33b280a52f..c05a349a56 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -58,6 +58,8 @@ scheme.SerializedValue = tObject({ d: tOptional(tString), u: tOptional(tString), bi: tOptional(tString), + m: tOptional(tType('SerializedValue')), + se: tOptional(tType('SerializedValue')), r: tOptional(tObject({ p: tString, f: tString, diff --git a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts index 6ba427eb2b..0a49410110 100644 --- a/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts +++ b/packages/playwright-core/src/server/isomorphic/utilityScriptSerializers.ts @@ -20,6 +20,8 @@ export type SerializedValue = { d: string } | { u: string } | { bi: string } | + { m: SerializedValue } | + { se: SerializedValue } | { r: { p: string, f: string} } | { a: SerializedValue[], id: number } | { o: { k: string, v: SerializedValue }[], id: number } | @@ -35,6 +37,14 @@ type VisitorInfo = { export function source() { + function isMap(obj: any): obj is Map { + return obj instanceof Map || Object.prototype.toString.call(obj) === '[object Map]'; + } + + function isSet(obj: any): obj is Set { + return obj instanceof Set || Object.prototype.toString.call(obj) === '[object Set]'; + } + function isRegExp(obj: any): obj is RegExp { try { return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]'; @@ -94,6 +104,10 @@ export function source() { return new URL(value.u); if ('bi' in value) return BigInt(value.bi); + if ('m' in value) + return new Map(parseEvaluationResultValue(value.m)); + if ('se' in value) + return new Set(parseEvaluationResultValue(value.se)); if ('r' in value) return new RegExp(value.r.p, value.r.f); if ('a' in value) { @@ -163,6 +177,11 @@ export function source() { if (typeof value === 'bigint') return { bi: value.toString() }; + if (isMap(value)) + return { m: serialize(Array.from(value), handleSerializer, visitorInfo) }; + if (isSet(value)) + return { se: serialize(Array.from(value), handleSerializer, visitorInfo) }; + if (isError(value)) { const error = value; if (error.stack?.startsWith(error.name + ': ' + error.message)) { diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index d5fff0b580..9e7abcbd1c 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -180,6 +180,8 @@ export type SerializedValue = { d?: string, u?: string, bi?: string, + m?: SerializedValue, + se?: SerializedValue, r?: { p: string, f: string, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 0ebaed4a88..de3e8686c5 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -82,6 +82,10 @@ SerializedValue: u: string? # String representation of BigInt. bi: string? + # JS representation of Map: [[key1, value1], [key2, value2], ...]. + m: SerializedValue? + # JS representation of Set: [item1, item2, ...]. + se: SerializedValue? # Regular expression pattern and flags. r: type: object? diff --git a/tests/page/page-evaluate.spec.ts b/tests/page/page-evaluate.spec.ts index db1120d6fe..2f3d7f18d9 100644 --- a/tests/page/page-evaluate.spec.ts +++ b/tests/page/page-evaluate.spec.ts @@ -99,9 +99,14 @@ it('should transfer bigint', async ({ page }) => { expect(await page.evaluate(a => a, 17n)).toBe(17n); }); -it('should transfer maps as empty objects', async ({ page }) => { - const result = await page.evaluate(a => a.x.constructor.name + ' ' + JSON.stringify(a.x), { x: new Map([[1, 2]]) }); - expect(result).toBe('Object {}'); +it('should transfer maps', async ({ page }) => { + expect(await page.evaluate(() => new Map([[1, { test: 42n }]]))).toEqual(new Map([[1, { test: 42n }]])); + expect(await page.evaluate(a => a, new Map([[1, { test: 17n }]]))).toEqual(new Map([[1, { test: 17n }]])); +}); + +it('should transfer sets', async ({ page }) => { + expect(await page.evaluate(() => new Set([1, { test: 42n }]))).toEqual(new Set([1, { test: 42n }])); + expect(await page.evaluate(a => a, new Set([1, { test: 17n }]))).toEqual(new Set([1, { test: 17n }])); }); it('should modify global environment', async ({ page }) => {