chore: add encoded versions of IndexedDB key/value (#34630)
This commit is contained in:
parent
11e1b8f30a
commit
7aac96d780
|
|
@ -80,7 +80,9 @@ Methods like [`method: APIRequestContext.get`] take the base URL into considerat
|
|||
- `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.
|
||||
|
||||
Populates context with given storage state. This option can be used to initialize context with logged-in information
|
||||
obtained via [`method: BrowserContext.storageState`] or [`method: APIRequestContext.storageState`]. Either a path to the
|
||||
|
|
|
|||
|
|
@ -896,7 +896,9 @@ context cookies from the response. The method will automatically follow redirect
|
|||
- `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 request context, contains current cookies and local storage snapshot if it was passed to the constructor.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -280,7 +280,9 @@ Specify environment variables that will be visible to the browser. Defaults to `
|
|||
- `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.
|
||||
|
||||
Learn more about [storage state and auth](../auth.md).
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
@ -514,13 +515,15 @@ export abstract class BrowserContext extends SdkObject {
|
|||
};
|
||||
const originsToSave = new Set(this._origins);
|
||||
|
||||
const collectScript = `(${storageScript.collect})((${utilityScriptSerializers.source})(), ${this._browser.options.name === 'firefox'})`;
|
||||
|
||||
// First try collecting storage stage from existing pages.
|
||||
for (const page of this.pages()) {
|
||||
const origin = page.mainFrame().origin();
|
||||
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(collectScript, 'utility');
|
||||
if (storage.localStorage.length || storage.indexedDB.length)
|
||||
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB });
|
||||
originsToSave.delete(origin);
|
||||
|
|
@ -540,7 +543,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: storageScript.Storage = await frame.evaluateExpression(collectScript, { world: 'utility' });
|
||||
if (storage.localStorage.length || storage.indexedDB.length)
|
||||
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB });
|
||||
}
|
||||
|
|
@ -605,7 +608,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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>, isFirefox: boolean): 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,39 @@ export async function collect(): Promise<Storage> {
|
|||
});
|
||||
}
|
||||
|
||||
function isPlainObject(v: any) {
|
||||
const ctor = v?.constructor;
|
||||
if (isFirefox) {
|
||||
const constructorImpl = ctor?.toString();
|
||||
if (constructorImpl.startsWith('function Object() {') && constructorImpl.includes('[native code]'))
|
||||
return true;
|
||||
}
|
||||
|
||||
return ctor === Object;
|
||||
}
|
||||
|
||||
function trySerialize(value: any): { trivial?: any, encoded?: any } {
|
||||
let trivial = true;
|
||||
const encoded = serializers.serializeAsCallArgument(value, v => {
|
||||
const isTrivial = (
|
||||
isPlainObject(v)
|
||||
|| 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 +73,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 +129,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 +159,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),
|
||||
)
|
||||
);
|
||||
}));
|
||||
|
|
|
|||
50
packages/playwright-core/types/types.d.ts
vendored
50
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
|
|
@ -10147,7 +10157,17 @@ export interface Browser {
|
|||
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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
|
|
@ -17725,7 +17745,17 @@ export interface APIRequest {
|
|||
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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
|
|
@ -18568,7 +18598,17 @@ export interface APIRequestContext {
|
|||
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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
|
|
@ -22454,7 +22494,17 @@ export interface BrowserContextOptions {
|
|||
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;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
|
|
|
|||
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[],
|
||||
records: {
|
||||
key?: any,
|
||||
value: any,
|
||||
keyEncoded?: any,
|
||||
value?: any,
|
||||
valueEncoded?: any,
|
||||
}[],
|
||||
indexes: {
|
||||
name: string,
|
||||
|
|
|
|||
|
|
@ -244,7 +244,9 @@ IndexedDBDatabase:
|
|||
type: object
|
||||
properties:
|
||||
key: json?
|
||||
value: json
|
||||
keyEncoded: json?
|
||||
value: json?
|
||||
valueEncoded: json?
|
||||
indexes:
|
||||
type: array
|
||||
items:
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue