diff --git a/docs/src/api/class-apirequestcontext.md b/docs/src/api/class-apirequestcontext.md index 40742e2a1f..cf4106552d 100644 --- a/docs/src/api/class-apirequestcontext.md +++ b/docs/src/api/class-apirequestcontext.md @@ -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. diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index a2ed96cfdc..cae2c0d139 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -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. diff --git a/docs/src/api/params.md b/docs/src/api/params.md index da35e7a248..c878907d28 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -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). diff --git a/docs/src/auth.md b/docs/src/auth.md index 4e57a97e95..4a3853edf1 100644 --- a/docs/src/auth.md +++ b/docs/src/auth.md @@ -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. diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 547f6c9692..4959f1d9b8 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -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 }); } - async storageState(options: { path?: string } = {}): Promise { + async storageState(options: { path?: string } = {}): Promise { const state = await this._channel.storageState(); if (options.path) { await mkdirIfNeeded(options.path); diff --git a/packages/playwright-core/src/client/types.ts b/packages/playwright-core/src/client/types.ts index 50632bf596..4f9dbd6db4 100644 --- a/packages/playwright-core/src/client/types.ts +++ b/packages/playwright-core/src/client/types.ts @@ -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; diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 1986be8b93..a5f299f782 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -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({})); diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 92edb09360..5cf76d83ca 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -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); diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 336203f33b..44602e0c82 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -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); diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 1570df5770..b1f4d4c1c6 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -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. diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 65878b44ce..fb34627de3 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -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; + + autoIncrement: boolean; + + indexes: Array<{ + name: string; + + keyPath?: string; + + keyPathArray?: Array; + + 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; }>; }>; }>; diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 954f0096c8..e68cbb0bc7 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -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 = {}; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index f8210c558e..a38b4f5e7c 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -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 diff --git a/tests/library/browsercontext-storage-state.spec.ts b/tests/library/browsercontext-storage-state.spec.ts index e2b2c4f3f8..27c446a2f4 100644 --- a/tests/library/browsercontext-storage-state.spec.ts +++ b/tests/library/browsercontext-storage-state.spec.ts @@ -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: [