diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 85cbc4f62a..a2ed96cfdc 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1511,8 +1511,25 @@ Whether to emulate network being offline for the browser context. - `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` ?<[string]> + - `value` <[string]> -Returns storage state for this browser context, contains current cookies and local storage snapshot. +Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot. ## async method: BrowserContext.storageState * since: v1.8 diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 04dc56e965..172f6fdb36 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -265,7 +265,22 @@ Specify environment variables that will be visible to the browser. Defaults to ` - `name` <[string]> - `value` <[string]> - `indexedDB` ?<[Array]<[Object]>> - - `name` <[string]> TODO: document more + - `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` ?<[string]> + - `value` <[string]> Learn more about [storage state and auth](../auth.md). diff --git a/docs/src/codegen.md b/docs/src/codegen.md index f0d9a605f5..641383d7b4 100644 --- a/docs/src/codegen.md +++ b/docs/src/codegen.md @@ -325,7 +325,7 @@ pwsh bin/Debug/netX/playwright.ps1 codegen --timezone="Europe/Rome" --geolocatio ### Preserve authenticated state -Run `codegen` with `--save-storage` to save [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) and [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) at the end of the session. This is useful to separately record an authentication step and reuse it later when recording more tests. +Run `codegen` with `--save-storage` to save [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) data at the end of the session. This is useful to separately record an authentication step and reuse it later when recording more tests. ```bash js npx playwright codegen github.com/microsoft/playwright --save-storage=auth.json @@ -375,7 +375,7 @@ Make sure you only use the `auth.json` locally as it contains sensitive informat #### Load authenticated state -Run with `--load-storage` to consume the previously loaded storage from the `auth.json`. This way, all [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) and [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) will be restored, bringing most web apps to the authenticated state without the need to login again. This means you can continue generating tests from the logged in state. +Run with `--load-storage` to consume the previously loaded storage from the `auth.json`. This way, all [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) and [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) data will be restored, bringing most web apps to the authenticated state without the need to login again. This means you can continue generating tests from the logged in state. ```bash js npx playwright codegen --load-storage=auth.json github.com/microsoft/playwright diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index e9f06d5126..0db440269c 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -166,7 +166,7 @@ scheme.IndexedDBDatabase = tObject({ scheme.OriginStorage = tObject({ origin: tString, localStorage: tArray(tType('NameValue')), - indexedDB: tOptional(tArray(tType('IndexedDBDatabase'))), + indexedDB: tArray(tType('IndexedDBDatabase')), }); scheme.SerializedError = tObject({ error: tOptional(tObject({ diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 2879ad51f5..29e7411676 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -515,20 +515,16 @@ export abstract class BrowserContext extends SdkObject { const originsToSave = new Set(this._origins); async function _collectStorageScript() { - async function _collectDatabase(dbInfo: IDBDatabaseInfo) { - if (!dbInfo.name) - return; - let db: IDBDatabase; - try { - db = await new Promise((resolve, reject) => { - const request = indexedDB.open(dbInfo.name!); - request.onerror = reject; - request.onsuccess = () => resolve(request.result); - }); - } catch { - return; - } + const idbResult = await Promise.all((await indexedDB.databases()).map(async dbInfo => { + if (!dbInfo.name) + throw new Error('Database name is empty'); + + const db = await new Promise((resolve, reject) => { + const request = indexedDB.open(dbInfo.name!); + request.onerror = reject; + request.onsuccess = () => resolve(request.result); + }); const transaction = db.transaction(db.objectStoreNames, 'readonly'); const stores = await Promise.all([...db.objectStoreNames].map(async storeName => { @@ -540,22 +536,19 @@ export abstract class BrowserContext extends SdkObject { }); const records = await Promise.all(keys.map(async key => { - const record = await new Promise((resolve, reject) => { + const record = await new Promise((resolve, reject) => { const request = objectStore.get(key); request.addEventListener('success', () => resolve(request.result)); request.addEventListener('error', reject); }); - if (!record) - return; - return { key: objectStore.keyPath === null ? key.toString() : undefined, value: record }; })); - const indexes = await Promise.all([...objectStore.indexNames].map(async indexName => { + const indexes = [...objectStore.indexNames].map(indexName => { const index = objectStore.index(indexName); return { name: index.name, @@ -564,7 +557,7 @@ export abstract class BrowserContext extends SdkObject { multiEntry: index.multiEntry, unique: index.unique, }; - })); + }); return { name: storeName, @@ -581,13 +574,11 @@ export abstract class BrowserContext extends SdkObject { version: dbInfo.version, stores, }; - } - - const idbResult = await Promise.all((await indexedDB.databases()).map(_collectDatabase).filter(Boolean)); + })); return { localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })), - indexedDB: idbResult.length ? idbResult : undefined, + indexedDB: idbResult, }; } @@ -607,8 +598,7 @@ export abstract class BrowserContext extends SdkObject { continue; try { const storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`(${_collectStorageScript.toString()})()`, 'utility'); - if (storage.indexedDB?.length) - serializeRecords(storage.indexedDB); + serializeRecords(storage.indexedDB); if (storage.localStorage.length || storage.indexedDB?.length) result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB } as channels.OriginStorage); originsToSave.delete(origin); @@ -629,9 +619,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' }); - if (storage.indexedDB?.length) - serializeRecords(storage.indexedDB); - if (storage.localStorage.length || storage.indexedDB?.length) + serializeRecords(storage.indexedDB); + if (storage.localStorage.length || storage.indexedDB.length) result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB } as channels.OriginStorage); } await page.close(internalMetadata); @@ -707,7 +696,7 @@ export abstract class BrowserContext extends SdkObject { for (const { name, value } of (originState.localStorage || [])) localStorage.setItem(name, value); - await Promise.all((originState.indexedDB || []).map(async dbInfo => { + await Promise.all(originState.indexedDB.map(async dbInfo => { await new Promise((resolve, reject) => { const openRequest = indexedDB.open(dbInfo.name, dbInfo.version); openRequest.addEventListener('upgradeneeded', () => { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 5e09d4ac46..2b0b292497 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -9260,7 +9260,8 @@ export interface BrowserContext { setOffline(offline: boolean): Promise; /** - * Returns storage state for this browser context, contains current cookies and local storage snapshot. + * Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB + * snapshot. * @param options */ storageState(options?: { @@ -9301,6 +9302,40 @@ export interface BrowserContext { 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?: string; + + value: string; + }>; + }>; + }>; }>; }>; @@ -10062,10 +10097,37 @@ export interface Browser { }>; indexedDB?: Array<{ - /** - * TODO: document more - */ 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?: string; + + value: string; + }>; + }>; }>; }>; }; @@ -22236,10 +22298,37 @@ export interface BrowserContextOptions { }>; indexedDB?: Array<{ - /** - * TODO: document more - */ 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?: string; + + value: string; + }>; + }>; }>; }>; }; diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index f1cee7bcc1..e2c53e4e99 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -296,7 +296,7 @@ export type IndexedDBDatabase = { export type OriginStorage = { origin: string, localStorage: NameValue[], - indexedDB?: IndexedDBDatabase[], + indexedDB: IndexedDBDatabase[], }; export type SerializedError = { diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 4a70663154..3b416c4526 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -262,7 +262,7 @@ OriginStorage: type: array items: NameValue indexedDB: - type: array? + type: array items: IndexedDBDatabase SerializedError: