diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 3e7529c4828ef..773ee276ff831 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1500,6 +1500,46 @@ Returns storage state for this browser context, contains current cookies, local Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage state snapshot. If your application uses IndexedDB to store authentication tokens, like Firebase Authentication, enable this. +## async method: BrowserContext.setStorageState +* since: v1.59 + +Clears the existing cookies, local storage and IndexedDB entries for all origins and sets the new storage state. + +**Usage** + +```js +// Load storage state from a file and apply it to the context. +await context.setStorageState('state.json'); +``` + +```java +// Load storage state from a file and apply it to the context. +context.setStorageState(Paths.get("state.json")); +``` + +```python async +# Load storage state from a file and apply it to the context. +await context.set_storage_state("state.json") +``` + +```python sync +# Load storage state from a file and apply it to the context. +context.set_storage_state("state.json") +``` + +```csharp +// Load storage state from a file and apply it to the context. +await context.SetStorageStateAsync("state.json"); +``` + +### param: BrowserContext.setStorageState.storageState = %%-js-python-context-option-storage-state-%% +* since: v1.59 +* langs: js, python + +### param: BrowserContext.setStorageState.storageState = %%-csharp-java-context-option-storage-state-path-%% +* since: v1.59 +* langs: csharp, java + ## property: BrowserContext.tracing * since: v1.12 - type: <[Tracing]> diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 50dd490708e13..e810de1338eb5 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -9611,6 +9611,71 @@ export interface BrowserContext { */ setOffline(offline: boolean): Promise; + /** + * Clears the existing cookies, local storage and IndexedDB entries for all origins and sets the new storage state. + * + * **Usage** + * + * ```js + * // Load storage state from a file and apply it to the context. + * await context.setStorageState('state.json'); + * ``` + * + * @param storageState Learn more about [storage state and auth](https://playwright.dev/docs/auth). + * + * Populates context with given storage state. This option can be used to initialize context with logged-in + * information obtained via + * [browserContext.storageState([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state). + */ + setStorageState(storageState: string|{ + /** + * Cookies to set for context + */ + cookies: Array<{ + name: string; + + value: string; + + /** + * Domain and path are required. For the cookie to apply to all subdomains as well, prefix domain with a dot, like + * this: ".example.com" + */ + domain: string; + + /** + * Domain and path are required + */ + path: string; + + /** + * Unix time in seconds. + */ + expires: number; + + httpOnly: boolean; + + secure: boolean; + + /** + * sameSite flag + */ + sameSite: "Strict"|"Lax"|"None"; + }>; + + origins: Array<{ + origin: string; + + /** + * localStorage to set for context + */ + localStorage: Array<{ + name: string; + + value: string; + }>; + }>; + }): Promise; + /** * Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB * snapshot. diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index fedd4c2b7ed9f..554fc7a6744c5 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -471,6 +471,11 @@ export class BrowserContext extends ChannelOwner return state; } + async setStorageState(storageState: string | SetStorageState): Promise { + const state = await prepareStorageState(this._platform, storageState); + await this._channel.setStorageState({ storageState: state }); + } + backgroundPages(): Page[] { return []; } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index bc253a5a7be63..9dab9b3ce1d28 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1051,6 +1051,13 @@ scheme.BrowserContextStorageStateResult = tObject({ cookies: tArray(tType('NetworkCookie')), origins: tArray(tType('OriginStorage')), }); +scheme.BrowserContextSetStorageStateParams = tObject({ + storageState: tOptional(tObject({ + cookies: tOptional(tArray(tType('SetNetworkCookie'))), + origins: tOptional(tArray(tType('SetOriginStorage'))), + })), +}); +scheme.BrowserContextSetStorageStateResult = tOptional(tObject({})); scheme.BrowserContextPauseParams = tOptional(tObject({})); scheme.BrowserContextPauseResult = tOptional(tObject({})); scheme.BrowserContextEnableRecorderParams = tObject({ diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index f8d1f2507b0b7..7a7c0f93ff700 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -644,7 +644,7 @@ export abstract class BrowserContext extends Sdk return this._creatingStorageStatePage; } - async setStorageState(progress: Progress, state: channels.BrowserNewContextParams['storageState'], mode: 'initial' | 'resetForReuse') { + async setStorageState(progress: Progress, state: channels.BrowserNewContextParams['storageState'], mode: 'initial' | 'resetForReuse' | 'api') { let page: Page | undefined; let interceptor: network.RouteHandler | undefined; try { diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index abbbe5ac53469..9f38c825a6da8 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -334,6 +334,10 @@ export class BrowserContextDispatcher extends Dispatcher { + await this._context.setStorageState(progress, params.storageState, 'api'); + } + async close(params: channels.BrowserContextCloseParams, progress: Progress): Promise { progress.metadata.potentiallyClosesScope = true; await this._context.close(params); diff --git a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts index 4da75fa607c63..296878c9a182b 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts @@ -20,7 +20,7 @@ export const methodMetainfo = new Map; + /** + * Clears the existing cookies, local storage and IndexedDB entries for all origins and sets the new storage state. + * + * **Usage** + * + * ```js + * // Load storage state from a file and apply it to the context. + * await context.setStorageState('state.json'); + * ``` + * + * @param storageState Learn more about [storage state and auth](https://playwright.dev/docs/auth). + * + * Populates context with given storage state. This option can be used to initialize context with logged-in + * information obtained via + * [browserContext.storageState([options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state). + */ + setStorageState(storageState: string|{ + /** + * Cookies to set for context + */ + cookies: Array<{ + name: string; + + value: string; + + /** + * Domain and path are required. For the cookie to apply to all subdomains as well, prefix domain with a dot, like + * this: ".example.com" + */ + domain: string; + + /** + * Domain and path are required + */ + path: string; + + /** + * Unix time in seconds. + */ + expires: number; + + httpOnly: boolean; + + secure: boolean; + + /** + * sameSite flag + */ + sameSite: "Strict"|"Lax"|"None"; + }>; + + origins: Array<{ + origin: string; + + /** + * localStorage to set for context + */ + localStorage: Array<{ + name: string; + + value: string; + }>; + }>; + }): Promise; + /** * Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB * snapshot. diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 86c22a67751ec..bad0582f17c80 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -1597,6 +1597,7 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT setWebSocketInterceptionPatterns(params: BrowserContextSetWebSocketInterceptionPatternsParams, progress?: Progress): Promise; setOffline(params: BrowserContextSetOfflineParams, progress?: Progress): Promise; storageState(params: BrowserContextStorageStateParams, progress?: Progress): Promise; + setStorageState(params: BrowserContextSetStorageStateParams, progress?: Progress): Promise; pause(params?: BrowserContextPauseParams, progress?: Progress): Promise; enableRecorder(params: BrowserContextEnableRecorderParams, progress?: Progress): Promise; disableRecorder(params?: BrowserContextDisableRecorderParams, progress?: Progress): Promise; @@ -1845,6 +1846,19 @@ export type BrowserContextStorageStateResult = { cookies: NetworkCookie[], origins: OriginStorage[], }; +export type BrowserContextSetStorageStateParams = { + storageState?: { + cookies?: SetNetworkCookie[], + origins?: SetOriginStorage[], + }, +}; +export type BrowserContextSetStorageStateOptions = { + storageState?: { + cookies?: SetNetworkCookie[], + origins?: SetOriginStorage[], + }, +}; +export type BrowserContextSetStorageStateResult = void; export type BrowserContextPauseParams = {}; export type BrowserContextPauseOptions = {}; export type BrowserContextPauseResult = void; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 9a66cd1beef50..58302a1db0d35 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -376,6 +376,7 @@ APIRequestContext: storageState: title: Get storage state + group: configuration parameters: indexedDB: boolean? returns: @@ -1299,6 +1300,7 @@ BrowserContext: storageState: title: Get storage state + group: configuration parameters: indexedDB: boolean? returns: @@ -1309,6 +1311,20 @@ BrowserContext: type: array items: OriginStorage + setStorageState: + title: Set storage state + group: configuration + parameters: + storageState: + type: object? + properties: + cookies: + type: array? + items: SetNetworkCookie + origins: + type: array? + items: SetOriginStorage + pause: title: Pause diff --git a/tests/library/browsercontext-storage-state.spec.ts b/tests/library/browsercontext-storage-state.spec.ts index b3e0d5bae94b2..70b494fdfc39f 100644 --- a/tests/library/browsercontext-storage-state.spec.ts +++ b/tests/library/browsercontext-storage-state.spec.ts @@ -19,6 +19,8 @@ import { attachFrame } from 'tests/config/utils'; import { browserTest as it, expect } from '../config/browserTest'; import fs from 'fs'; +import type { BrowserContext } from 'playwright-core'; + it('should capture local storage', async ({ contextFactory }) => { const context = await contextFactory(); const page1 = await context.newPage(); @@ -71,6 +73,25 @@ it('should set local storage', async ({ contextFactory }) => { await page.goto('https://www.example.com'); const localStorage = await page.evaluate('window.localStorage'); expect(localStorage).toEqual({ name1: 'value1' }); + + // Now use setStorageState to replace the storage + await context.setStorageState({ + cookies: [], + origins: [ + { + origin: 'https://www.example.com', + localStorage: [{ + name: 'name2', + value: 'value2' + }] + }, + ] + }); + expect(context.pages()).toHaveLength(1); + await page.goto('https://www.example.com'); + const localStorage2 = await page.evaluate('window.localStorage'); + expect(localStorage2).toEqual({ name2: 'value2' }); + await context.close(); }); @@ -131,38 +152,48 @@ it('should round-trip through the file', async ({ contextFactory }, testInfo) => const written = await fs.promises.readFile(path, 'utf8'); expect(JSON.stringify(state, undefined, 2)).toBe(written); - const context2 = await contextFactory({ storageState: path }); - const page2 = await context2.newPage(); - await page2.route('**/*', route => { - route.fulfill({ body: '' }).catch(() => {}); - }); - await page2.goto('https://www.example.com'); - const localStorage = await page2.evaluate('window.localStorage'); - expect(localStorage).toEqual({ name1: 'value1' }); - const cookie = await page2.evaluate('document.cookie'); - expect(cookie).toEqual('username=John Doe'); - const idbValues = await page2.evaluate(() => new Promise((resolve, reject) => { - const openRequest = indexedDB.open('db', 42); - openRequest.addEventListener('success', async () => { - const db = openRequest.result; - const transaction = db.transaction(['store', 'store2'], 'readonly'); - const request1 = transaction.objectStore('store').get('foo'); - const request2 = transaction.objectStore('store2').get('foo'); - - const [result1, result2] = await Promise.all([request1, request2].map(request => new Promise((resolve, reject) => { - request.addEventListener('success', () => resolve(request.result)); - request.addEventListener('error', () => reject(request.error)); - }))); - - resolve([result1, new TextDecoder().decode(result2 as any)]); + const checkContext = async (context: BrowserContext) => { + const page = await context.newPage(); + await page.route('**/*', route => { + route.fulfill({ body: '' }).catch(() => {}); }); - openRequest.addEventListener('error', () => reject(openRequest.error)); - })); - expect(idbValues).toEqual([ - { name: 'foo', date: new Date(0), null: null }, - 'bar' - ]); + await page.goto('https://www.example.com'); + const localStorage = await page.evaluate('window.localStorage'); + expect(localStorage).toEqual({ name1: 'value1' }); + const cookie = await page.evaluate('document.cookie'); + expect(cookie).toEqual('username=John Doe'); + const idbValues = await page.evaluate(() => new Promise((resolve, reject) => { + const openRequest = indexedDB.open('db', 42); + openRequest.addEventListener('success', async () => { + const db = openRequest.result; + const transaction = db.transaction(['store', 'store2'], 'readonly'); + const request1 = transaction.objectStore('store').get('foo'); + const request2 = transaction.objectStore('store2').get('foo'); + + const [result1, result2] = await Promise.all([request1, request2].map(request => new Promise((resolve, reject) => { + request.addEventListener('success', () => resolve(request.result)); + request.addEventListener('error', () => reject(request.error)); + }))); + + resolve([result1, new TextDecoder().decode(result2 as any)]); + }); + openRequest.addEventListener('error', () => reject(openRequest.error)); + })); + expect(idbValues).toEqual([ + { name: 'foo', date: new Date(0), null: null }, + 'bar' + ]); + }; + + const context2 = await contextFactory({ storageState: path }); + await checkContext(context2); await context2.close(); + + const context3 = await contextFactory(); + await context3.setStorageState(path); + expect(context3.pages()).toHaveLength(0); + await checkContext(context3); + await context3.close(); }); it('should capture cookies', async ({ server, context, page, contextFactory }) => { @@ -504,3 +535,10 @@ it('should support empty indexedDB', { annotation: { type: 'issue', description: const context = await contextFactory({ storageState }); expect(await context.storageState({ indexedDB: true })).toEqual(storageState); }); + +it('setStorageState should handle missing file', async ({ contextFactory }, testInfo) => { + const context = await contextFactory(); + const file = testInfo.outputPath('does-not-exist.json'); + const error = await context.setStorageState(file).catch(e => e); + expect(error.message).toContain(`Error reading storage state from ${file}`); +}); diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts index 7fdeec6b4dda0..4564712a4b975 100644 --- a/tests/playwright-test/ui-mode-trace.spec.ts +++ b/tests/playwright-test/ui-mode-trace.spec.ts @@ -311,7 +311,6 @@ test('should not fail on internal page logs', async ({ runUITest, server }) => { /Create context/, /Create page/, /Navigate to "\/empty.html"/, - /Get storage state/, /After Hooks/, ]); });