add encoded versions

This commit is contained in:
Simon Knott 2025-02-05 15:02:18 +01:00
parent 311625b891
commit f4dcfd68b3
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
8 changed files with 72 additions and 16 deletions

View file

@ -1527,7 +1527,9 @@ Whether to emulate network being offline for the browser context.
- `multiEntry` <[boolean]> - `multiEntry` <[boolean]>
- `records` <[Array]<[Object]>> - `records` <[Array]<[Object]>>
- `key` ?<[Object]> - `key` ?<[Object]>
- `keyEncoded` ?<[Object]> if `key` is not JSON-serializable, this contains an encoded version that preserves types.
- `value` <[Object]> - `value` <[Object]>
- `valueEncoded` ?<[Object]> if `value` is not JSON-serializable, this contains an encoded version that preserves types.
Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot. Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.

View file

@ -152,7 +152,9 @@ scheme.IndexedDBDatabase = tObject({
keyPathArray: tOptional(tArray(tString)), keyPathArray: tOptional(tArray(tString)),
records: tArray(tObject({ records: tArray(tObject({
key: tOptional(tAny), key: tOptional(tAny),
value: tAny, keyEncoded: tOptional(tAny),
value: tOptional(tAny),
valueEncoded: tOptional(tAny),
})), })),
indexes: tArray(tObject({ indexes: tArray(tObject({
name: tString, name: tString,

View file

@ -44,6 +44,7 @@ import { Clock } from './clock';
import type { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; import type { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
import { RecorderApp } from './recorder/recorderApp'; import { RecorderApp } from './recorder/recorderApp';
import * as storageScript from './storageScript'; import * as storageScript from './storageScript';
import * as utilityScriptSerializers from './isomorphic/utilityScriptSerializers';
export abstract class BrowserContext extends SdkObject { export abstract class BrowserContext extends SdkObject {
static Events = { static Events = {
@ -520,7 +521,7 @@ export abstract class BrowserContext extends SdkObject {
if (!origin || !originsToSave.has(origin)) if (!origin || !originsToSave.has(origin))
continue; continue;
try { try {
const storage: storageScript.Storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`(${storageScript.collect})()`, 'utility'); const storage: storageScript.Storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`(${storageScript.collect})((${utilityScriptSerializers.source})())`, 'utility');
if (storage.localStorage.length || storage.indexedDB.length) if (storage.localStorage.length || storage.indexedDB.length)
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB }); result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB });
originsToSave.delete(origin); originsToSave.delete(origin);
@ -540,7 +541,7 @@ export abstract class BrowserContext extends SdkObject {
for (const origin of originsToSave) { for (const origin of originsToSave) {
const frame = page.mainFrame(); const frame = page.mainFrame();
await frame.goto(internalMetadata, origin); await frame.goto(internalMetadata, origin);
const storage: Awaited<ReturnType<typeof storageScript.collect>> = await frame.evaluateExpression(`(${storageScript.collect})()`, { world: 'utility' }); const storage: Awaited<ReturnType<typeof storageScript.collect>> = await frame.evaluateExpression(`(${storageScript.collect})((${utilityScriptSerializers.source})())`, { world: 'utility' });
if (storage.localStorage.length || storage.indexedDB.length) if (storage.localStorage.length || storage.indexedDB.length)
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB }); result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB });
} }
@ -605,7 +606,7 @@ export abstract class BrowserContext extends SdkObject {
for (const originState of state.origins) { for (const originState of state.origins) {
const frame = page.mainFrame(); const frame = page.mainFrame();
await frame.goto(metadata, originState.origin); await frame.goto(metadata, originState.origin);
await frame.evaluateExpression(storageScript.restore.toString(), { isFunction: true, world: 'utility' }, originState); await frame.evaluateExpression(`(${storageScript.restore})(${JSON.stringify(originState)}, (${utilityScriptSerializers.source})())`, { world: 'utility' });
} }
await page.close(internalMetadata); await page.close(internalMetadata);
} }

View file

@ -15,10 +15,11 @@
*/ */
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import type { source } from './isomorphic/utilityScriptSerializers';
export type Storage = Omit<channels.OriginStorage, 'origin'>; export type Storage = Omit<channels.OriginStorage, 'origin'>;
export async function collect(): Promise<Storage> { export async function collect(serializers: ReturnType<typeof source>): Promise<Storage> {
const idbResult = await Promise.all((await indexedDB.databases()).map(async dbInfo => { const idbResult = await Promise.all((await indexedDB.databases()).map(async dbInfo => {
if (!dbInfo.name) if (!dbInfo.name)
throw new Error('Database name is empty'); throw new Error('Database name is empty');
@ -32,6 +33,28 @@ export async function collect(): Promise<Storage> {
}); });
} }
function trySerialize(value: any): { trivial?: any, encoded?: any } {
let trivial = true;
const encoded = serializers.serializeAsCallArgument(value, v => {
const isTrivial = (
v?.constructor === Object
|| Array.isArray(v)
|| typeof v === 'string'
|| typeof v === 'number'
|| typeof v === 'boolean'
|| Object.is(v, null)
);
if (!isTrivial)
trivial = false;
return { fallThrough: v };
});
if (trivial)
return { trivial: value };
return { encoded };
}
const db = await idbRequestToPromise(indexedDB.open(dbInfo.name)); const db = await idbRequestToPromise(indexedDB.open(dbInfo.name));
const transaction = db.transaction(db.objectStoreNames, 'readonly'); const transaction = db.transaction(db.objectStoreNames, 'readonly');
const stores = await Promise.all([...db.objectStoreNames].map(async storeName => { const stores = await Promise.all([...db.objectStoreNames].map(async storeName => {
@ -39,10 +62,24 @@ export async function collect(): Promise<Storage> {
const keys = await idbRequestToPromise(objectStore.getAllKeys()); const keys = await idbRequestToPromise(objectStore.getAllKeys());
const records = await Promise.all(keys.map(async key => { const records = await Promise.all(keys.map(async key => {
return { const record: channels.OriginStorage['indexedDB'][0]['stores'][0]['records'][0] = {};
key: objectStore.keyPath === null ? key : undefined,
value: await idbRequestToPromise(objectStore.get(key)) if (objectStore.keyPath === null) {
}; const { encoded, trivial } = trySerialize(key);
if (trivial)
record.key = trivial;
else
record.keyEncoded = encoded;
}
const value = await idbRequestToPromise(objectStore.get(key));
const { encoded, trivial } = trySerialize(value);
if (trivial)
record.value = trivial;
else
record.valueEncoded = encoded;
return record;
})); }));
const indexes = [...objectStore.indexNames].map(indexName => { const indexes = [...objectStore.indexNames].map(indexName => {
@ -81,7 +118,7 @@ export async function collect(): Promise<Storage> {
}; };
} }
export async function restore(originState: channels.SetOriginStorage) { export async function restore(originState: channels.SetOriginStorage, serializers: ReturnType<typeof source>) {
for (const { name, value } of (originState.localStorage || [])) for (const { name, value } of (originState.localStorage || []))
localStorage.setItem(name, value); localStorage.setItem(name, value);
@ -111,8 +148,8 @@ export async function restore(originState: channels.SetOriginStorage) {
await Promise.all(store.records.map(async record => { await Promise.all(store.records.map(async record => {
await idbRequestToPromise( await idbRequestToPromise(
objectStore.add( objectStore.add(
record.value, record.value ?? serializers.parseEvaluationResultValue(record.valueEncoded),
objectStore.keyPath === null ? record.key : undefined record.key ?? serializers.parseEvaluationResultValue(record.keyEncoded),
) )
); );
})); }));

View file

@ -9338,7 +9338,17 @@ export interface BrowserContext {
records: Array<{ records: Array<{
key?: Object; key?: Object;
/**
* if `key` is not JSON-serializable, this contains an encoded version that preserves types.
*/
keyEncoded?: Object;
value: Object; value: Object;
/**
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
*/
valueEncoded?: Object;
}>; }>;
}>; }>;
}>; }>;

View file

@ -281,7 +281,9 @@ export type IndexedDBDatabase = {
keyPathArray?: string[], keyPathArray?: string[],
records: { records: {
key?: any, key?: any,
value: any, keyEncoded?: any,
value?: any,
valueEncoded?: any,
}[], }[],
indexes: { indexes: {
name: string, name: string,

View file

@ -244,7 +244,9 @@ IndexedDBDatabase:
type: object type: object
properties: properties:
key: json? key: json?
value: json keyEncoded: json?
value: json?
valueEncoded: json?
indexes: indexes:
type: array type: array
items: items:

View file

@ -96,7 +96,7 @@ it('should round-trip through the file', async ({ contextFactory }, testInfo) =>
openRequest.onsuccess = () => { openRequest.onsuccess = () => {
const request = openRequest.result.transaction('store', 'readwrite') const request = openRequest.result.transaction('store', 'readwrite')
.objectStore('store') .objectStore('store')
.put('foo', 'bar'); .put({ name: 'foo', date: new Date(0) }, 'bar');
request.addEventListener('success', resolve); request.addEventListener('success', resolve);
request.addEventListener('error', reject); request.addEventListener('error', reject);
}; };
@ -131,7 +131,7 @@ it('should round-trip through the file', async ({ contextFactory }, testInfo) =>
}); });
openRequest.addEventListener('error', () => reject(openRequest.error)); openRequest.addEventListener('error', () => reject(openRequest.error));
})); }));
expect(idbValue).toEqual('foo'); expect(idbValue).toEqual({ name: 'foo', date: new Date(0) });
await context2.close(); await context2.close();
}); });