Skip to content

Commit 311625b

Browse files
authored
feat: recreate IndexedDB in storagestate (#34591)
1 parent fb3e8ed commit 311625b

File tree

24 files changed

+1521
-52
lines changed

24 files changed

+1521
-52
lines changed

docs/src/api/class-apirequest.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,23 @@ Methods like [`method: APIRequestContext.get`] take the base URL into considerat
6464
- `localStorage` <[Array]<[Object]>>
6565
- `name` <[string]>
6666
- `value` <[string]>
67+
- `indexedDB` ?<[Array]<[Object]>> indexedDB to set for context
68+
- `name` <[string]> database name
69+
- `version` <[int]> database version
70+
- `stores` <[Array]<[Object]>>
71+
- `name` <[string]>
72+
- `keyPath` ?<[string]>
73+
- `keyPathArray` ?<[Array]<[string]>>
74+
- `autoIncrement` <[boolean]>
75+
- `indexes` <[Array]<[Object]>>
76+
- `name` <[string]>
77+
- `keyPath` ?<[string]>
78+
- `keyPathArray` ?<[Array]<[string]>>
79+
- `unique` <[boolean]>
80+
- `multiEntry` <[boolean]>
81+
- `records` <[Array]<[Object]>>
82+
- `key` ?<[Object]>
83+
- `value` <[Object]>
6784

6885
Populates context with given storage state. This option can be used to initialize context with logged-in information
6986
obtained via [`method: BrowserContext.storageState`] or [`method: APIRequestContext.storageState`]. Either a path to the

docs/src/api/class-apirequestcontext.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,23 @@ context cookies from the response. The method will automatically follow redirect
880880
- `localStorage` <[Array]<[Object]>>
881881
- `name` <[string]>
882882
- `value` <[string]>
883+
- `indexedDB` <[Array]<[Object]>>
884+
- `name` <[string]>
885+
- `version` <[int]>
886+
- `stores` <[Array]<[Object]>>
887+
- `name` <[string]>
888+
- `keyPath` ?<[string]>
889+
- `keyPathArray` ?<[Array]<[string]>>
890+
- `autoIncrement` <[boolean]>
891+
- `indexes` <[Array]<[Object]>>
892+
- `name` <[string]>
893+
- `keyPath` ?<[string]>
894+
- `keyPathArray` ?<[Array]<[string]>>
895+
- `unique` <[boolean]>
896+
- `multiEntry` <[boolean]>
897+
- `records` <[Array]<[Object]>>
898+
- `key` ?<[Object]>
899+
- `value` <[Object]>
883900

884901
Returns storage state for this request context, contains current cookies and local storage snapshot if it was passed to the constructor.
885902

docs/src/api/class-browsercontext.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,8 +1511,25 @@ Whether to emulate network being offline for the browser context.
15111511
- `localStorage` <[Array]<[Object]>>
15121512
- `name` <[string]>
15131513
- `value` <[string]>
1514-
1515-
Returns storage state for this browser context, contains current cookies and local storage snapshot.
1514+
- `indexedDB` <[Array]<[Object]>>
1515+
- `name` <[string]>
1516+
- `version` <[int]>
1517+
- `stores` <[Array]<[Object]>>
1518+
- `name` <[string]>
1519+
- `keyPath` ?<[string]>
1520+
- `keyPathArray` ?<[Array]<[string]>>
1521+
- `autoIncrement` <[boolean]>
1522+
- `indexes` <[Array]<[Object]>>
1523+
- `name` <[string]>
1524+
- `keyPath` ?<[string]>
1525+
- `keyPathArray` ?<[Array]<[string]>>
1526+
- `unique` <[boolean]>
1527+
- `multiEntry` <[boolean]>
1528+
- `records` <[Array]<[Object]>>
1529+
- `key` ?<[Object]>
1530+
- `value` <[Object]>
1531+
1532+
Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
15161533

15171534
## async method: BrowserContext.storageState
15181535
* since: v1.8

docs/src/api/params.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,11 +259,28 @@ Specify environment variables that will be visible to the browser. Defaults to `
259259
- `httpOnly` <[boolean]>
260260
- `secure` <[boolean]>
261261
- `sameSite` <[SameSiteAttribute]<"Strict"|"Lax"|"None">> sameSite flag
262-
- `origins` <[Array]<[Object]>> localStorage to set for context
262+
- `origins` <[Array]<[Object]>>
263263
- `origin` <[string]>
264-
- `localStorage` <[Array]<[Object]>>
264+
- `localStorage` <[Array]<[Object]>> localStorage to set for context
265265
- `name` <[string]>
266266
- `value` <[string]>
267+
- `indexedDB` ?<[Array]<[Object]>> indexedDB to set for context
268+
- `name` <[string]> database name
269+
- `version` <[int]> database version
270+
- `stores` <[Array]<[Object]>>
271+
- `name` <[string]>
272+
- `keyPath` ?<[string]>
273+
- `keyPathArray` ?<[Array]<[string]>>
274+
- `autoIncrement` <[boolean]>
275+
- `indexes` <[Array]<[Object]>>
276+
- `name` <[string]>
277+
- `keyPath` ?<[string]>
278+
- `keyPathArray` ?<[Array]<[string]>>
279+
- `unique` <[boolean]>
280+
- `multiEntry` <[boolean]>
281+
- `records` <[Array]<[Object]>>
282+
- `key` ?<[Object]>
283+
- `value` <[Object]>
267284

268285
Learn more about [storage state and auth](../auth.md).
269286

docs/src/auth.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,9 @@ existing authentication state instead.
266266
Playwright provides a way to reuse the signed-in state in the tests. That way you can log
267267
in only once and then skip the log in step for all of the tests.
268268

269-
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) or in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage). 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.
269+
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.
270270

271-
Cookies and local storage state can be used across different browsers. They depend on your application's authentication model: some apps might require both cookies and local storage.
271+
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.
272272

273273
The following code snippet retrieves state from an authenticated context and creates a new context with that state.
274274

@@ -583,7 +583,7 @@ test('admin and user', async ({ adminPage, userPage }) => {
583583

584584
### Session storage
585585

586-
Reusing authenticated state covers [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) and [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage) based authentication. Rarely, [session storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) is used for storing information associated with the signed-in state. Session storage is specific to a particular domain and is not persisted across page loads. Playwright does not provide API to persist session storage, but the following snippet can be used to save/load session storage.
586+
Reusing authenticated state covers [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage) and [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) based authentication. Rarely, [session storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) is used for storing information associated with the signed-in state. Session storage is specific to a particular domain and is not persisted across page loads. Playwright does not provide API to persist session storage, but the following snippet can be used to save/load session storage.
587587

588588
```js
589589
// Get session storage and store as env variable

docs/src/codegen.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ pwsh bin/Debug/netX/playwright.ps1 codegen --timezone="Europe/Rome" --geolocatio
325325

326326
### Preserve authenticated state
327327

328-
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.
328+
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.
329329

330330
```bash js
331331
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
375375

376376
#### Load authenticated state
377377

378-
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.
378+
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.
379379

380380
```bash js
381381
npx playwright codegen --load-storage=auth.json github.com/microsoft/playwright

packages/playwright-core/src/client/browserContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { Worker } from './worker';
2828
import { Events } from './events';
2929
import { TimeoutSettings } from '../common/timeoutSettings';
3030
import { Waiter } from './waiter';
31-
import type { Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types';
31+
import type { Headers, WaitForEventOptions, BrowserContextOptions, LaunchOptions, StorageState } from './types';
3232
import { type URLMatch, headersObjectToArray, isRegExp, isString, urlMatchesEqual, mkdirIfNeeded } from '../utils';
3333
import type * as api from '../../types/types';
3434
import type * as structs from '../../types/structs';

packages/playwright-core/src/client/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export type StorageState = {
4141
};
4242
export type SetStorageState = {
4343
cookies?: channels.SetNetworkCookie[],
44-
origins?: channels.OriginStorage[]
44+
origins?: channels.SetOriginStorage[]
4545
};
4646

4747
export type LifecycleEvent = channels.LifecycleEvent;

packages/playwright-core/src/protocol/validator.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,36 @@ scheme.NameValue = tObject({
142142
name: tString,
143143
value: tString,
144144
});
145+
scheme.IndexedDBDatabase = tObject({
146+
name: tString,
147+
version: tNumber,
148+
stores: tArray(tObject({
149+
name: tString,
150+
autoIncrement: tBoolean,
151+
keyPath: tOptional(tString),
152+
keyPathArray: tOptional(tArray(tString)),
153+
records: tArray(tObject({
154+
key: tOptional(tAny),
155+
value: tAny,
156+
})),
157+
indexes: tArray(tObject({
158+
name: tString,
159+
keyPath: tOptional(tString),
160+
keyPathArray: tOptional(tArray(tString)),
161+
multiEntry: tBoolean,
162+
unique: tBoolean,
163+
})),
164+
})),
165+
});
166+
scheme.SetOriginStorage = tObject({
167+
origin: tString,
168+
localStorage: tArray(tType('NameValue')),
169+
indexedDB: tOptional(tArray(tType('IndexedDBDatabase'))),
170+
});
145171
scheme.OriginStorage = tObject({
146172
origin: tString,
147173
localStorage: tArray(tType('NameValue')),
174+
indexedDB: tArray(tType('IndexedDBDatabase')),
148175
});
149176
scheme.SerializedError = tObject({
150177
error: tOptional(tObject({
@@ -361,7 +388,7 @@ scheme.PlaywrightNewRequestParams = tObject({
361388
timeout: tOptional(tNumber),
362389
storageState: tOptional(tObject({
363390
cookies: tOptional(tArray(tType('NetworkCookie'))),
364-
origins: tOptional(tArray(tType('OriginStorage'))),
391+
origins: tOptional(tArray(tType('SetOriginStorage'))),
365392
})),
366393
tracesDir: tOptional(tString),
367394
});
@@ -689,7 +716,7 @@ scheme.BrowserNewContextParams = tObject({
689716
})),
690717
storageState: tOptional(tObject({
691718
cookies: tOptional(tArray(tType('SetNetworkCookie'))),
692-
origins: tOptional(tArray(tType('OriginStorage'))),
719+
origins: tOptional(tArray(tType('SetOriginStorage'))),
693720
})),
694721
});
695722
scheme.BrowserNewContextResult = tObject({
@@ -759,7 +786,7 @@ scheme.BrowserNewContextForReuseParams = tObject({
759786
})),
760787
storageState: tOptional(tObject({
761788
cookies: tOptional(tArray(tType('SetNetworkCookie'))),
762-
origins: tOptional(tArray(tType('OriginStorage'))),
789+
origins: tOptional(tArray(tType('SetOriginStorage'))),
763790
})),
764791
});
765792
scheme.BrowserNewContextForReuseResult = tObject({

packages/playwright-core/src/server/browserContext.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import type { Artifact } from './artifact';
4343
import { Clock } from './clock';
4444
import type { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
4545
import { RecorderApp } from './recorder/recorderApp';
46+
import * as storageScript from './storageScript';
4647

4748
export abstract class BrowserContext extends SdkObject {
4849
static Events = {
@@ -519,11 +520,9 @@ export abstract class BrowserContext extends SdkObject {
519520
if (!origin || !originsToSave.has(origin))
520521
continue;
521522
try {
522-
const storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`({
523-
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
524-
})`, 'utility');
525-
if (storage.localStorage.length)
526-
result.origins.push({ origin, localStorage: storage.localStorage } as channels.OriginStorage);
523+
const storage: storageScript.Storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`(${storageScript.collect})()`, 'utility');
524+
if (storage.localStorage.length || storage.indexedDB.length)
525+
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB });
527526
originsToSave.delete(origin);
528527
} catch {
529528
// When failed on the live page, we'll retry on the blank page below.
@@ -539,15 +538,11 @@ export abstract class BrowserContext extends SdkObject {
539538
return true;
540539
});
541540
for (const origin of originsToSave) {
542-
const originStorage: channels.OriginStorage = { origin, localStorage: [] };
543541
const frame = page.mainFrame();
544542
await frame.goto(internalMetadata, origin);
545-
const storage = await frame.evaluateExpression(`({
546-
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
547-
})`, { world: 'utility' });
548-
originStorage.localStorage = storage.localStorage;
549-
if (storage.localStorage.length)
550-
result.origins.push(originStorage);
543+
const storage: Awaited<ReturnType<typeof storageScript.collect>> = await frame.evaluateExpression(`(${storageScript.collect})()`, { world: 'utility' });
544+
if (storage.localStorage.length || storage.indexedDB.length)
545+
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB });
551546
}
552547
await page.close(internalMetadata);
553548
}
@@ -610,11 +605,7 @@ export abstract class BrowserContext extends SdkObject {
610605
for (const originState of state.origins) {
611606
const frame = page.mainFrame();
612607
await frame.goto(metadata, originState.origin);
613-
await frame.evaluateExpression(`
614-
originState => {
615-
for (const { name, value } of (originState.localStorage || []))
616-
localStorage.setItem(name, value);
617-
}`, { isFunction: true, world: 'utility' }, originState);
608+
await frame.evaluateExpression(storageScript.restore.toString(), { isFunction: true, world: 'utility' }, originState);
618609
}
619610
await page.close(internalMetadata);
620611
}

0 commit comments

Comments
 (0)