diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 51add38e0e..2a5dfe765f 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -484,14 +484,34 @@ export abstract class BrowserContext extends SdkObject { cookies: await this.cookies(), origins: [] }; - if (this._origins.size) { + const originsToSave = new Set(this._origins); + + // First try collecting storage stage from existing pages. + for (const page of this.pages()) { + const origin = page.mainFrame().origin(); + if (!origin || !originsToSave.has(origin)) + continue; + try { + const storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`({ + localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })), + })`, false, 'utility'); + if (storage.localStorage.length) + result.origins.push({ origin, localStorage: storage.localStorage } as channels.OriginStorage); + originsToSave.delete(origin); + } catch { + // When failed on the live page, we'll retry on the blank page below. + } + } + + // If there are still origins to save, create a blank page to iterate over origins. + if (originsToSave.size) { const internalMetadata = serverSideCallMetadata(); const page = await this.newPage(internalMetadata); await page._setServerRequestInterceptor(handler => { handler.fulfill({ body: '', requestUrl: handler.request().url() }).catch(() => {}); return true; }); - for (const origin of this._origins) { + for (const origin of originsToSave) { const originStorage: channels.OriginStorage = { origin, localStorage: [] }; const frame = page.mainFrame(); await frame.goto(internalMetadata, origin); diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index dab9a1ebcc..6dc5563d96 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -914,6 +914,12 @@ export class Frame extends SdkObject { return this._url; } + origin(): string | undefined { + if (!this._url.startsWith('http')) + return; + return network.parsedURL(this._url)?.origin; + } + parentFrame(): Frame | null { return this._parentFrame; } diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 3a666fa6a6..d449de1f79 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -19,7 +19,7 @@ import type * as dom from './dom'; import * as frames from './frames'; import * as input from './input'; import * as js from './javascript'; -import * as network from './network'; +import type * as network from './network'; import type * as channels from '@protocol/channels'; import type { ScreenshotOptions } from './screenshotter'; import { Screenshotter, validateScreenshotOptions } from './screenshotter'; @@ -706,12 +706,9 @@ export class Page extends SdkObject { frameNavigatedToNewDocument(frame: frames.Frame) { this.emit(Page.Events.InternalFrameNavigatedToNewDocument, frame); - const url = frame.url(); - if (!url.startsWith('http')) - return; - const purl = network.parsedURL(url); - if (purl) - this._browserContext.addVisitedOrigin(purl.origin); + const origin = frame.origin(); + if (origin) + this._browserContext.addVisitedOrigin(origin); } allBindings() { diff --git a/tests/library/browsercontext-storage-state.spec.ts b/tests/library/browsercontext-storage-state.spec.ts index 2e4fa6b050..7bd2581c9a 100644 --- a/tests/library/browsercontext-storage-state.spec.ts +++ b/tests/library/browsercontext-storage-state.spec.ts @@ -34,17 +34,17 @@ it('should capture local storage', async ({ contextFactory }) => { }); const { origins } = await context.storageState(); expect(origins).toEqual([{ - origin: 'https://www.example.com', - localStorage: [{ - name: 'name1', - value: 'value1' - }], - }, { origin: 'https://www.domain.com', localStorage: [{ name: 'name2', value: 'value2' }], + }, { + origin: 'https://www.example.com', + localStorage: [{ + name: 'name1', + value: 'value1' + }], }]); }); @@ -222,3 +222,56 @@ it('should serialize storageState with lone surrogates', async ({ page, context, const storageState = await context.storageState(); expect(storageState.origins[0].localStorage[0].value).toBe(String.fromCharCode(55934)); }); + +it('should work when service worker is intefering', async ({ page, context, server, isAndroid, isElectron }) => { + it.skip(isAndroid); + it.skip(isElectron); + + server.setRoute('/', (req, res) => { + res.writeHead(200, { 'content-type': 'text/html' }); + res.end(` + + `); + }); + + server.setRoute('/sw.js', (req, res) => { + res.writeHead(200, { 'content-type': 'application/javascript' }); + res.end(` + const kHtmlPage = \` + + \`; + + console.log('from sw 1'); + self.addEventListener('fetch', event => { + console.log('fetching ' + event.request.url); + const blob = new Blob([kHtmlPage], { type: 'text/html' }); + const response = new Response(blob, { status: 200 , statusText: 'OK' }); + event.respondWith(response); + }); + + self.addEventListener('activate', event => { + console.log('from sw 2'); + event.waitUntil(clients.claim()); + }); + `); + }); + + await page.goto(server.PREFIX); + await page.evaluate(() => window['activationPromise']); + + const storageState = await context.storageState(); + expect(storageState.origins[0].localStorage[0]).toEqual({ name: 'foo', value: 'bar' }); +});