chore: provide more actionable error messages when serializing values (#35297)

This commit is contained in:
Adam Gastineau 2025-03-25 10:32:44 -07:00 committed by GitHub
parent 6f1ec11072
commit 714d5d1b1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 42 additions and 11 deletions

View File

@ -17,10 +17,10 @@
import type { SerializedValue } from '@protocol/channels';
export function parseSerializedValue(value: SerializedValue, handles: any[] | undefined): any {
return innerParseSerializedValue(value, handles, new Map());
return innerParseSerializedValue(value, handles, new Map(), []);
}
function innerParseSerializedValue(value: SerializedValue, handles: any[] | undefined, refs: Map<number, object>): any {
function innerParseSerializedValue(value: SerializedValue, handles: any[] | undefined, refs: Map<number, object>, accessChain: Array<string | number>): any {
if (value.ref !== undefined)
return refs.get(value.ref);
if (value.n !== undefined)
@ -61,15 +61,15 @@ function innerParseSerializedValue(value: SerializedValue, handles: any[] | unde
if (value.a !== undefined) {
const result: any[] = [];
refs.set(value.id!, result);
for (const v of value.a)
result.push(innerParseSerializedValue(v, handles, refs));
for (let i = 0; i < value.a.length; i++)
result.push(innerParseSerializedValue(value.a[i], handles, refs, [...accessChain, i]));
return result;
}
if (value.o !== undefined) {
const result: any = {};
refs.set(value.id!, result);
for (const { k, v } of value.o)
result[k] = innerParseSerializedValue(v, handles, refs);
result[k] = innerParseSerializedValue(v, handles, refs, [...accessChain, k]);
return result;
}
if (value.h !== undefined) {
@ -77,7 +77,7 @@ function innerParseSerializedValue(value: SerializedValue, handles: any[] | unde
throw new Error('Unexpected handle');
return handles[value.h];
}
throw new Error('Unexpected value');
throw new Error(`Attempting to deserialize unexpected value${accessChainToDisplayString(accessChain)}: ${value}`);
}
export type HandleOrValue = { h: number } | { fallThrough: any };
@ -87,10 +87,10 @@ type VisitorInfo = {
};
export function serializeValue(value: any, handleSerializer: (value: any) => HandleOrValue): SerializedValue {
return innerSerializeValue(value, handleSerializer, { lastId: 0, visited: new Map() });
return innerSerializeValue(value, handleSerializer, { lastId: 0, visited: new Map() }, []);
}
function innerSerializeValue(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue {
function innerSerializeValue(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo, accessChain: Array<string | number>): SerializedValue {
const handle = handleSerializer(value);
if ('fallThrough' in handle)
value = handle.fallThrough;
@ -137,7 +137,7 @@ function innerSerializeValue(value: any, handleSerializer: (value: any) => Handl
const id = ++visitorInfo.lastId;
visitorInfo.visited.set(value, id);
for (let i = 0; i < value.length; ++i)
a.push(innerSerializeValue(value[i], handleSerializer, visitorInfo));
a.push(innerSerializeValue(value[i], handleSerializer, visitorInfo, [...accessChain, i]));
return { a, id };
}
if (typeof value === 'object') {
@ -145,10 +145,21 @@ function innerSerializeValue(value: any, handleSerializer: (value: any) => Handl
const id = ++visitorInfo.lastId;
visitorInfo.visited.set(value, id);
for (const name of Object.keys(value))
o.push({ k: name, v: innerSerializeValue(value[name], handleSerializer, visitorInfo) });
o.push({ k: name, v: innerSerializeValue(value[name], handleSerializer, visitorInfo, [...accessChain, name]) });
return { o, id };
}
throw new Error('Unexpected value');
// Likely only functions can reach here.
throw new Error(`Attempting to serialize unexpected value${accessChainToDisplayString(accessChain)}: ${value}`);
}
function accessChainToDisplayString(accessChain: Array<string | number>): string {
const chainString = accessChain.map((accessor, i) => {
if (typeof accessor === 'string')
return i ? `.${accessor}` : accessor;
return `[${accessor}]`;
}).join('');
return chainString.length > 0 ? ` at position "${chainString}"` : '';
}
function isRegExp(obj: any): obj is RegExp {

View File

@ -416,6 +416,26 @@ it('should throw for too deep reference chain', {
: 'Cannot serialize result: object reference chain is too long.');
});
it('should throw usable message for unserializable shallow function', async ({ page }) => {
await expect(() => page.evaluate(arg => arg, () => { }))
.rejects.toThrow(/Attempting to serialize unexpected value: \(\) => {}/);
});
it('should throw usable message for unserializable object one deep function', async ({ page }) => {
await expect(() => page.evaluate(arg => arg, { aProperty: () => { } }))
.rejects.toThrow(/Attempting to serialize unexpected value at position "aProperty": \(\) => {}/);
});
it('should throw usable message for unserializable object nested function', async ({ page }) => {
await expect(() => page.evaluate(arg => arg, { a: { inner: { property: () => { } } } }))
.rejects.toThrow(/Attempting to serialize unexpected value at position "a\.inner\.property": \(\) => {}/);
});
it('should throw usable message for unserializable array nested function', async ({ page }) => {
await expect(() => page.evaluate(arg => arg, { a: { inner: ['firstValue', { property: () => { } }] } }))
.rejects.toThrow(/Attempting to serialize unexpected value at position "a\.inner\[1\]\.property": \(\) => {}/);
});
it('should alias Window, Document and Node', async ({ page }) => {
const object = await page.evaluate('[window, document, document.body]');
expect(object).toEqual(['ref: <Window>', 'ref: <Document>', 'ref: <Node>']);