diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index ee77c4e533..6f8e849db7 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -184,7 +184,7 @@ export abstract class BrowserContext extends SdkObject { await page?._frameManager.closeOpenDialogs(); // Navigate to about:blank first to ensure no page scripts are running after this point. await page?.mainFrame().goto(metadata, 'about:blank', { timeout: 0 }); - await this._clearStorage(); + await this._resetStorage(); await this._removeExposedBindings(); await this._removeInitScripts(); // TODO: following can be optimized to not perform noops. @@ -196,7 +196,7 @@ export abstract class BrowserContext extends SdkObject { await this.setGeolocation(this._options.geolocation); await this.setOffline(!!this._options.offline); await this.setUserAgent(this._options.userAgent); - await this.clearCookies(); + await this._resetCookies(); await page?.resetForReuse(metadata); } @@ -474,8 +474,10 @@ export abstract class BrowserContext extends SdkObject { return result; } - async _clearStorage() { - if (!this._origins.size) + async _resetStorage() { + const oldOrigins = this._origins; + const newOrigins = new Map(this._options.storageState?.origins?.map(p => [p.origin, p]) || []); + if (!oldOrigins.size && !newOrigins.size) return; let page = this.pages()[0]; @@ -484,15 +486,25 @@ export abstract class BrowserContext extends SdkObject { await page._setServerRequestInterceptor(handler => { handler.fulfill({ body: '' }).catch(() => {}); }); - for (const origin of this._origins) { + + for (const origin of new Set([...oldOrigins, ...newOrigins.keys()])) { const frame = page.mainFrame(); await frame.goto(internalMetadata, origin); - await frame.clearStorageForCurrentOriginBestEffort(); + await frame.resetStorageForCurrentOriginBestEffort(newOrigins.get(origin)); } + await page._setServerRequestInterceptor(undefined); + + this._origins = new Set([...newOrigins.keys()]); // It is safe to not restore the URL to about:blank since we are doing it in Page::resetForReuse. } + async _resetCookies() { + await this.clearCookies(); + if (this._options.storageState?.cookies) + await this.addCookies(this._options.storageState?.cookies); + } + isSettingStorageState(): boolean { return this._settingStorageState; } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index b4a93929f7..9340fd3e69 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1698,13 +1698,17 @@ export class Frame extends SdkObject { } } - async clearStorageForCurrentOriginBestEffort() { + async resetStorageForCurrentOriginBestEffort(newStorage: channels.OriginStorage | undefined) { const context = await this._utilityContext(); - await context.evaluate(async () => { - // Clean DOMStorage + await context.evaluate(async ({ ls }) => { + // Clean DOMStorage. sessionStorage.clear(); localStorage.clear(); + // Add new DOM Storage values. + for (const entry of ls || []) + localStorage[entry.name] = entry.value; + // Clean Service Workers const registrations = navigator.serviceWorker ? await navigator.serviceWorker.getRegistrations() : []; await Promise.all(registrations.map(r => r.unregister())).catch(() => {}); @@ -1715,7 +1719,7 @@ export class Frame extends SdkObject { if (db.name) indexedDB.deleteDatabase(db.name!); } - }).catch(() => {}); + }, { ls: newStorage?.localStorage }).catch(() => {}); } } diff --git a/tests/playwright-test/playwright.ct-reuse.spec.ts b/tests/playwright-test/playwright.ct-reuse.spec.ts deleted file mode 100644 index 6dbdbc9a4a..0000000000 --- a/tests/playwright-test/playwright.ct-reuse.spec.ts +++ /dev/null @@ -1,255 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { test, expect } from './playwright-test-fixtures'; - -test('should reuse context', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright/index.html': ``, - 'playwright/index.ts': ` - //@no-header - `, - - 'src/reuse.test.tsx': ` - //@no-header - import { test, expect } from '@playwright/experimental-ct-react'; - let lastContextGuid; - - test('one', async ({ context }) => { - lastContextGuid = context._guid; - }); - - test('two', async ({ context }) => { - expect(context._guid).toBe(lastContextGuid); - }); - - test.describe(() => { - test.use({ colorScheme: 'dark' }); - test('dark', async ({ context }) => { - expect(context._guid).toBe(lastContextGuid); - }); - }); - - test.describe(() => { - test.use({ userAgent: 'UA' }); - test('UA', async ({ context }) => { - expect(context._guid).toBe(lastContextGuid); - }); - }); - - test.describe(() => { - test.use({ timezoneId: 'Europe/Berlin' }); - test('tz', async ({ context }) => { - expect(context._guid).not.toBe(lastContextGuid); - }); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(5); -}); - -test('should not reuse context with video', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': ` - export default { - use: { video: 'on' }, - }; - `, - 'playwright/index.html': ``, - 'playwright/index.ts': ` - //@no-header - `, - - 'src/reuse.test.tsx': ` - //@no-header - import { test, expect } from '@playwright/experimental-ct-react'; - let lastContext; - - test('one', async ({ context }) => { - lastContext = context; - }); - - test('two', async ({ context }) => { - expect(context).not.toBe(lastContext); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(2); -}); - -test('should not reuse context with trace', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright.config.ts': ` - export default { - use: { trace: 'on' }, - }; - `, - 'playwright/index.html': ``, - 'playwright/index.ts': ` - //@no-header - `, - - 'src/reuse.test.tsx': ` - //@no-header - import { test, expect } from '@playwright/experimental-ct-react'; - let lastContext; - - test('one', async ({ context }) => { - lastContext = context; - }); - - test('two', async ({ context }) => { - expect(context).not.toBe(lastContext); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(2); -}); - -test('should work with manually closed pages', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright/index.html': ``, - 'playwright/index.ts': ` - //@no-header - `, - - 'src/button.test.tsx': ` - //@no-header - import { test, expect } from '@playwright/experimental-ct-react'; - - test('closes page', async ({ mount, page }) => { - let hadEvent = false; - const component = await mount(); - await expect(component).toHaveText('Submit'); - await component.click(); - expect(hadEvent).toBe(true); - await page.close(); - }); - - test('creates a new page', async ({ mount, page, context }) => { - let hadEvent = false; - const component = await mount(); - await expect(component).toHaveText('Submit'); - await component.click(); - expect(hadEvent).toBe(true); - await page.close(); - await context.newPage(); - }); - - test('still works', async ({ mount }) => { - let hadEvent = false; - const component = await mount(); - await expect(component).toHaveText('Submit'); - await component.click(); - expect(hadEvent).toBe(true); - }); - `, - }, { workers: 1 }); - - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(3); -}); - -test('should clean storage', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'playwright/index.html': ``, - 'playwright/index.ts': ` - //@no-header - `, - - 'src/reuse.test.tsx': ` - //@no-header - import { test, expect } from '@playwright/experimental-ct-react'; - let lastContextGuid; - - test('one', async ({ context, page }) => { - lastContextGuid = context._guid; - - // Spam local storage. - page.evaluate(async () => { - while (true) { - localStorage.foo = 'bar'; - sessionStorage.foo = 'bar'; - await new Promise(f => setTimeout(f, 0)); - } - }).catch(() => {}); - - const local = await page.evaluate('localStorage.foo'); - const session = await page.evaluate('sessionStorage.foo'); - expect(local).toBe('bar'); - expect(session).toBe('bar'); - }); - - test('two', async ({ context, page }) => { - expect(context._guid).toBe(lastContextGuid); - const local = await page.evaluate('localStorage.foo'); - const session = await page.evaluate('sessionStorage.foo'); - - expect(local).toBeFalsy(); - expect(session).toBeFalsy(); - }); - `, - }, { workers: 1 }); - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(2); -}); - -test('should clean db', async ({ runInlineTest }) => { - test.slow(); - const result = await runInlineTest({ - 'playwright/index.html': ``, - 'playwright/index.ts': ` - //@no-header - `, - - 'src/reuse.test.tsx': ` - //@no-header - import { test, expect } from '@playwright/experimental-ct-react'; - let lastContextGuid; - - test('one', async ({ context, page }) => { - lastContextGuid = context._guid; - await page.evaluate(async () => { - const dbRequest = indexedDB.open('db', 1); - await new Promise(f => dbRequest.onsuccess = f); - }); - const dbnames = await page.evaluate(async () => { - const dbs = await indexedDB.databases(); - return dbs.map(db => db.name); - }); - expect(dbnames).toEqual(['db']); - }); - - test('two', async ({ context, page }) => { - expect(context._guid).toBe(lastContextGuid); - const dbnames = await page.evaluate(async () => { - const dbs = await indexedDB.databases(); - return dbs.map(db => db.name); - }); - - expect(dbnames).toEqual([]); - }); - `, - }, { workers: 1 }); - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(2); -}); diff --git a/tests/playwright-test/playwright.reuse.spec.ts b/tests/playwright-test/playwright.reuse.spec.ts new file mode 100644 index 0000000000..16827a6c5e --- /dev/null +++ b/tests/playwright-test/playwright.reuse.spec.ts @@ -0,0 +1,351 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './playwright-test-fixtures'; + +test('should reuse context', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'src/reuse.test.ts': ` + const { test } = pwt; + let lastContextGuid; + test('one', async ({ context }) => { + lastContextGuid = context._guid; + }); + + test('two', async ({ context }) => { + expect(context._guid).toBe(lastContextGuid); + }); + + test.describe(() => { + test.use({ colorScheme: 'dark' }); + test('dark', async ({ context }) => { + expect(context._guid).toBe(lastContextGuid); + }); + }); + + test.describe(() => { + test.use({ userAgent: 'UA' }); + test('UA', async ({ context }) => { + expect(context._guid).toBe(lastContextGuid); + }); + }); + + test.describe(() => { + test.use({ timezoneId: 'Europe/Berlin' }); + test('tz', async ({ context }) => { + expect(context._guid).not.toBe(lastContextGuid); + }); + }); + `, + }, { workers: 1 }, { PW_TEST_REUSE_CONTEXT: '1' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(5); +}); + +test('should not reuse context with video', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + export default { + use: { video: 'on' }, + }; + `, + 'src/reuse.test.ts': ` + const { test } = pwt; + let lastContext; + + test('one', async ({ context }) => { + lastContext = context; + }); + + test('two', async ({ context }) => { + expect(context).not.toBe(lastContext); + }); + `, + }, { workers: 1 }, { PW_TEST_REUSE_CONTEXT: '1' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); +}); + +test('should not reuse context with trace', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + export default { + use: { trace: 'on' }, + }; + `, + 'src/reuse.test.ts': ` + const { test } = pwt; + let lastContext; + + test('one', async ({ context }) => { + lastContext = context; + }); + + test('two', async ({ context }) => { + expect(context).not.toBe(lastContext); + }); + `, + }, { workers: 1 }, { PW_TEST_REUSE_CONTEXT: '1' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); +}); + +test('should work with manually closed pages', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'src/button.test.ts': ` + const { test } = pwt; + + test('closes page', async ({ page }) => { + await page.close(); + }); + + test('creates a new page', async ({ page, context }) => { + await page.setContent(''); + await expect(page.locator('button')).toHaveText('Submit'); + await page.locator('button').click(); + await page.close(); + await context.newPage(); + }); + + test('still works', async ({ page }) => { + await page.setContent(''); + await expect(page.locator('button')).toHaveText('Submit'); + await page.locator('button').click(); + }); + `, + }, { workers: 1 }, { PW_TEST_REUSE_CONTEXT: '1' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); +}); + +test('should clean storage', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'src/reuse.test.ts': ` + const { test } = pwt; + let lastContextGuid; + + test.beforeEach(async ({ page }) => { + await page.route('**/*', route => route.fulfill('')); + await page.goto('http://example.com'); + }); + + test('one', async ({ context, page }) => { + lastContextGuid = context._guid; + + // Spam local storage. + page.evaluate(async () => { + while (true) { + localStorage.foo = 'bar'; + sessionStorage.foo = 'bar'; + await new Promise(f => setTimeout(f, 0)); + } + }).catch(() => {}); + + const local = await page.evaluate('localStorage.foo'); + const session = await page.evaluate('sessionStorage.foo'); + expect(local).toBe('bar'); + expect(session).toBe('bar'); + }); + + test('two', async ({ context, page }) => { + expect(context._guid).toBe(lastContextGuid); + const local = await page.evaluate('localStorage.foo'); + const session = await page.evaluate('sessionStorage.foo'); + + expect(local).toBeFalsy(); + expect(session).toBeFalsy(); + }); + `, + }, { workers: 1 }, { PW_TEST_REUSE_CONTEXT: '1' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); +}); + +test('should restore localStorage', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'src/reuse.test.ts': ` + const { test } = pwt; + let lastContextGuid; + + test.use({ + storageState: { + origins: [{ + origin: 'http://example.com', + localStorage: [{ + name: 'foo', + value: 'fooValue' + }] + }, { + origin: 'http://another.com', + localStorage: [{ + name: 'foo', + value: 'anotherValue' + }] + }] + } + }); + + test.beforeEach(async ({ page }) => { + await page.route('**/*', route => route.fulfill('')); + await page.goto('http://example.com'); + }); + + test('one', async ({ context, page }) => { + lastContextGuid = context._guid; + + { + const local = await page.evaluate('localStorage.foo'); + const session = await page.evaluate('sessionStorage.foo'); + expect(local).toBe('fooValue'); + expect(session).toBeFalsy(); + } + + // Overwrite localStorage. + await page.evaluate(() => { + localStorage.foo = 'bar'; + sessionStorage.foo = 'bar'; + }); + + { + const local = await page.evaluate('localStorage.foo'); + const session = await page.evaluate('sessionStorage.foo'); + expect(local).toBe('bar'); + expect(session).toBe('bar'); + } + }); + + test('two', async ({ context, page }) => { + expect(context._guid).toBe(lastContextGuid); + const local = await page.evaluate('localStorage.foo'); + const session = await page.evaluate('sessionStorage.foo'); + + expect(local).toBe('fooValue'); + expect(session).toBeFalsy(); + }); + + test('three', async ({ context, page }) => { + await page.goto('http://another.com'); + expect(context._guid).toBe(lastContextGuid); + const local = await page.evaluate('localStorage.foo'); + expect(local).toBe('anotherValue'); + }); + `, + }, { workers: 1 }, { PW_TEST_REUSE_CONTEXT: '1' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); +}); + +test('should clean db', async ({ runInlineTest }) => { + test.slow(); + const result = await runInlineTest({ + 'src/reuse.test.ts': ` + const { test } = pwt; + let lastContextGuid; + + test.beforeEach(async ({ page }) => { + await page.route('**/*', route => route.fulfill('')); + await page.goto('http://example.com'); + }); + + test('one', async ({ context, page }) => { + lastContextGuid = context._guid; + await page.evaluate(async () => { + const dbRequest = indexedDB.open('db', 1); + await new Promise(f => dbRequest.onsuccess = f); + }); + const dbnames = await page.evaluate(async () => { + const dbs = await indexedDB.databases(); + return dbs.map(db => db.name); + }); + expect(dbnames).toEqual(['db']); + }); + + test('two', async ({ context, page }) => { + expect(context._guid).toBe(lastContextGuid); + const dbnames = await page.evaluate(async () => { + const dbs = await indexedDB.databases(); + return dbs.map(db => db.name); + }); + + expect(dbnames).toEqual([]); + }); + `, + }, { workers: 1 }, { PW_TEST_REUSE_CONTEXT: '1' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); +}); + +test('should restore cookies', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'src/reuse.test.ts': ` + const { test } = pwt; + let lastContextGuid; + + test.use({ + storageState: { + cookies: [{ + name: 'name', + value: 'value', + domain: 'example.com', + path: '/', + }] + } + }); + + test.beforeEach(async ({ page }) => { + await page.route('**/*', route => route.fulfill('')); + await page.goto('http://example.com'); + }); + + test('one', async ({ context, page }) => { + lastContextGuid = context._guid; + + { + const cookie = await page.evaluate('document.cookie'); + expect(cookie).toBe('name=value'); + } + + // Overwrite cookie. + await page.evaluate(async () => { + document.cookie = 'name=value2'; + }); + + { + const cookie = await page.evaluate('document.cookie'); + expect(cookie).toBe('name=value2'); + } + }); + + test('two', async ({ context, page }) => { + expect(context._guid).toBe(lastContextGuid); + const cookie = await page.evaluate('document.cookie'); + expect(cookie).toBe('name=value'); + }); + + test('three', async ({ context, page }) => { + await page.goto('http://another.com'); + const cookie = await page.evaluate('document.cookie'); + expect(cookie).toBe(''); + }); + `, + }, { workers: 1 }, { PW_TEST_REUSE_CONTEXT: '1' }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); +});