diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index a14d31efc3..5fd8bf7146 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -180,7 +180,7 @@ export abstract class BrowserContext extends SdkObject { await page?._frameManager.closeOpenDialogs(); // This should be before the navigation to about:blank so that we could save on // a navigation as we clear local storage. - await this._clearLocalStorage(); + await this._clearStorage(); await page?.mainFrame().goto(metadata, 'about:blank', { timeout: 0 }); await this._removeExposedBindings(); await this._removeInitScripts(); @@ -471,7 +471,7 @@ export abstract class BrowserContext extends SdkObject { return result; } - async _clearLocalStorage() { + async _clearStorage() { if (!this._origins.size) return; let page = this.pages()[0]; @@ -479,7 +479,7 @@ export abstract class BrowserContext extends SdkObject { // Fast path. if (page && originArray.length === 1 && page.mainFrame().url().startsWith(originArray[0])) { - await page.mainFrame().evaluateExpression(`localStorage.clear()`, false, undefined, 'utility'); + await page.mainFrame().clearStorageForCurrentOriginBestEffort(); return; } @@ -492,7 +492,7 @@ export abstract class BrowserContext extends SdkObject { for (const origin of this._origins) { const frame = page.mainFrame(); await frame.goto(internalMetadata, origin); - await frame.evaluateExpression(`localStorage.clear()`, false, undefined, 'utility'); + await frame.clearStorageForCurrentOriginBestEffort(); } await page._setServerRequestInterceptor(undefined); } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index bc8acb9703..b5eb170eef 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1699,6 +1699,26 @@ export class Frame extends SdkObject { throw new Error(`Error: frame navigated while waiting for selector "${selector}"`); } } + + async clearStorageForCurrentOriginBestEffort() { + const context = await this._utilityContext(); + await context.evaluate(async () => { + // Clean DOMStorage + sessionStorage.clear(); + localStorage.clear(); + + // Clean Service Workers + const registrations = await navigator.serviceWorker.getRegistrations(); + await Promise.all(registrations.map(r => r.unregister())); + + // Clean IndexedDB + for (const db of await indexedDB.databases?.() || []) { + // Do not wait for the callback - it is called on timer in Chromium (slow). + if (db.name) + indexedDB.deleteDatabase(db.name!); + } + }); + } } class RerunnableTask { diff --git a/tests/playwright-test/playwright.ct-reuse.spec.ts b/tests/playwright-test/playwright.ct-reuse.spec.ts index 9dabd12548..d5863c40b9 100644 --- a/tests/playwright-test/playwright.ct-reuse.spec.ts +++ b/tests/playwright-test/playwright.ct-reuse.spec.ts @@ -168,3 +168,84 @@ test('should work with manually closed pages', async ({ runInlineTest }) => { 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; + + await page.evaluate(async () => { + 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).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); +});