diff --git a/docs/src/api/params.md b/docs/src/api/params.md index d161f91066..92d046c482 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -264,6 +264,8 @@ Specify environment variables that will be visible to the browser. Defaults to ` - `localStorage` <[Array]<[Object]>> - `name` <[string]> - `value` <[string]> + - `indexedDB` ?<[Array]<[Object]>> + - `name` <[string]> Learn more about [storage state and auth](../auth.md). diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index cdcef14996..0eff23f3fc 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -142,9 +142,28 @@ scheme.NameValue = tObject({ name: tString, value: tString, }); +scheme.IndexedDBDatabase = tObject({ + name: tString, + stores: tArray(tObject({ + name: tString, + autoIncrement: tBoolean, + keyPath: tArray(tString), + records: tArray(tObject({ + key: tString, + value: tString, + })), + indexes: tArray(tObject({ + name: tString, + keyPath: tArray(tString), + multiEntry: tBoolean, + unique: tBoolean, + })), + })), +}); scheme.OriginStorage = tObject({ origin: tString, localStorage: tArray(tType('NameValue')), + indexedDB: tOptional(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 091d31d6cb..17a802dbff 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -513,9 +513,78 @@ export abstract class BrowserContext extends SdkObject { }; const originsToSave = new Set(this._origins); - function _collectStorageScript() { + 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 transaction = db.transaction(db.objectStoreNames, 'readonly'); + const stores = await Promise.all([...db.objectStoreNames].map(async storeName => { + const objectStore = transaction.objectStore(storeName); + const keys = await new Promise((resolve, reject) => { + const request = objectStore.getAllKeys(); + request.addEventListener('success', () => resolve(request.result)); + request.addEventListener('error', reject); + }); + + const records = await Promise.all(keys.map(async key => { + 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, + value: JSON.stringify(record) + }; + })); + + const indexes = await Promise.all([...objectStore.indexNames].map(async indexName => { + const index = objectStore.index(indexName); + return { + name: index.name, + keyPath: Array.isArray(index.keyPath) ? index.keyPath : [index.keyPath], + multiEntry: index.multiEntry, + unique: index.unique, + }; + })); + + return { + name: storeName, + records: records.filter(Boolean), + indexes, + autoIncrement: objectStore.autoIncrement, + keyPath: Array.isArray(objectStore.keyPath) ? objectStore.keyPath : [objectStore.keyPath], + }; + })); + + return { + name: dbInfo.name, + 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, }; } @@ -526,8 +595,8 @@ export abstract class BrowserContext extends SdkObject { continue; try { const storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`(${_collectStorageScript.toString()})()`, 'utility'); - if (storage.localStorage.length) - result.origins.push({ origin, localStorage: storage.localStorage } as channels.OriginStorage); + if (storage.localStorage.length || storage.indexedDB?.length) + 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. @@ -546,8 +615,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.localStorage.length) - result.origins.push({ origin, localStorage: storage.localStorage } as channels.OriginStorage); + if (storage.localStorage.length || storage.indexedDB?.length) + result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB } as channels.OriginStorage); } await page.close(internalMetadata); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 67efa84ec6..14725e558a 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -10060,6 +10060,10 @@ export interface Browser { value: string; }>; + + indexedDB?: Array<{ + name: string; + }>; }>; }; @@ -22227,6 +22231,10 @@ export interface BrowserContextOptions { value: string; }>; + + indexedDB?: Array<{ + name: string; + }>; }>; }; diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 30f8c03088..bc43db1165 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -271,9 +271,29 @@ export type NameValue = { value: string, }; +export type IndexedDBDatabase = { + name: string, + stores: { + name: string, + autoIncrement: boolean, + keyPath: string[], + records: { + key: string, + value: string, + }[], + indexes: { + name: string, + keyPath: string[], + multiEntry: boolean, + unique: boolean, + }[], + }[], +}; + export type OriginStorage = { origin: string, localStorage: NameValue[], + indexedDB?: IndexedDBDatabase[], }; export type SerializedError = { diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 77501efa9b..99e126dff6 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -222,6 +222,38 @@ NameValue: name: string value: string +IndexedDBDatabase: + type: object + properties: + name: string + stores: + type: array + items: + type: object + properties: + name: string + autoIncrement: boolean + keyPath: + type: array + items: string + records: + type: array + items: + type: object + properties: + key: string + value: string + indexes: + type: array + items: + type: object + properties: + name: string + keyPath: + type: array + items: string + multiEntry: boolean + unique: boolean OriginStorage: type: object @@ -230,7 +262,9 @@ OriginStorage: localStorage: type: array items: NameValue - + indexedDB: + type: array? + items: IndexedDBDatabase SerializedError: type: object diff --git a/tests/library/browsercontext-storage-state.spec.ts b/tests/library/browsercontext-storage-state.spec.ts index d71657d1be..e9e7443558 100644 --- a/tests/library/browsercontext-storage-state.spec.ts +++ b/tests/library/browsercontext-storage-state.spec.ts @@ -316,3 +316,83 @@ it('should roundtrip local storage in third-party context', async ({ page, conte expect(localStorage).toEqual({ name1: 'value1' }); await context2.close(); }); + +it('should support IndexedDB', async ({ page, contextFactory }) => { + await page.goto('https://mdn.github.io/dom-examples/to-do-notifications/'); + await page.getByLabel('Task title').fill('Pet the cat'); + await page.getByLabel('Hours').fill('1'); + await page.getByLabel('Mins').fill('1'); + await page.getByText('Add Task').click(); + + const storageState = await page.context().storageState(); + expect(storageState.origins).toEqual([ + { + origin: 'https://mdn.github.io', + localStorage: [], + indexedDB: [ + { + name: 'toDoList', + stores: [ + { + name: 'toDoList', + autoIncrement: false, + keyPath: ['taskTitle'], + records: [ + { + key: 'Pet the cat', + value: JSON.stringify({ + taskTitle: 'Pet the cat', + hours: '1', + minutes: '1', + day: '01', + month: 'January', + year: '2025', + notified: 'no' + }) + } + ], + indexes: [ + { + 'name': 'day', + 'keyPath': ['day'], + 'multiEntry': false, + 'unique': false, + }, + { + 'name': 'hours', + 'keyPath': ['hours'], + 'multiEntry': false, + 'unique': false, + }, + { + 'name': 'minutes', + 'keyPath': ['minutes'], + 'multiEntry': false, + 'unique': false, + }, + { + 'name': 'month', + 'keyPath': ['month'], + 'multiEntry': false, + 'unique': false, + }, + { + 'name': 'notified', + 'keyPath': ['notified'], + 'multiEntry': false, + 'unique': false, + }, + { + 'name': 'year', + 'keyPath': ['year'], + 'multiEntry': false, + 'unique': false, + }, + ] + } + ] + } + ] + } + ]); +});