store unserialised

This commit is contained in:
Simon Knott 2025-02-05 12:02:45 +01:00
parent 33d8be42cb
commit 51077a0799
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
14 changed files with 113 additions and 102 deletions

View file

@ -880,6 +880,23 @@ context cookies from the response. The method will automatically follow redirect
- `localStorage` <[Array]<[Object]>>
- `name` <[string]>
- `value` <[string]>
- `indexedDB` ?<[Array]<[Object]>>
- `name` <[string]>
- `version` <[int]>
- `stores` <[Array]<[Object]>>
- `name` <[string]>
- `keyPath` ?<[string]>
- `keyPathArray` ?<[Array]<[string]>>
- `autoIncrement` <[boolean]>
- `indexes` <[Array]<[Object]>>
- `name` <[string]>
- `keyPath` ?<[string]>
- `keyPathArray` ?<[Array]<[string]>>
- `unique` <[boolean]>
- `multiEntry` <[boolean]>
- `records` <[Array]<[Object]>>
- `key` ?<[Object]>
- `value` <[Object]>
Returns storage state for this request context, contains current cookies and local storage snapshot if it was passed to the constructor.

View file

@ -1526,8 +1526,8 @@ Whether to emulate network being offline for the browser context.
- `unique` <[boolean]>
- `multiEntry` <[boolean]>
- `records` <[Array]<[Object]>>
- `key` ?<[string]>
- `value` <[string]>
- `key` ?<[Object]>
- `value` <[Object]>
Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.

View file

@ -279,8 +279,8 @@ Specify environment variables that will be visible to the browser. Defaults to `
- `unique` <[boolean]>
- `multiEntry` <[boolean]>
- `records` <[Array]<[Object]>>
- `key` ?<[string]> opaque key, only defined if stores uses out-of-line keys
- `value` <[string]> opaque value
- `key` ?<[Object]>
- `value` <[Object]>
Learn more about [storage state and auth](../auth.md).

View file

@ -268,7 +268,7 @@ in only once and then skip the log in step for all of the tests.
Web apps use cookie-based or token-based authentication, where authenticated state is stored as [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage) or in [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). Playwright provides [`method: BrowserContext.storageState`] method that can be used to retrieve storage state from authenticated contexts and then create new contexts with prepopulated state.
Cookies, local storage and IndexedDB state can be used across different browsers. They depend on your application's authentication model: some apps might require both cookies and local storage or IndexedDB.
Cookies, local storage and IndexedDB state can be used across different browsers. They depend on your application's authentication model which may require some combination of cookies, local storage or IndexedDB.
The following code snippet retrieves state from an authenticated context and creates a new context with that state.

View file

@ -28,7 +28,7 @@ import { Worker } from './worker';
import { Events } from './events';
import { TimeoutSettings } from '../common/timeoutSettings';
import { Waiter } from './waiter';
import type { Headers, WaitForEventOptions, BrowserContextOptions, LaunchOptions, StorageStateWithIndexedDB } from './types';
import type { Headers, WaitForEventOptions, BrowserContextOptions, LaunchOptions, StorageState } from './types';
import { type URLMatch, headersObjectToArray, isRegExp, isString, urlMatchesEqual, mkdirIfNeeded } from '../utils';
import type * as api from '../../types/types';
import type * as structs from '../../types/structs';
@ -425,7 +425,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
});
}
async storageState(options: { path?: string } = {}): Promise<StorageStateWithIndexedDB> {
async storageState(options: { path?: string } = {}): Promise<StorageState> {
const state = await this._channel.storageState();
if (options.path) {
await mkdirIfNeeded(options.path);

View file

@ -39,13 +39,9 @@ export type StorageState = {
cookies: channels.NetworkCookie[],
origins: channels.OriginStorage[]
};
export type StorageStateWithIndexedDB = {
cookies: channels.NetworkCookie[],
origins: channels.OriginStorageWithRequiredIndexedDB[]
};
export type SetStorageState = {
cookies?: channels.SetNetworkCookie[],
origins?: channels.OriginStorage[]
origins?: channels.SetOriginStorage[]
};
export type LifecycleEvent = channels.LifecycleEvent;

View file

@ -149,26 +149,26 @@ scheme.IndexedDBDatabase = tObject({
name: tString,
autoIncrement: tBoolean,
keyPath: tOptional(tString),
keyPathArray: tOptional(tType('string[]')),
keyPathArray: tOptional(tArray(tString)),
records: tArray(tObject({
key: tOptional(tString),
value: tString,
key: tOptional(tAny),
value: tAny,
})),
indexes: tArray(tObject({
name: tString,
keyPath: tOptional(tString),
keyPathArray: tOptional(tType('string[]')),
keyPathArray: tOptional(tArray(tString)),
multiEntry: tBoolean,
unique: tBoolean,
})),
})),
});
scheme.OriginStorage = tObject({
scheme.SetOriginStorage = tObject({
origin: tString,
localStorage: tArray(tType('NameValue')),
indexedDB: tOptional(tArray(tType('IndexedDBDatabase'))),
});
scheme.OriginStorageWithRequiredIndexedDB = tObject({
scheme.OriginStorage = tObject({
origin: tString,
localStorage: tArray(tType('NameValue')),
indexedDB: tArray(tType('IndexedDBDatabase')),
@ -388,7 +388,7 @@ scheme.PlaywrightNewRequestParams = tObject({
timeout: tOptional(tNumber),
storageState: tOptional(tObject({
cookies: tOptional(tArray(tType('NetworkCookie'))),
origins: tOptional(tArray(tType('OriginStorage'))),
origins: tOptional(tArray(tType('SetOriginStorage'))),
})),
tracesDir: tOptional(tString),
});
@ -714,7 +714,7 @@ scheme.BrowserNewContextParams = tObject({
})),
storageState: tOptional(tObject({
cookies: tOptional(tArray(tType('SetNetworkCookie'))),
origins: tOptional(tArray(tType('OriginStorage'))),
origins: tOptional(tArray(tType('SetOriginStorage'))),
})),
});
scheme.BrowserNewContextResult = tObject({
@ -783,7 +783,7 @@ scheme.BrowserNewContextForReuseParams = tObject({
})),
storageState: tOptional(tObject({
cookies: tOptional(tArray(tType('SetNetworkCookie'))),
origins: tOptional(tArray(tType('OriginStorage'))),
origins: tOptional(tArray(tType('SetOriginStorage'))),
})),
});
scheme.BrowserNewContextForReuseResult = tObject({
@ -990,7 +990,7 @@ scheme.BrowserContextSetOfflineResult = tOptional(tObject({}));
scheme.BrowserContextStorageStateParams = tOptional(tObject({}));
scheme.BrowserContextStorageStateResult = tObject({
cookies: tArray(tType('NetworkCookie')),
origins: tArray(tType('OriginStorageWithRequiredIndexedDB')),
origins: tArray(tType('OriginStorage')),
});
scheme.BrowserContextPauseParams = tOptional(tObject({}));
scheme.BrowserContextPauseResult = tOptional(tObject({}));

View file

@ -43,7 +43,6 @@ import type { Artifact } from './artifact';
import { Clock } from './clock';
import type { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
import { RecorderApp } from './recorder/recorderApp';
import * as utilitySerializers from './isomorphic/utilityScriptSerializers';
export abstract class BrowserContext extends SdkObject {
static Events = {
@ -574,18 +573,6 @@ export abstract class BrowserContext extends SdkObject {
};
}
function serializeRecords(indexedDBs: channels.IndexedDBDatabase[]) {
for (const db of indexedDBs) {
for (const store of db.stores) {
for (const record of store.records) {
if (record.key !== undefined)
record.key = JSON.stringify(utilitySerializers.serializeAsCallArgument(record.value, v => ({ fallThrough: v })));
record.value = JSON.stringify(utilitySerializers.serializeAsCallArgument(record.value, v => ({ fallThrough: v })));
}
}
}
}
// First try collecting storage stage from existing pages.
for (const page of this.pages()) {
const origin = page.mainFrame().origin();
@ -593,9 +580,8 @@ export abstract class BrowserContext extends SdkObject {
continue;
try {
const storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`(${_collectStorageScript.toString()})()`, 'utility');
serializeRecords(storage.indexedDB);
if (storage.localStorage.length || storage.indexedDB?.length)
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB } as channels.OriginStorageWithRequiredIndexedDB);
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB } as channels.OriginStorage);
originsToSave.delete(origin);
} catch {
// When failed on the live page, we'll retry on the blank page below.
@ -614,9 +600,8 @@ export abstract class BrowserContext extends SdkObject {
const frame = page.mainFrame();
await frame.goto(internalMetadata, origin);
const storage = await frame.evaluateExpression(`(${_collectStorageScript.toString()})()`, { world: 'utility' });
serializeRecords(storage.indexedDB);
if (storage.localStorage.length || storage.indexedDB.length)
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB } as channels.OriginStorageWithRequiredIndexedDB);
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB } as channels.OriginStorage);
}
await page.close(internalMetadata);
}
@ -680,16 +665,6 @@ export abstract class BrowserContext extends SdkObject {
const frame = page.mainFrame();
await frame.goto(metadata, originState.origin);
for (const dbInfo of (originState.indexedDB || [])) {
for (const store of dbInfo.stores) {
for (const record of store.records) {
if (record.key !== undefined)
record.key = utilitySerializers.parseEvaluationResultValue(JSON.parse(record.key));
record.value = utilitySerializers.parseEvaluationResultValue(JSON.parse(record.value));
}
}
}
async function _restoreStorageState(originState: channels.OriginStorage) {
for (const { name, value } of (originState.localStorage || []))
localStorage.setItem(name, value);

View file

@ -644,7 +644,7 @@ export class GlobalAPIRequestContext extends APIRequestContext {
proxy.server = url;
}
if (options.storageState) {
this._origins = options.storageState.origins;
this._origins = options.storageState.origins?.map(origin => ({ indexedDB: [], ...origin }));
this._cookieStore.addCookies(options.storageState.cookies || []);
}
verifyClientCertificates(options.clientCertificates);

View file

@ -1717,7 +1717,7 @@ export class Frame extends SdkObject {
}, { source, arg });
}
async resetStorageForCurrentOriginBestEffort(newStorage: channels.OriginStorage | undefined) {
async resetStorageForCurrentOriginBestEffort(newStorage: channels.SetOriginStorage | undefined) {
const context = await this._utilityContext();
await context.evaluate(async ({ ls }) => {
// Clean DOMStorage.

View file

@ -9330,9 +9330,9 @@ export interface BrowserContext {
}>;
records: Array<{
key?: string;
key?: Object;
value: string;
value: Object;
}>;
}>;
}>;
@ -10132,15 +10132,9 @@ export interface Browser {
}>;
records: Array<{
/**
* opaque key, only defined if stores uses out-of-line keys
*/
key?: string;
key?: Object;
/**
* opaque value
*/
value: string;
value: Object;
}>;
}>;
}>;
@ -18478,6 +18472,40 @@ export interface APIRequestContext {
value: string;
}>;
indexedDB?: Array<{
name: string;
version: number;
stores: Array<{
name: string;
keyPath?: string;
keyPathArray?: Array<string>;
autoIncrement: boolean;
indexes: Array<{
name: string;
keyPath?: string;
keyPathArray?: Array<string>;
unique: boolean;
multiEntry: boolean;
}>;
records: Array<{
key?: Object;
value: Object;
}>;
}>;
}>;
}>;
}>;
@ -22348,15 +22376,9 @@ export interface BrowserContextOptions {
}>;
records: Array<{
/**
* opaque key, only defined if stores uses out-of-line keys
*/
key?: string;
key?: Object;
/**
* opaque value
*/
value: string;
value: Object;
}>;
}>;
}>;

View file

@ -280,8 +280,8 @@ export type IndexedDBDatabase = {
keyPath?: string,
keyPathArray?: string[],
records: {
key?: string,
value: string,
key?: any,
value: any,
}[],
indexes: {
name: string,
@ -293,13 +293,13 @@ export type IndexedDBDatabase = {
}[],
};
export type OriginStorage = {
export type SetOriginStorage = {
origin: string,
localStorage: NameValue[],
indexedDB?: IndexedDBDatabase[],
};
export type OriginStorageWithRequiredIndexedDB = {
export type OriginStorage = {
origin: string,
localStorage: NameValue[],
indexedDB: IndexedDBDatabase[],
@ -639,7 +639,7 @@ export type PlaywrightNewRequestParams = {
timeout?: number,
storageState?: {
cookies?: NetworkCookie[],
origins?: OriginStorage[],
origins?: SetOriginStorage[],
},
tracesDir?: string,
};
@ -670,7 +670,7 @@ export type PlaywrightNewRequestOptions = {
timeout?: number,
storageState?: {
cookies?: NetworkCookie[],
origins?: OriginStorage[],
origins?: SetOriginStorage[],
},
tracesDir?: string,
};
@ -1249,7 +1249,7 @@ export type BrowserNewContextParams = {
},
storageState?: {
cookies?: SetNetworkCookie[],
origins?: OriginStorage[],
origins?: SetOriginStorage[],
},
};
export type BrowserNewContextOptions = {
@ -1315,7 +1315,7 @@ export type BrowserNewContextOptions = {
},
storageState?: {
cookies?: SetNetworkCookie[],
origins?: OriginStorage[],
origins?: SetOriginStorage[],
},
};
export type BrowserNewContextResult = {
@ -1384,7 +1384,7 @@ export type BrowserNewContextForReuseParams = {
},
storageState?: {
cookies?: SetNetworkCookie[],
origins?: OriginStorage[],
origins?: SetOriginStorage[],
},
};
export type BrowserNewContextForReuseOptions = {
@ -1450,7 +1450,7 @@ export type BrowserNewContextForReuseOptions = {
},
storageState?: {
cookies?: SetNetworkCookie[],
origins?: OriginStorage[],
origins?: SetOriginStorage[],
},
};
export type BrowserNewContextForReuseResult = {
@ -1793,7 +1793,7 @@ export type BrowserContextStorageStateParams = {};
export type BrowserContextStorageStateOptions = {};
export type BrowserContextStorageStateResult = {
cookies: NetworkCookie[],
origins: OriginStorageWithRequiredIndexedDB[],
origins: OriginStorage[],
};
export type BrowserContextPauseParams = {};
export type BrowserContextPauseOptions = {};

View file

@ -235,14 +235,16 @@ IndexedDBDatabase:
name: string
autoIncrement: boolean
keyPath: string?
keyPathArray: string[]?
keyPathArray:
type: array?
items: string
records:
type: array
items:
type: object
properties:
key: string?
value: string
key: json?
value: json
indexes:
type: array
items:
@ -250,11 +252,13 @@ IndexedDBDatabase:
properties:
name: string
keyPath: string?
keyPathArray: string[]?
keyPathArray:
type: array?
items: string
multiEntry: boolean
unique: boolean
OriginStorage:
SetOriginStorage:
type: object
properties:
origin: string
@ -265,7 +269,7 @@ OriginStorage:
type: array?
items: IndexedDBDatabase
OriginStorageWithRequiredIndexedDB:
OriginStorage:
type: object
properties:
origin: string
@ -774,7 +778,7 @@ Playwright:
items: NetworkCookie
origins:
type: array?
items: OriginStorage
items: SetOriginStorage
tracesDir: string?
returns:
@ -1008,7 +1012,7 @@ Browser:
items: SetNetworkCookie
origins:
type: array?
items: OriginStorage
items: SetOriginStorage
returns:
context: BrowserContext
@ -1030,7 +1034,7 @@ Browser:
items: SetNetworkCookie
origins:
type: array?
items: OriginStorage
items: SetOriginStorage
returns:
context: BrowserContext
@ -1228,7 +1232,7 @@ BrowserContext:
items: NetworkCookie
origins:
type: array
items: OriginStorageWithRequiredIndexedDB
items: OriginStorage
pause:
experimental: True

View file

@ -342,18 +342,15 @@ it('should support IndexedDB', async ({ page, server, contextFactory }) => {
keyPath: 'taskTitle',
records: [
{
value: JSON.stringify({
o: [
{ k: 'taskTitle', v: 'Pet the cat' },
{ k: 'hours', v: '1' },
{ k: 'minutes', v: '1' },
{ k: 'day', v: '01' },
{ k: 'month', v: 'January' },
{ k: 'year', v: '2025' },
{ k: 'notified', v: 'no' }
],
id: 1
}),
value: {
day: '01',
hours: '1',
minutes: '1',
month: 'January',
notified: 'no',
taskTitle: 'Pet the cat',
year: '2025',
},
},
],
indexes: [