fix(storageState): try to collect storage state on existing pages first (#29915)

This helps in a case where navigating to an origin fails for some
reason, for example because a registered service worker loads some
content into the supposedly blank page.

Fixes #29402.
This commit is contained in:
Dmitry Gozman 2024-03-12 19:20:35 -07:00 committed by GitHub
parent 2ce421b27a
commit 349b25e61a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 91 additions and 15 deletions

View File

@ -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: '<html></html>', 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);

View File

@ -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;
}

View File

@ -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() {

View File

@ -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(`
<script>
console.log('from page');
window.localStorage.foo = 'bar';
window.registrationPromise = navigator.serviceWorker.register('sw.js');
window.activationPromise = new Promise(resolve => navigator.serviceWorker.oncontrollerchange = resolve);
</script>
`);
});
server.setRoute('/sw.js', (req, res) => {
res.writeHead(200, { 'content-type': 'application/javascript' });
res.end(`
const kHtmlPage = \`
<script>
console.log('from sw page');
let counter = window.localStorage.counter || 0;
++counter;
window.localStorage.counter = counter;
setTimeout(() => {
window.location.href = counter + '.html';
}, 0);
</script>
\`;
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' });
});