From f4dcfd68b38f2c84f56bd690848016b2ba80600e Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Wed, 5 Feb 2025 15:02:18 +0100 Subject: [PATCH] add encoded versions --- docs/src/api/class-browsercontext.md | 2 + .../playwright-core/src/protocol/validator.ts | 4 +- .../src/server/browserContext.ts | 7 +-- .../src/server/storageScript.ts | 53 ++++++++++++++++--- packages/playwright-core/types/types.d.ts | 10 ++++ packages/protocol/src/channels.d.ts | 4 +- packages/protocol/src/protocol.yml | 4 +- .../browsercontext-storage-state.spec.ts | 4 +- 8 files changed, 72 insertions(+), 16 deletions(-) diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index cae2c0d139..0da156b664 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -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. diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 8981df6f4a..35832219ee 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -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, diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 8de81bb1ee..2eab61d2e1 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -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> = await frame.evaluateExpression(`(${storageScript.collect})()`, { world: 'utility' }); + const storage: Awaited> = 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); } diff --git a/packages/playwright-core/src/server/storageScript.ts b/packages/playwright-core/src/server/storageScript.ts index acde6b54f7..0986650acc 100644 --- a/packages/playwright-core/src/server/storageScript.ts +++ b/packages/playwright-core/src/server/storageScript.ts @@ -15,10 +15,11 @@ */ import type * as channels from '@protocol/channels'; +import type { source } from './isomorphic/utilityScriptSerializers'; export type Storage = Omit; -export async function collect(): Promise { +export async function collect(serializers: ReturnType): Promise { 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 { }); } + 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 { 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 { }; } -export async function restore(originState: channels.SetOriginStorage) { +export async function restore(originState: channels.SetOriginStorage, serializers: ReturnType) { 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), ) ); })); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 4e86e5b58b..196f211e20 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -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; }>; }>; }>; diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index bc8c7c56bb..b0ccdcbc2a 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -281,7 +281,9 @@ export type IndexedDBDatabase = { keyPathArray?: string[], records: { key?: any, - value: any, + keyEncoded?: any, + value?: any, + valueEncoded?: any, }[], indexes: { name: string, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index bdf55069e4..b581eacb4e 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -244,7 +244,9 @@ IndexedDBDatabase: type: object properties: key: json? - value: json + keyEncoded: json? + value: json? + valueEncoded: json? indexes: type: array items: diff --git a/tests/library/browsercontext-storage-state.spec.ts b/tests/library/browsercontext-storage-state.spec.ts index 04a1eee02c..4b003a6c81 100644 --- a/tests/library/browsercontext-storage-state.spec.ts +++ b/tests/library/browsercontext-storage-state.spec.ts @@ -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(); });