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]>
|
- `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.
|
||||||
|
|
||||||
Populates context with given storage state. This option can be used to initialize context with logged-in information
|
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
|
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]>
|
- `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 request context, contains current cookies and local storage snapshot if it was passed to the constructor.
|
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]>
|
- `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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -280,7 +280,9 @@ Specify environment variables that will be visible to the browser. Defaults to `
|
||||||
- `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.
|
||||||
|
|
||||||
Learn more about [storage state and auth](../auth.md).
|
Learn more about [storage state and auth](../auth.md).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
@ -514,13 +515,15 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
};
|
};
|
||||||
const originsToSave = new Set(this._origins);
|
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.
|
// First try collecting storage stage from existing pages.
|
||||||
for (const page of this.pages()) {
|
for (const page of this.pages()) {
|
||||||
const origin = page.mainFrame().origin();
|
const origin = page.mainFrame().origin();
|
||||||
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(collectScript, '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 +543,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: storageScript.Storage = await frame.evaluateExpression(collectScript, { 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 +608,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>, isFirefox: boolean): 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,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 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 +73,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 +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 || []))
|
for (const { name, value } of (originState.localStorage || []))
|
||||||
localStorage.setItem(name, value);
|
localStorage.setItem(name, value);
|
||||||
|
|
||||||
|
|
@ -111,8 +159,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),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
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<{
|
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;
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
|
|
@ -10147,7 +10157,17 @@ export interface Browser {
|
||||||
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;
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
|
|
@ -17725,7 +17745,17 @@ export interface APIRequest {
|
||||||
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;
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
|
|
@ -18568,7 +18598,17 @@ export interface APIRequestContext {
|
||||||
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;
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
}>;
|
}>;
|
||||||
|
|
@ -22454,7 +22494,17 @@ export interface BrowserContextOptions {
|
||||||
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