Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/src/api/params.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,11 +259,13 @@ Specify environment variables that will be visible to the browser. Defaults to `
- `httpOnly` <[boolean]>
- `secure` <[boolean]>
- `sameSite` <[SameSiteAttribute]<"Strict"|"Lax"|"None">> sameSite flag
- `origins` <[Array]<[Object]>> localStorage to set for context
- `origins` <[Array]<[Object]>>
- `origin` <[string]>
- `localStorage` <[Array]<[Object]>>
- `localStorage` <[Array]<[Object]>> localStorage to set for context
- `name` <[string]>
- `value` <[string]>
- `indexedDB` ?<[Array]<[Object]>>
- `name` <[string]> TODO: document more

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

Expand Down
20 changes: 20 additions & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,29 @@ scheme.NameValue = tObject({
name: tString,
value: tString,
});
scheme.IndexedDBDatabase = tObject({
name: tString,
version: tNumber,
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({
Expand Down
134 changes: 117 additions & 17 deletions packages/playwright-core/src/server/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,17 +513,90 @@ 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 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<any[]>((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<any[]>((resolve, reject) => {
const request = objectStore.get(key);
request.addEventListener('success', () => resolve(request.result));
request.addEventListener('error', reject);
});

if (!record)
return;

return {
key: key.toString(),
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,
};
}

// First try collecting storage stage from existing pages.
for (const page of this.pages()) {
const origin = page.mainFrame().origin();
if (!origin || !originsToSave.has(origin))
continue;
try {
const storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`({
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
})`, 'utility');
if (storage.localStorage.length)
result.origins.push({ origin, localStorage: storage.localStorage } as channels.OriginStorage);
const storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`(${_collectStorageScript.toString()})()`, 'utility');
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.
Expand All @@ -539,15 +612,11 @@ export abstract class BrowserContext extends SdkObject {
return true;
});
for (const origin of originsToSave) {
const originStorage: channels.OriginStorage = { origin, localStorage: [] };
const frame = page.mainFrame();
await frame.goto(internalMetadata, origin);
const storage = await frame.evaluateExpression(`({
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
})`, { world: 'utility' });
originStorage.localStorage = storage.localStorage;
if (storage.localStorage.length)
result.origins.push(originStorage);
const storage = await frame.evaluateExpression(`(${_collectStorageScript.toString()})()`, { world: 'utility' });
if (storage.localStorage.length || storage.indexedDB?.length)
result.origins.push({ origin, localStorage: storage.localStorage, indexedDB: storage.indexedDB } as channels.OriginStorage);
}
await page.close(internalMetadata);
}
Expand Down Expand Up @@ -610,11 +679,42 @@ export abstract class BrowserContext extends SdkObject {
for (const originState of state.origins) {
const frame = page.mainFrame();
await frame.goto(metadata, originState.origin);
await frame.evaluateExpression(`
originState => {
for (const { name, value } of (originState.localStorage || []))
localStorage.setItem(name, value);
}`, { isFunction: true, world: 'utility' }, originState);

async function _restoreStorageState(originState: channels.OriginStorage) {
for (const { name, value } of (originState.localStorage || []))
localStorage.setItem(name, value);

await Promise.all((originState.indexedDB || []).map(async dbInfo => {
await new Promise((resolve, reject) => {
const openRequest = indexedDB.open(dbInfo.name, dbInfo.version);
openRequest.addEventListener('upgradeneeded', () => {
const db = openRequest.result;
for (const store of dbInfo.stores) {
const objectStore = db.createObjectStore(store.name, { autoIncrement: store.autoIncrement, keyPath: store.keyPath });
for (const index of store.indexes)
objectStore.createIndex(index.name, index.keyPath, { unique: index.unique, multiEntry: index.multiEntry });
}
});
openRequest.addEventListener('success', async () => {
const db = openRequest.result;
const transaction = db.transaction(db.objectStoreNames, 'readwrite');
Promise.all(dbInfo.stores.flatMap(store => {
const objectStore = transaction.objectStore(store.name);
return store.records.map(record => new Promise((resolve, reject) => {
const request = objectStore.add(
JSON.parse(record.value),
objectStore.keyPath === null ? record.key : undefined
);
request.addEventListener('success', resolve);
request.addEventListener('error', reject);
}));
})).then(resolve, reject);
});
});
}));
}

await frame.evaluateExpression(_restoreStorageState.toString(), { isFunction: true, world: 'utility' }, originState);
}
await page.close(internalMetadata);
}
Expand Down
26 changes: 20 additions & 6 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10049,17 +10049,24 @@ export interface Browser {
sameSite: "Strict"|"Lax"|"None";
}>;

/**
* localStorage to set for context
*/
origins: Array<{
origin: string;

/**
* localStorage to set for context
*/
localStorage: Array<{
name: string;

value: string;
}>;

indexedDB?: Array<{
/**
* TODO: document more
*/
name: string;
}>;
}>;
};

Expand Down Expand Up @@ -22216,17 +22223,24 @@ export interface BrowserContextOptions {
sameSite: "Strict"|"Lax"|"None";
}>;

/**
* localStorage to set for context
*/
origins: Array<{
origin: string;

/**
* localStorage to set for context
*/
localStorage: Array<{
name: string;

value: string;
}>;

indexedDB?: Array<{
/**
* TODO: document more
*/
name: string;
}>;
}>;
};

Expand Down
21 changes: 21 additions & 0 deletions packages/protocol/src/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,9 +271,30 @@ export type NameValue = {
value: string,
};

export type IndexedDBDatabase = {
name: string,
version: number,
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 = {
Expand Down
37 changes: 36 additions & 1 deletion packages/protocol/src/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,39 @@ NameValue:
name: string
value: string

IndexedDBDatabase:
type: object
properties:
name: string
version: number
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
Expand All @@ -230,7 +263,9 @@ OriginStorage:
localStorage:
type: array
items: NameValue

indexedDB:
type: array?
items: IndexedDBDatabase

SerializedError:
type: object
Expand Down
Loading
Loading