store data

This commit is contained in:
Simon Knott 2025-02-03 12:26:30 +01:00
parent e3cc3a7f62
commit e596207f99
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
7 changed files with 238 additions and 6 deletions

View file

@ -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).

View file

@ -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({

View file

@ -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<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,
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);
}

View file

@ -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;
}>;
}>;
};

View file

@ -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 = {

View file

@ -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

View file

@ -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,
},
]
}
]
}
]
}
]);
});