mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
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:
parent
2ce421b27a
commit
349b25e61a
@ -484,14 +484,34 @@ export abstract class BrowserContext extends SdkObject {
|
|||||||
cookies: await this.cookies(),
|
cookies: await this.cookies(),
|
||||||
origins: []
|
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 internalMetadata = serverSideCallMetadata();
|
||||||
const page = await this.newPage(internalMetadata);
|
const page = await this.newPage(internalMetadata);
|
||||||
await page._setServerRequestInterceptor(handler => {
|
await page._setServerRequestInterceptor(handler => {
|
||||||
handler.fulfill({ body: '<html></html>', requestUrl: handler.request().url() }).catch(() => {});
|
handler.fulfill({ body: '<html></html>', requestUrl: handler.request().url() }).catch(() => {});
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
for (const origin of this._origins) {
|
for (const origin of originsToSave) {
|
||||||
const originStorage: channels.OriginStorage = { origin, localStorage: [] };
|
const originStorage: channels.OriginStorage = { origin, localStorage: [] };
|
||||||
const frame = page.mainFrame();
|
const frame = page.mainFrame();
|
||||||
await frame.goto(internalMetadata, origin);
|
await frame.goto(internalMetadata, origin);
|
||||||
|
@ -914,6 +914,12 @@ export class Frame extends SdkObject {
|
|||||||
return this._url;
|
return this._url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
origin(): string | undefined {
|
||||||
|
if (!this._url.startsWith('http'))
|
||||||
|
return;
|
||||||
|
return network.parsedURL(this._url)?.origin;
|
||||||
|
}
|
||||||
|
|
||||||
parentFrame(): Frame | null {
|
parentFrame(): Frame | null {
|
||||||
return this._parentFrame;
|
return this._parentFrame;
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ import type * as dom from './dom';
|
|||||||
import * as frames from './frames';
|
import * as frames from './frames';
|
||||||
import * as input from './input';
|
import * as input from './input';
|
||||||
import * as js from './javascript';
|
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 * as channels from '@protocol/channels';
|
||||||
import type { ScreenshotOptions } from './screenshotter';
|
import type { ScreenshotOptions } from './screenshotter';
|
||||||
import { Screenshotter, validateScreenshotOptions } from './screenshotter';
|
import { Screenshotter, validateScreenshotOptions } from './screenshotter';
|
||||||
@ -706,12 +706,9 @@ export class Page extends SdkObject {
|
|||||||
|
|
||||||
frameNavigatedToNewDocument(frame: frames.Frame) {
|
frameNavigatedToNewDocument(frame: frames.Frame) {
|
||||||
this.emit(Page.Events.InternalFrameNavigatedToNewDocument, frame);
|
this.emit(Page.Events.InternalFrameNavigatedToNewDocument, frame);
|
||||||
const url = frame.url();
|
const origin = frame.origin();
|
||||||
if (!url.startsWith('http'))
|
if (origin)
|
||||||
return;
|
this._browserContext.addVisitedOrigin(origin);
|
||||||
const purl = network.parsedURL(url);
|
|
||||||
if (purl)
|
|
||||||
this._browserContext.addVisitedOrigin(purl.origin);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
allBindings() {
|
allBindings() {
|
||||||
|
@ -34,17 +34,17 @@ it('should capture local storage', async ({ contextFactory }) => {
|
|||||||
});
|
});
|
||||||
const { origins } = await context.storageState();
|
const { origins } = await context.storageState();
|
||||||
expect(origins).toEqual([{
|
expect(origins).toEqual([{
|
||||||
origin: 'https://www.example.com',
|
|
||||||
localStorage: [{
|
|
||||||
name: 'name1',
|
|
||||||
value: 'value1'
|
|
||||||
}],
|
|
||||||
}, {
|
|
||||||
origin: 'https://www.domain.com',
|
origin: 'https://www.domain.com',
|
||||||
localStorage: [{
|
localStorage: [{
|
||||||
name: 'name2',
|
name: 'name2',
|
||||||
value: 'value2'
|
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();
|
const storageState = await context.storageState();
|
||||||
expect(storageState.origins[0].localStorage[0].value).toBe(String.fromCharCode(55934));
|
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' });
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user