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]>
- `records` <[Array]<[Object]>>
- `key` ?<[Object]>
- `keyEncoded` ?<[Object]> if `key` is not JSON-serializable, this contains an encoded version that preserves types.
- `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.

View file

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

View file

@ -44,6 +44,7 @@ import { Clock } from './clock';
import type { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
import { RecorderApp } from './recorder/recorderApp';
import * as storageScript from './storageScript';
import * as utilityScriptSerializers from './isomorphic/utilityScriptSerializers';
export abstract class BrowserContext extends SdkObject {
static Events = {
@ -520,7 +521,7 @@ export abstract class BrowserContext extends SdkObject {
if (!origin || !originsToSave.has(origin))
continue;
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)
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB });
originsToSave.delete(origin);
@ -540,7 +541,7 @@ export abstract class BrowserContext extends SdkObject {
for (const origin of originsToSave) {
const frame = page.mainFrame();
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)
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) {
const frame = page.mainFrame();
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);
}

View file

@ -15,10 +15,11 @@
*/
import type * as channels from '@protocol/channels';
import type { source } from './isomorphic/utilityScriptSerializers';
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 => {
if (!dbInfo.name)
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 transaction = db.transaction(db.objectStoreNames, 'readonly');
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 records = await Promise.all(keys.map(async key => {
return {
key: objectStore.keyPath === null ? key : undefined,
value: await idbRequestToPromise(objectStore.get(key))
};
const record: channels.OriginStorage['indexedDB'][0]['stores'][0]['records'][0] = {};
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 => {
@ -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 || []))
localStorage.setItem(name, value);
@ -111,8 +148,8 @@ export async function restore(originState: channels.SetOriginStorage) {
await Promise.all(store.records.map(async record => {
await idbRequestToPromise(
objectStore.add(
record.value,
objectStore.keyPath === null ? record.key : undefined
record.value ?? serializers.parseEvaluationResultValue(record.valueEncoded),
record.key ?? serializers.parseEvaluationResultValue(record.keyEncoded),
)
);
}));

View file

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

View file

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

View file

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