mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-07 03:39:48 +03:00
feat(bindings): best-effort serialize circular structures (#14008)
This commit is contained in:
parent
1658172b2c
commit
e55f7bd896
@ -23,19 +23,23 @@ export type SerializedValue =
|
||||
{ o: { k: string, v: SerializedValue }[] } |
|
||||
{ h: number };
|
||||
|
||||
function isRegExp(obj: any): obj is RegExp {
|
||||
export type HandleOrValue = { h: number } | { fallThrough: any };
|
||||
|
||||
export function source(aliasComplexAndCircularObjects: boolean = false) {
|
||||
|
||||
function isRegExp(obj: any): obj is RegExp {
|
||||
return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]';
|
||||
}
|
||||
}
|
||||
|
||||
function isDate(obj: any): obj is Date {
|
||||
function isDate(obj: any): obj is Date {
|
||||
return obj instanceof Date || Object.prototype.toString.call(obj) === '[object Date]';
|
||||
}
|
||||
}
|
||||
|
||||
function isError(obj: any): obj is Error {
|
||||
function isError(obj: any): obj is Error {
|
||||
return obj instanceof Error || (obj && obj.__proto__ && obj.__proto__.name === 'Error');
|
||||
}
|
||||
}
|
||||
|
||||
export function parseEvaluationResultValue(value: SerializedValue, handles: any[] = []): any {
|
||||
function parseEvaluationResultValue(value: SerializedValue, handles: any[] = []): any {
|
||||
if (Object.is(value, undefined))
|
||||
return undefined;
|
||||
if (typeof value === 'object' && value) {
|
||||
@ -70,22 +74,48 @@ export function parseEvaluationResultValue(value: SerializedValue, handles: any[
|
||||
return handles[value.h];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export type HandleOrValue = { h: number } | { fallThrough: any };
|
||||
export function serializeAsCallArgument(value: any, handleSerializer: (value: any) => HandleOrValue): SerializedValue {
|
||||
function serializeAsCallArgument(value: any, handleSerializer: (value: any) => HandleOrValue): SerializedValue {
|
||||
return serialize(value, handleSerializer, new Set());
|
||||
}
|
||||
}
|
||||
|
||||
function serialize(value: any, handleSerializer: (value: any) => HandleOrValue, visited: Set<any>): SerializedValue {
|
||||
function serialize(value: any, handleSerializer: (value: any) => HandleOrValue, visited: Set<any>): SerializedValue {
|
||||
if (!aliasComplexAndCircularObjects)
|
||||
return innerSerialize(value, handleSerializer, visited);
|
||||
try {
|
||||
const alias = serializeComplexObjectAsAlias(value);
|
||||
return alias || innerSerialize(value, handleSerializer, visited);
|
||||
} catch (error) {
|
||||
return error.stack;
|
||||
}
|
||||
}
|
||||
|
||||
function serializeComplexObjectAsAlias(value: any): string | undefined {
|
||||
if (value && typeof value === 'object') {
|
||||
if (globalThis.Window && value instanceof globalThis.Window)
|
||||
return 'ref: <Window>';
|
||||
if (globalThis.Document && value instanceof globalThis.Document)
|
||||
return 'ref: <Document>';
|
||||
if (globalThis.Node && value instanceof globalThis.Node)
|
||||
return 'ref: <Node>';
|
||||
}
|
||||
}
|
||||
|
||||
function innerSerialize(value: any, handleSerializer: (value: any) => HandleOrValue, visited: Set<any>): SerializedValue {
|
||||
const result = handleSerializer(value);
|
||||
if ('fallThrough' in result)
|
||||
value = result.fallThrough;
|
||||
else
|
||||
return result;
|
||||
|
||||
if (visited.has(value))
|
||||
if (visited.has(value)) {
|
||||
if (aliasComplexAndCircularObjects) {
|
||||
const alias = serializeComplexObjectAsAlias(value);
|
||||
return alias || '[Circular Ref]';
|
||||
}
|
||||
throw new Error('Argument is a circular structure');
|
||||
}
|
||||
if (typeof value === 'symbol')
|
||||
return { v: 'undefined' };
|
||||
if (Object.is(value, undefined))
|
||||
@ -148,4 +178,11 @@ function serialize(value: any, handleSerializer: (value: any) => HandleOrValue,
|
||||
visited.delete(value);
|
||||
return { o };
|
||||
}
|
||||
}
|
||||
|
||||
return { parseEvaluationResultValue, serializeAsCallArgument };
|
||||
}
|
||||
|
||||
const result = source();
|
||||
export const parseEvaluationResultValue = result.parseEvaluationResultValue;
|
||||
export const serializeAsCallArgument = result.serializeAsCallArgument;
|
||||
|
@ -42,6 +42,8 @@ import type { Artifact } from './artifact';
|
||||
import type { TimeoutOptions } from '../common/types';
|
||||
import type { ParsedSelector } from './isomorphic/selectorParser';
|
||||
import { isInvalidSelectorError } from './isomorphic/selectorParser';
|
||||
import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSerializers';
|
||||
import type { SerializedValue } from './isomorphic/utilityScriptSerializers';
|
||||
|
||||
export interface PageDelegate {
|
||||
readonly rawMouse: input.RawMouse;
|
||||
@ -712,6 +714,12 @@ export class Worker extends SdkObject {
|
||||
}
|
||||
}
|
||||
|
||||
type BindingPayload = {
|
||||
name: string;
|
||||
seq: number;
|
||||
serializedArgs?: SerializedValue[],
|
||||
};
|
||||
|
||||
export class PageBinding {
|
||||
readonly name: string;
|
||||
readonly playwrightFunction: frames.FunctionWithSource;
|
||||
@ -721,12 +729,12 @@ export class PageBinding {
|
||||
constructor(name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean) {
|
||||
this.name = name;
|
||||
this.playwrightFunction = playwrightFunction;
|
||||
this.source = `(${addPageBinding.toString()})(${JSON.stringify(name)}, ${needsHandle})`;
|
||||
this.source = `(${addPageBinding.toString()})(${JSON.stringify(name)}, ${needsHandle}, (${source})(true))`;
|
||||
this.needsHandle = needsHandle;
|
||||
}
|
||||
|
||||
static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) {
|
||||
const { name, seq, args } = JSON.parse(payload);
|
||||
const { name, seq, serializedArgs } = JSON.parse(payload) as BindingPayload;
|
||||
try {
|
||||
assert(context.world);
|
||||
const binding = page.getBinding(name)!;
|
||||
@ -735,6 +743,7 @@ export class PageBinding {
|
||||
const handle = await context.evaluateHandle(takeHandle, { name, seq }).catch(e => null);
|
||||
result = await binding.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, handle);
|
||||
} else {
|
||||
const args = serializedArgs!.map(a => parseEvaluationResultValue(a, []));
|
||||
result = await binding.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, ...args);
|
||||
}
|
||||
context.evaluate(deliverResult, { name, seq, result }).catch(e => debugLogger.log('error', e));
|
||||
@ -770,7 +779,7 @@ export class PageBinding {
|
||||
}
|
||||
}
|
||||
|
||||
function addPageBinding(bindingName: string, needsHandle: boolean) {
|
||||
function addPageBinding(bindingName: string, needsHandle: boolean, utilityScriptSerializers: ReturnType<typeof source>) {
|
||||
const binding = (globalThis as any)[bindingName];
|
||||
if (binding.__installed)
|
||||
return;
|
||||
@ -783,7 +792,7 @@ function addPageBinding(bindingName: string, needsHandle: boolean) {
|
||||
callbacks = new Map();
|
||||
me['callbacks'] = callbacks;
|
||||
}
|
||||
const seq = (me['lastSeq'] || 0) + 1;
|
||||
const seq: number = (me['lastSeq'] || 0) + 1;
|
||||
me['lastSeq'] = seq;
|
||||
let handles = me['handles'];
|
||||
if (!handles) {
|
||||
@ -791,12 +800,17 @@ function addPageBinding(bindingName: string, needsHandle: boolean) {
|
||||
me['handles'] = handles;
|
||||
}
|
||||
const promise = new Promise((resolve, reject) => callbacks.set(seq, { resolve, reject }));
|
||||
let payload: BindingPayload;
|
||||
if (needsHandle) {
|
||||
handles.set(seq, args[0]);
|
||||
binding(JSON.stringify({ name: bindingName, seq }));
|
||||
payload = { name: bindingName, seq };
|
||||
} else {
|
||||
binding(JSON.stringify({ name: bindingName, seq, args }));
|
||||
const serializedArgs = args.map(a => utilityScriptSerializers.serializeAsCallArgument(a, v => {
|
||||
return { fallThrough: v };
|
||||
}));
|
||||
payload = { name: bindingName, seq, serializedArgs };
|
||||
}
|
||||
binding(JSON.stringify(payload));
|
||||
return promise;
|
||||
};
|
||||
(globalThis as any)[bindingName].__installed = true;
|
||||
|
@ -284,3 +284,19 @@ it('should retain internal binding after reset', async ({ page }) => {
|
||||
await (page as any)._removeExposedBindings();
|
||||
expect(await page.evaluate('__pw_add(5, 6)')).toBe(11);
|
||||
});
|
||||
|
||||
it('should alias Window, Document and Node', async ({ page }) => {
|
||||
let object: any;
|
||||
await page.exposeBinding('log', (source, obj) => object = obj);
|
||||
await page.evaluate('window.log([window, document, document.body])');
|
||||
expect(object).toEqual(['ref: <Window>', 'ref: <Document>', 'ref: <Node>']);
|
||||
});
|
||||
|
||||
it('should trim cycles', async ({ page }) => {
|
||||
let object: any;
|
||||
await page.exposeBinding('log', (source, obj) => object = obj);
|
||||
await page.evaluate('const a = { a: 1 }; a.a = a; window.log(a)');
|
||||
expect(object).toEqual({
|
||||
a: '[Circular Ref]',
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user