This commit is contained in:
Simon Knott 2025-02-04 10:56:05 +01:00
parent e26bd2a0c0
commit da0ad1ef75
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
8 changed files with 153 additions and 43 deletions

View file

@ -1511,8 +1511,25 @@ Whether to emulate network being offline for the browser context.
- `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` ?<[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 ## async method: BrowserContext.storageState
* since: v1.8 * since: v1.8

View file

@ -265,7 +265,22 @@ Specify environment variables that will be visible to the browser. Defaults to `
- `name` <[string]> - `name` <[string]>
- `value` <[string]> - `value` <[string]>
- `indexedDB` ?<[Array]<[Object]>> - `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). Learn more about [storage state and auth](../auth.md).

View file

@ -325,7 +325,7 @@ pwsh bin/Debug/netX/playwright.ps1 codegen --timezone="Europe/Rome" --geolocatio
### Preserve authenticated state ### 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 ```bash js
npx playwright codegen github.com/microsoft/playwright --save-storage=auth.json 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 #### 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 ```bash js
npx playwright codegen --load-storage=auth.json github.com/microsoft/playwright npx playwright codegen --load-storage=auth.json github.com/microsoft/playwright

View file

@ -166,7 +166,7 @@ scheme.IndexedDBDatabase = tObject({
scheme.OriginStorage = tObject({ scheme.OriginStorage = tObject({
origin: tString, origin: tString,
localStorage: tArray(tType('NameValue')), localStorage: tArray(tType('NameValue')),
indexedDB: tOptional(tArray(tType('IndexedDBDatabase'))), indexedDB: tArray(tType('IndexedDBDatabase')),
}); });
scheme.SerializedError = tObject({ scheme.SerializedError = tObject({
error: tOptional(tObject({ error: tOptional(tObject({

View file

@ -515,20 +515,16 @@ export abstract class BrowserContext extends SdkObject {
const originsToSave = new Set(this._origins); const originsToSave = new Set(this._origins);
async function _collectStorageScript() { async function _collectStorageScript() {
async function _collectDatabase(dbInfo: IDBDatabaseInfo) {
if (!dbInfo.name)
return;
let db: IDBDatabase; const idbResult = await Promise.all((await indexedDB.databases()).map(async dbInfo => {
try { if (!dbInfo.name)
db = await new Promise((resolve, reject) => { throw new Error('Database name is empty');
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(dbInfo.name!); const request = indexedDB.open(dbInfo.name!);
request.onerror = reject; request.onerror = reject;
request.onsuccess = () => resolve(request.result); request.onsuccess = () => resolve(request.result);
}); });
} catch {
return;
}
const transaction = db.transaction(db.objectStoreNames, 'readonly'); const transaction = db.transaction(db.objectStoreNames, 'readonly');
const stores = await Promise.all([...db.objectStoreNames].map(async storeName => { 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 records = await Promise.all(keys.map(async key => {
const record = await new Promise<any[]>((resolve, reject) => { const record = await new Promise<any>((resolve, reject) => {
const request = objectStore.get(key); const request = objectStore.get(key);
request.addEventListener('success', () => resolve(request.result)); request.addEventListener('success', () => resolve(request.result));
request.addEventListener('error', reject); request.addEventListener('error', reject);
}); });
if (!record)
return;
return { return {
key: objectStore.keyPath === null ? key.toString() : undefined, key: objectStore.keyPath === null ? key.toString() : undefined,
value: record value: record
}; };
})); }));
const indexes = await Promise.all([...objectStore.indexNames].map(async indexName => { const indexes = [...objectStore.indexNames].map(indexName => {
const index = objectStore.index(indexName); const index = objectStore.index(indexName);
return { return {
name: index.name, name: index.name,
@ -564,7 +557,7 @@ export abstract class BrowserContext extends SdkObject {
multiEntry: index.multiEntry, multiEntry: index.multiEntry,
unique: index.unique, unique: index.unique,
}; };
})); });
return { return {
name: storeName, name: storeName,
@ -581,13 +574,11 @@ export abstract class BrowserContext extends SdkObject {
version: dbInfo.version, version: dbInfo.version,
stores, stores,
}; };
} }));
const idbResult = await Promise.all((await indexedDB.databases()).map(_collectDatabase).filter(Boolean));
return { return {
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })), localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
indexedDB: idbResult.length ? idbResult : undefined, indexedDB: idbResult,
}; };
} }
@ -607,7 +598,6 @@ 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');
if (storage.indexedDB?.length)
serializeRecords(storage.indexedDB); 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.OriginStorage); result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB } as channels.OriginStorage);
@ -629,9 +619,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' });
if (storage.indexedDB?.length)
serializeRecords(storage.indexedDB); 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.OriginStorage); result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB } as channels.OriginStorage);
} }
await page.close(internalMetadata); await page.close(internalMetadata);
@ -707,7 +696,7 @@ export abstract class BrowserContext extends SdkObject {
for (const { name, value } of (originState.localStorage || [])) for (const { name, value } of (originState.localStorage || []))
localStorage.setItem(name, value); localStorage.setItem(name, value);
await Promise.all((originState.indexedDB || []).map(async dbInfo => { await Promise.all(originState.indexedDB.map(async dbInfo => {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const openRequest = indexedDB.open(dbInfo.name, dbInfo.version); const openRequest = indexedDB.open(dbInfo.name, dbInfo.version);
openRequest.addEventListener('upgradeneeded', () => { openRequest.addEventListener('upgradeneeded', () => {

View file

@ -9260,7 +9260,8 @@ export interface BrowserContext {
setOffline(offline: boolean): Promise<void>; setOffline(offline: boolean): Promise<void>;
/** /**
* 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 * @param options
*/ */
storageState(options?: { storageState(options?: {
@ -9301,6 +9302,40 @@ export interface BrowserContext {
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?: string;
value: string;
}>;
}>;
}>;
}>; }>;
}>; }>;
@ -10062,10 +10097,37 @@ export interface Browser {
}>; }>;
indexedDB?: Array<{ indexedDB?: Array<{
/**
* TODO: document more
*/
name: string; 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?: string;
value: string;
}>;
}>;
}>; }>;
}>; }>;
}; };
@ -22236,10 +22298,37 @@ export interface BrowserContextOptions {
}>; }>;
indexedDB?: Array<{ indexedDB?: Array<{
/**
* TODO: document more
*/
name: string; 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?: string;
value: string;
}>;
}>;
}>; }>;
}>; }>;
}; };

View file

@ -296,7 +296,7 @@ export type IndexedDBDatabase = {
export type OriginStorage = { export type OriginStorage = {
origin: string, origin: string,
localStorage: NameValue[], localStorage: NameValue[],
indexedDB?: IndexedDBDatabase[], indexedDB: IndexedDBDatabase[],
}; };
export type SerializedError = { export type SerializedError = {

View file

@ -262,7 +262,7 @@ OriginStorage:
type: array type: array
items: NameValue items: NameValue
indexedDB: indexedDB:
type: array? type: array
items: IndexedDBDatabase items: IndexedDBDatabase
SerializedError: SerializedError: