add encoded versions
This commit is contained in:
parent
311625b891
commit
f4dcfd68b3
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
10
packages/playwright-core/types/types.d.ts
vendored
10
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -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;
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
|
|
|
||||||
4
packages/protocol/src/channels.d.ts
vendored
4
packages/protocol/src/channels.d.ts
vendored
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue