diff --git a/packages/playwright-core/src/dispatchers/browserDispatcher.ts b/packages/playwright-core/src/dispatchers/browserDispatcher.ts index d145747bc2..b8a92fe068 100644 --- a/packages/playwright-core/src/dispatchers/browserDispatcher.ts +++ b/packages/playwright-core/src/dispatchers/browserDispatcher.ts @@ -38,9 +38,7 @@ export class BrowserDispatcher extends Dispatcher { - const context = await this._object.newContext(params); - if (params.storageState) - await context.setStorageState(metadata, params.storageState); + const context = await this._object.newContext(metadata, params); return { context: new BrowserContextDispatcher(this._scope, context) }; } @@ -91,12 +89,10 @@ export class ConnectedBrowserDispatcher extends Dispatcher { if (params.recordVideo) params.recordVideo.dir = this._object.options.artifactsDir; - const context = await this._object.newContext(params); + const context = await this._object.newContext(metadata, params); this._contexts.add(context); context._setSelectors(this.selectors); context.on(BrowserContext.Events.Close, () => this._contexts.delete(context)); - if (params.storageState) - await context.setStorageState(metadata, params.storageState); return { context: new BrowserContextDispatcher(this._scope, context) }; } diff --git a/packages/playwright-core/src/server/browser.ts b/packages/playwright-core/src/server/browser.ts index 6e79480b11..fb6da28634 100644 --- a/packages/playwright-core/src/server/browser.ts +++ b/packages/playwright-core/src/server/browser.ts @@ -15,13 +15,13 @@ */ import * as types from './types'; -import { BrowserContext } from './browserContext'; +import { BrowserContext, validateBrowserContextOptions } from './browserContext'; import { Page } from './page'; import { Download } from './download'; import { ProxySettings } from './types'; import { ChildProcess } from 'child_process'; import { RecentLogsCollector } from '../utils/debugLogger'; -import { SdkObject } from './instrumentation'; +import { CallMetadata, SdkObject } from './instrumentation'; import { Artifact } from './artifact'; import { Selectors } from './selectors'; @@ -74,12 +74,20 @@ export abstract class Browser extends SdkObject { this.options = options; } - abstract newContext(options: types.BrowserContextOptions): Promise; + abstract doCreateNewContext(options: types.BrowserContextOptions): Promise; abstract contexts(): BrowserContext[]; abstract isConnected(): boolean; abstract version(): string; abstract userAgent(): string; + async newContext(metadata: CallMetadata, options: types.BrowserContextOptions): Promise { + validateBrowserContextOptions(options, this.options); + const context = await this.doCreateNewContext(options); + if (options.storageState) + await context.setStorageState(metadata, options.storageState); + return context; + } + _downloadCreated(page: Page, uuid: string, url: string, suggestedFilename?: string) { const download = new Download(page, this.options.downloadsPath || '', uuid, url, suggestedFilename); this._downloads.set(uuid, download); diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index cd2b7081fe..39c08b94c7 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -68,6 +68,7 @@ export abstract class BrowserContext extends SdkObject { readonly fetchRequest: BrowserContextAPIRequestContext; private _customCloseHandler?: () => Promise; readonly _tempDirs: string[] = []; + private _settingStorageState = false; constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) { super(browser, 'browser-context'); @@ -375,25 +376,34 @@ export abstract class BrowserContext extends SdkObject { return result; } + isSettingStorageState(): boolean { + return this._settingStorageState; + } + async setStorageState(metadata: CallMetadata, state: types.SetStorageState) { - if (state.cookies) - await this.addCookies(state.cookies); - if (state.origins && state.origins.length) { - const internalMetadata = serverSideCallMetadata(); - const page = await this.newPage(internalMetadata); - await page._setServerRequestInterceptor(handler => { - handler.fulfill({ body: '' }).catch(() => {}); - }); - 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); - }`, true, originState, 'utility'); + this._settingStorageState = true; + try { + if (state.cookies) + await this.addCookies(state.cookies); + if (state.origins && state.origins.length) { + const internalMetadata = serverSideCallMetadata(); + const page = await this.newPage(internalMetadata); + await page._setServerRequestInterceptor(handler => { + handler.fulfill({ body: '' }).catch(() => {}); + }); + 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); + }`, true, originState, 'utility'); + } + await page.close(internalMetadata); } - await page.close(internalMetadata); + } finally { + this._settingStorageState = false; } } diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index b7fb59819f..937582c631 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -16,7 +16,7 @@ */ import { Browser, BrowserOptions } from '../browser'; -import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; +import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext'; import { assert } from '../../utils/utils'; import * as network from '../network'; import { Page, PageBinding, PageDelegate, Worker } from '../page'; @@ -92,9 +92,7 @@ export class CRBrowser extends Browser { this._session.on('Browser.downloadProgress', this._onDownloadProgress.bind(this)); } - async newContext(options: types.BrowserContextOptions): Promise { - validateBrowserContextOptions(options, this.options); - + async doCreateNewContext(options: types.BrowserContextOptions): Promise { let proxyBypassList = undefined; if (options.proxy) { if (process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK) diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 60e7521600..679d862dfc 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -439,7 +439,8 @@ class FrameSession { } async _initialize(hasUIWindow: boolean) { - if (hasUIWindow && + const isSettingStorageState = this._page._browserContext.isSettingStorageState(); + if (!isSettingStorageState && hasUIWindow && !this._crPage._browserContext._browser.isClank() && !this._crPage._browserContext._options.noDefaultViewport) { const { windowId } = await this._client.send('Browser.getWindowForTarget'); @@ -447,7 +448,7 @@ class FrameSession { } let screencastOptions: types.PageScreencastOptions | undefined; - if (this._isMainFrame() && this._crPage._browserContext._options.recordVideo && hasUIWindow) { + if (!isSettingStorageState && this._isMainFrame() && this._crPage._browserContext._options.recordVideo && hasUIWindow) { const screencastId = createGuid(); const outputFile = path.join(this._crPage._browserContext._options.recordVideo.dir, screencastId + '.webm'); screencastOptions = { @@ -512,41 +513,43 @@ class FrameSession { this._networkManager.initialize(), this._client.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }), ]; - if (this._isMainFrame()) - promises.push(this._client.send('Emulation.setFocusEmulationEnabled', { enabled: true })); - const options = this._crPage._browserContext._options; - if (options.bypassCSP) - promises.push(this._client.send('Page.setBypassCSP', { enabled: true })); - if (options.ignoreHTTPSErrors) - promises.push(this._client.send('Security.setIgnoreCertificateErrors', { ignore: true })); - if (this._isMainFrame()) - promises.push(this._updateViewport()); - if (options.hasTouch) - promises.push(this._client.send('Emulation.setTouchEmulationEnabled', { enabled: true })); - if (options.javaScriptEnabled === false) - promises.push(this._client.send('Emulation.setScriptExecutionDisabled', { value: true })); - if (options.userAgent || options.locale) - promises.push(this._client.send('Emulation.setUserAgentOverride', { userAgent: options.userAgent || '', acceptLanguage: options.locale })); - if (options.locale) - promises.push(emulateLocale(this._client, options.locale)); - if (options.timezoneId) - promises.push(emulateTimezone(this._client, options.timezoneId)); - if (!this._crPage._browserContext._browser.options.headful) - promises.push(this._setDefaultFontFamilies(this._client)); - promises.push(this._updateGeolocation(true)); - promises.push(this._updateExtraHTTPHeaders(true)); - promises.push(this._updateRequestInterception()); - promises.push(this._updateOffline(true)); - promises.push(this._updateHttpCredentials(true)); - promises.push(this._updateEmulateMedia(true)); - for (const binding of this._crPage._page.allBindings()) - promises.push(this._initBinding(binding)); - for (const source of this._crPage._browserContext._evaluateOnNewDocumentSources) - promises.push(this._evaluateOnNewDocument(source, 'main')); - for (const source of this._crPage._page._evaluateOnNewDocumentSources) - promises.push(this._evaluateOnNewDocument(source, 'main')); - if (screencastOptions) - promises.push(this._startVideoRecording(screencastOptions)); + if (!isSettingStorageState) { + if (this._isMainFrame()) + promises.push(this._client.send('Emulation.setFocusEmulationEnabled', { enabled: true })); + const options = this._crPage._browserContext._options; + if (options.bypassCSP) + promises.push(this._client.send('Page.setBypassCSP', { enabled: true })); + if (options.ignoreHTTPSErrors) + promises.push(this._client.send('Security.setIgnoreCertificateErrors', { ignore: true })); + if (this._isMainFrame()) + promises.push(this._updateViewport()); + if (options.hasTouch) + promises.push(this._client.send('Emulation.setTouchEmulationEnabled', { enabled: true })); + if (options.javaScriptEnabled === false) + promises.push(this._client.send('Emulation.setScriptExecutionDisabled', { value: true })); + if (options.userAgent || options.locale) + promises.push(this._client.send('Emulation.setUserAgentOverride', { userAgent: options.userAgent || '', acceptLanguage: options.locale })); + if (options.locale) + promises.push(emulateLocale(this._client, options.locale)); + if (options.timezoneId) + promises.push(emulateTimezone(this._client, options.timezoneId)); + if (!this._crPage._browserContext._browser.options.headful) + promises.push(this._setDefaultFontFamilies(this._client)); + promises.push(this._updateGeolocation(true)); + promises.push(this._updateExtraHTTPHeaders(true)); + promises.push(this._updateRequestInterception()); + promises.push(this._updateOffline(true)); + promises.push(this._updateHttpCredentials(true)); + promises.push(this._updateEmulateMedia(true)); + for (const binding of this._crPage._page.allBindings()) + promises.push(this._initBinding(binding)); + for (const source of this._crPage._browserContext._evaluateOnNewDocumentSources) + promises.push(this._evaluateOnNewDocument(source, 'main')); + for (const source of this._crPage._page._evaluateOnNewDocumentSources) + promises.push(this._evaluateOnNewDocument(source, 'main')); + if (screencastOptions) + promises.push(this._startVideoRecording(screencastOptions)); + } promises.push(this._client.send('Runtime.runIfWaitingForDebugger')); promises.push(this._firstNonInitialNavigationCommittedPromise); await Promise.all(promises); diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index 0b1d798b51..ed24cf53c8 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -18,7 +18,7 @@ import { kBrowserClosedError } from '../../utils/errors'; import { assert } from '../../utils/utils'; import { Browser, BrowserOptions } from '../browser'; -import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; +import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext'; import * as network from '../network'; import { Page, PageBinding, PageDelegate } from '../page'; import { ConnectionTransport } from '../transport'; @@ -76,8 +76,7 @@ export class FFBrowser extends Browser { return !this._connection._closed; } - async newContext(options: types.BrowserContextOptions): Promise { - validateBrowserContextOptions(options, this.options); + async doCreateNewContext(options: types.BrowserContextOptions): Promise { if (options.isMobile) throw new Error('options.isMobile is not supported in Firefox'); const { browserContextId } = await this._connection.send('Browser.createBrowserContext', { removeOnDetach: true }); diff --git a/packages/playwright-core/src/server/types.ts b/packages/playwright-core/src/server/types.ts index df2d844a7d..f5aeb683ae 100644 --- a/packages/playwright-core/src/server/types.ts +++ b/packages/playwright-core/src/server/types.ts @@ -257,6 +257,7 @@ export type BrowserContextOptions = { omitContent?: boolean, path: string }, + storageState?: SetStorageState, strictSelectors?: boolean, proxy?: ProxySettings, baseURL?: string, diff --git a/packages/playwright-core/src/server/webkit/wkBrowser.ts b/packages/playwright-core/src/server/webkit/wkBrowser.ts index 2c6e48f4a1..3dcbed9682 100644 --- a/packages/playwright-core/src/server/webkit/wkBrowser.ts +++ b/packages/playwright-core/src/server/webkit/wkBrowser.ts @@ -16,7 +16,7 @@ */ import { Browser, BrowserOptions } from '../browser'; -import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; +import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext'; import { eventsHelper, RegisteredListener } from '../../utils/eventsHelper'; import { assert } from '../../utils/utils'; import * as network from '../network'; @@ -79,8 +79,7 @@ export class WKBrowser extends Browser { this._didClose(); } - async newContext(options: types.BrowserContextOptions): Promise { - validateBrowserContextOptions(options, this.options); + async doCreateNewContext(options: types.BrowserContextOptions): Promise { const createOptions = options.proxy ? { proxyServer: options.proxy.server, proxyBypassList: options.proxy.bypass diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 3aa0e250a0..500d72da6c 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -114,6 +114,8 @@ export class WKPage implements PageDelegate { } private async _initializePageProxySession() { + if (this._page._browserContext.isSettingStorageState()) + return; const promises: Promise[] = [ this._pageProxySession.send('Dialog.enable'), this._pageProxySession.send('Emulation.setActiveAndFocused', { active: true }), @@ -183,6 +185,10 @@ export class WKPage implements PageDelegate { promises.push(session.send('Network.setInterceptionEnabled', { enabled: true })); promises.push(session.send('Network.addInterception', { url: '.*', stage: 'request', isRegex: true })); } + if (this._page._browserContext.isSettingStorageState()) { + await Promise.all(promises); + return; + } const contextOptions = this._browserContext._options; if (contextOptions.userAgent) diff --git a/tests/browsercontext-storage-state.spec.ts b/tests/browsercontext-storage-state.spec.ts index bf9ece7000..59aa16a296 100644 --- a/tests/browsercontext-storage-state.spec.ts +++ b/tests/browsercontext-storage-state.spec.ts @@ -48,8 +48,8 @@ it('should capture local storage', async ({ contextFactory }) => { }]); }); -it('should set local storage', async ({ browser }) => { - const context = await browser.newContext({ +it('should set local storage', async ({ contextFactory }) => { + const context = await contextFactory({ storageState: { cookies: [], origins: [ @@ -162,3 +162,35 @@ it('should not emit events about internal page', async ({ contextFactory }) => { await context.storageState(); expect(events).toHaveLength(0); }); + +it('should not restore localStorage twice', async ({ contextFactory }) => { + const context = await contextFactory({ + storageState: { + cookies: [], + origins: [ + { + origin: 'https://www.example.com', + localStorage: [{ + name: 'name1', + value: 'value1' + }] + }, + ] + } + }); + const page = await context.newPage(); + await page.route('**/*', route => { + route.fulfill({ body: '' }).catch(() => {}); + }); + await page.goto('https://www.example.com'); + const localStorage1 = await page.evaluate('window.localStorage'); + expect(localStorage1).toEqual({ name1: 'value1' }); + + await page.evaluate(() => window.localStorage['name1'] = 'value2'); + + await page.goto('https://www.example.com'); + const localStorage2 = await page.evaluate('window.localStorage'); + expect(localStorage2).toEqual({ name1: 'value2' }); + + await context.close(); +});