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]>> - `localStorage` <[Array]<[Object]>>
- `name` <[string]> - `name` <[string]>
- `value` <[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. 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]> - `unique` <[boolean]>
- `multiEntry` <[boolean]> - `multiEntry` <[boolean]>
- `records` <[Array]<[Object]>> - `records` <[Array]<[Object]>>
- `key` ?<[string]> - `key` ?<[Object]>
- `value` <[string]> - `value` <[Object]>
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.

View file

@ -279,8 +279,8 @@ Specify environment variables that will be visible to the browser. Defaults to `
- `unique` <[boolean]> - `unique` <[boolean]>
- `multiEntry` <[boolean]> - `multiEntry` <[boolean]>
- `records` <[Array]<[Object]>> - `records` <[Array]<[Object]>>
- `key` ?<[string]> opaque key, only defined if stores uses out-of-line keys - `key` ?<[Object]>
- `value` <[string]> opaque value - `value` <[Object]>
Learn more about [storage state and auth](../auth.md). 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. 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. 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 { Events } from './events';
import { TimeoutSettings } from '../common/timeoutSettings'; import { TimeoutSettings } from '../common/timeoutSettings';
import { Waiter } from './waiter'; 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 URLMatch, headersObjectToArray, isRegExp, isString, urlMatchesEqual, mkdirIfNeeded } from '../utils';
import type * as api from '../../types/types'; import type * as api from '../../types/types';
import type * as structs from '../../types/structs'; 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(); const state = await this._channel.storageState();
if (options.path) { if (options.path) {
await mkdirIfNeeded(options.path); await mkdirIfNeeded(options.path);

View file

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

View file

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

View file

@ -43,7 +43,6 @@ import type { Artifact } from './artifact';
import { Clock } from './clock'; 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 utilitySerializers from './isomorphic/utilityScriptSerializers';
export abstract class BrowserContext extends SdkObject { export abstract class BrowserContext extends SdkObject {
static Events = { 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. // 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();
@ -593,9 +580,8 @@ export abstract class BrowserContext extends SdkObject {
continue; continue;
try { try {
const storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`(${_collectStorageScript.toString()})()`, 'utility'); const storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`(${_collectStorageScript.toString()})()`, 'utility');
serializeRecords(storage.indexedDB);
if (storage.localStorage.length || storage.indexedDB?.length) 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); originsToSave.delete(origin);
} catch { } catch {
// When failed on the live page, we'll retry on the blank page below. // 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(); const frame = page.mainFrame();
await frame.goto(internalMetadata, origin); await frame.goto(internalMetadata, origin);
const storage = await frame.evaluateExpression(`(${_collectStorageScript.toString()})()`, { world: 'utility' }); const storage = await frame.evaluateExpression(`(${_collectStorageScript.toString()})()`, { world: 'utility' });
serializeRecords(storage.indexedDB);
if (storage.localStorage.length || storage.indexedDB.length) 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); await page.close(internalMetadata);
} }
@ -680,16 +665,6 @@ export abstract class BrowserContext extends SdkObject {
const frame = page.mainFrame(); const frame = page.mainFrame();
await frame.goto(metadata, originState.origin); 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) { async function _restoreStorageState(originState: channels.OriginStorage) {
for (const { name, value } of (originState.localStorage || [])) for (const { name, value } of (originState.localStorage || []))
localStorage.setItem(name, value); localStorage.setItem(name, value);

View file

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

View file

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

View file

@ -9330,9 +9330,9 @@ export interface BrowserContext {
}>; }>;
records: Array<{ records: Array<{
key?: string; key?: Object;
value: string; value: Object;
}>; }>;
}>; }>;
}>; }>;
@ -10132,15 +10132,9 @@ export interface Browser {
}>; }>;
records: Array<{ records: Array<{
/** key?: Object;
* opaque key, only defined if stores uses out-of-line keys
*/
key?: string;
/** value: Object;
* opaque value
*/
value: string;
}>; }>;
}>; }>;
}>; }>;
@ -18478,6 +18472,40 @@ export interface APIRequestContext {
value: string; 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<{ records: Array<{
/** key?: Object;
* opaque key, only defined if stores uses out-of-line keys
*/
key?: string;
/** value: Object;
* opaque value
*/
value: string;
}>; }>;
}>; }>;
}>; }>;

View file

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

View file

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

View file

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