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' });
+});