chore: do not initialize full sessions for pages used in session restore (#12886)

This commit is contained in:
Pavel Feldman 2022-03-18 17:17:37 -08:00 committed by GitHub
parent 4aaa63beaa
commit 98ed81dc00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 127 additions and 75 deletions

View File

@ -38,9 +38,7 @@ export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserChann
} }
async newContext(params: channels.BrowserNewContextParams, metadata: CallMetadata): Promise<channels.BrowserNewContextResult> { async newContext(params: channels.BrowserNewContextParams, metadata: CallMetadata): Promise<channels.BrowserNewContextResult> {
const context = await this._object.newContext(params); const context = await this._object.newContext(metadata, params);
if (params.storageState)
await context.setStorageState(metadata, params.storageState);
return { context: new BrowserContextDispatcher(this._scope, context) }; return { context: new BrowserContextDispatcher(this._scope, context) };
} }
@ -91,12 +89,10 @@ export class ConnectedBrowserDispatcher extends Dispatcher<Browser, channels.Bro
async newContext(params: channels.BrowserNewContextParams, metadata: CallMetadata): Promise<channels.BrowserNewContextResult> { async newContext(params: channels.BrowserNewContextParams, metadata: CallMetadata): Promise<channels.BrowserNewContextResult> {
if (params.recordVideo) if (params.recordVideo)
params.recordVideo.dir = this._object.options.artifactsDir; params.recordVideo.dir = this._object.options.artifactsDir;
const context = await this._object.newContext(params); const context = await this._object.newContext(metadata, params);
this._contexts.add(context); this._contexts.add(context);
context._setSelectors(this.selectors); context._setSelectors(this.selectors);
context.on(BrowserContext.Events.Close, () => this._contexts.delete(context)); context.on(BrowserContext.Events.Close, () => this._contexts.delete(context));
if (params.storageState)
await context.setStorageState(metadata, params.storageState);
return { context: new BrowserContextDispatcher(this._scope, context) }; return { context: new BrowserContextDispatcher(this._scope, context) };
} }

View File

@ -15,13 +15,13 @@
*/ */
import * as types from './types'; import * as types from './types';
import { BrowserContext } from './browserContext'; import { BrowserContext, validateBrowserContextOptions } from './browserContext';
import { Page } from './page'; import { Page } from './page';
import { Download } from './download'; import { Download } from './download';
import { ProxySettings } from './types'; import { ProxySettings } from './types';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
import { RecentLogsCollector } from '../utils/debugLogger'; import { RecentLogsCollector } from '../utils/debugLogger';
import { SdkObject } from './instrumentation'; import { CallMetadata, SdkObject } from './instrumentation';
import { Artifact } from './artifact'; import { Artifact } from './artifact';
import { Selectors } from './selectors'; import { Selectors } from './selectors';
@ -74,12 +74,20 @@ export abstract class Browser extends SdkObject {
this.options = options; this.options = options;
} }
abstract newContext(options: types.BrowserContextOptions): Promise<BrowserContext>; abstract doCreateNewContext(options: types.BrowserContextOptions): Promise<BrowserContext>;
abstract contexts(): BrowserContext[]; abstract contexts(): BrowserContext[];
abstract isConnected(): boolean; abstract isConnected(): boolean;
abstract version(): string; abstract version(): string;
abstract userAgent(): string; abstract userAgent(): string;
async newContext(metadata: CallMetadata, options: types.BrowserContextOptions): Promise<BrowserContext> {
validateBrowserContextOptions(options, this.options);
const context = await this.doCreateNewContext(options);
if (options.storageState)
await context.setStorageState(metadata, options.storageState);
return context;
}
_downloadCreated(page: Page, uuid: string, url: string, suggestedFilename?: string) { _downloadCreated(page: Page, uuid: string, url: string, suggestedFilename?: string) {
const download = new Download(page, this.options.downloadsPath || '', uuid, url, suggestedFilename); const download = new Download(page, this.options.downloadsPath || '', uuid, url, suggestedFilename);
this._downloads.set(uuid, download); this._downloads.set(uuid, download);

View File

@ -68,6 +68,7 @@ export abstract class BrowserContext extends SdkObject {
readonly fetchRequest: BrowserContextAPIRequestContext; readonly fetchRequest: BrowserContextAPIRequestContext;
private _customCloseHandler?: () => Promise<any>; private _customCloseHandler?: () => Promise<any>;
readonly _tempDirs: string[] = []; readonly _tempDirs: string[] = [];
private _settingStorageState = false;
constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) { constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) {
super(browser, 'browser-context'); super(browser, 'browser-context');
@ -375,25 +376,34 @@ export abstract class BrowserContext extends SdkObject {
return result; return result;
} }
isSettingStorageState(): boolean {
return this._settingStorageState;
}
async setStorageState(metadata: CallMetadata, state: types.SetStorageState) { async setStorageState(metadata: CallMetadata, state: types.SetStorageState) {
if (state.cookies) this._settingStorageState = true;
await this.addCookies(state.cookies); try {
if (state.origins && state.origins.length) { if (state.cookies)
const internalMetadata = serverSideCallMetadata(); await this.addCookies(state.cookies);
const page = await this.newPage(internalMetadata); if (state.origins && state.origins.length) {
await page._setServerRequestInterceptor(handler => { const internalMetadata = serverSideCallMetadata();
handler.fulfill({ body: '<html></html>' }).catch(() => {}); const page = await this.newPage(internalMetadata);
}); await page._setServerRequestInterceptor(handler => {
for (const originState of state.origins) { handler.fulfill({ body: '<html></html>' }).catch(() => {});
const frame = page.mainFrame(); });
await frame.goto(metadata, originState.origin); for (const originState of state.origins) {
await frame.evaluateExpression(` const frame = page.mainFrame();
originState => { await frame.goto(metadata, originState.origin);
for (const { name, value } of (originState.localStorage || [])) await frame.evaluateExpression(`
localStorage.setItem(name, value); originState => {
}`, true, originState, 'utility'); for (const { name, value } of (originState.localStorage || []))
localStorage.setItem(name, value);
}`, true, originState, 'utility');
}
await page.close(internalMetadata);
} }
await page.close(internalMetadata); } finally {
this._settingStorageState = false;
} }
} }

View File

@ -16,7 +16,7 @@
*/ */
import { Browser, BrowserOptions } from '../browser'; import { Browser, BrowserOptions } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import { assert } from '../../utils/utils'; import { assert } from '../../utils/utils';
import * as network from '../network'; import * as network from '../network';
import { Page, PageBinding, PageDelegate, Worker } from '../page'; import { Page, PageBinding, PageDelegate, Worker } from '../page';
@ -92,9 +92,7 @@ export class CRBrowser extends Browser {
this._session.on('Browser.downloadProgress', this._onDownloadProgress.bind(this)); this._session.on('Browser.downloadProgress', this._onDownloadProgress.bind(this));
} }
async newContext(options: types.BrowserContextOptions): Promise<BrowserContext> { async doCreateNewContext(options: types.BrowserContextOptions): Promise<BrowserContext> {
validateBrowserContextOptions(options, this.options);
let proxyBypassList = undefined; let proxyBypassList = undefined;
if (options.proxy) { if (options.proxy) {
if (process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK) if (process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK)

View File

@ -439,7 +439,8 @@ class FrameSession {
} }
async _initialize(hasUIWindow: boolean) { async _initialize(hasUIWindow: boolean) {
if (hasUIWindow && const isSettingStorageState = this._page._browserContext.isSettingStorageState();
if (!isSettingStorageState && hasUIWindow &&
!this._crPage._browserContext._browser.isClank() && !this._crPage._browserContext._browser.isClank() &&
!this._crPage._browserContext._options.noDefaultViewport) { !this._crPage._browserContext._options.noDefaultViewport) {
const { windowId } = await this._client.send('Browser.getWindowForTarget'); const { windowId } = await this._client.send('Browser.getWindowForTarget');
@ -447,7 +448,7 @@ class FrameSession {
} }
let screencastOptions: types.PageScreencastOptions | undefined; let screencastOptions: types.PageScreencastOptions | undefined;
if (this._isMainFrame() && this._crPage._browserContext._options.recordVideo && hasUIWindow) { if (!isSettingStorageState && this._isMainFrame() && this._crPage._browserContext._options.recordVideo && hasUIWindow) {
const screencastId = createGuid(); const screencastId = createGuid();
const outputFile = path.join(this._crPage._browserContext._options.recordVideo.dir, screencastId + '.webm'); const outputFile = path.join(this._crPage._browserContext._options.recordVideo.dir, screencastId + '.webm');
screencastOptions = { screencastOptions = {
@ -512,41 +513,43 @@ class FrameSession {
this._networkManager.initialize(), this._networkManager.initialize(),
this._client.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }), this._client.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }),
]; ];
if (this._isMainFrame()) if (!isSettingStorageState) {
promises.push(this._client.send('Emulation.setFocusEmulationEnabled', { enabled: true })); if (this._isMainFrame())
const options = this._crPage._browserContext._options; promises.push(this._client.send('Emulation.setFocusEmulationEnabled', { enabled: true }));
if (options.bypassCSP) const options = this._crPage._browserContext._options;
promises.push(this._client.send('Page.setBypassCSP', { enabled: true })); if (options.bypassCSP)
if (options.ignoreHTTPSErrors) promises.push(this._client.send('Page.setBypassCSP', { enabled: true }));
promises.push(this._client.send('Security.setIgnoreCertificateErrors', { ignore: true })); if (options.ignoreHTTPSErrors)
if (this._isMainFrame()) promises.push(this._client.send('Security.setIgnoreCertificateErrors', { ignore: true }));
promises.push(this._updateViewport()); if (this._isMainFrame())
if (options.hasTouch) promises.push(this._updateViewport());
promises.push(this._client.send('Emulation.setTouchEmulationEnabled', { enabled: true })); if (options.hasTouch)
if (options.javaScriptEnabled === false) promises.push(this._client.send('Emulation.setTouchEmulationEnabled', { enabled: true }));
promises.push(this._client.send('Emulation.setScriptExecutionDisabled', { value: true })); if (options.javaScriptEnabled === false)
if (options.userAgent || options.locale) promises.push(this._client.send('Emulation.setScriptExecutionDisabled', { value: true }));
promises.push(this._client.send('Emulation.setUserAgentOverride', { userAgent: options.userAgent || '', acceptLanguage: options.locale })); if (options.userAgent || options.locale)
if (options.locale) promises.push(this._client.send('Emulation.setUserAgentOverride', { userAgent: options.userAgent || '', acceptLanguage: options.locale }));
promises.push(emulateLocale(this._client, options.locale)); if (options.locale)
if (options.timezoneId) promises.push(emulateLocale(this._client, options.locale));
promises.push(emulateTimezone(this._client, options.timezoneId)); if (options.timezoneId)
if (!this._crPage._browserContext._browser.options.headful) promises.push(emulateTimezone(this._client, options.timezoneId));
promises.push(this._setDefaultFontFamilies(this._client)); if (!this._crPage._browserContext._browser.options.headful)
promises.push(this._updateGeolocation(true)); promises.push(this._setDefaultFontFamilies(this._client));
promises.push(this._updateExtraHTTPHeaders(true)); promises.push(this._updateGeolocation(true));
promises.push(this._updateRequestInterception()); promises.push(this._updateExtraHTTPHeaders(true));
promises.push(this._updateOffline(true)); promises.push(this._updateRequestInterception());
promises.push(this._updateHttpCredentials(true)); promises.push(this._updateOffline(true));
promises.push(this._updateEmulateMedia(true)); promises.push(this._updateHttpCredentials(true));
for (const binding of this._crPage._page.allBindings()) promises.push(this._updateEmulateMedia(true));
promises.push(this._initBinding(binding)); for (const binding of this._crPage._page.allBindings())
for (const source of this._crPage._browserContext._evaluateOnNewDocumentSources) promises.push(this._initBinding(binding));
promises.push(this._evaluateOnNewDocument(source, 'main')); for (const source of this._crPage._browserContext._evaluateOnNewDocumentSources)
for (const source of this._crPage._page._evaluateOnNewDocumentSources) promises.push(this._evaluateOnNewDocument(source, 'main'));
promises.push(this._evaluateOnNewDocument(source, 'main')); for (const source of this._crPage._page._evaluateOnNewDocumentSources)
if (screencastOptions) promises.push(this._evaluateOnNewDocument(source, 'main'));
promises.push(this._startVideoRecording(screencastOptions)); if (screencastOptions)
promises.push(this._startVideoRecording(screencastOptions));
}
promises.push(this._client.send('Runtime.runIfWaitingForDebugger')); promises.push(this._client.send('Runtime.runIfWaitingForDebugger'));
promises.push(this._firstNonInitialNavigationCommittedPromise); promises.push(this._firstNonInitialNavigationCommittedPromise);
await Promise.all(promises); await Promise.all(promises);

View File

@ -18,7 +18,7 @@
import { kBrowserClosedError } from '../../utils/errors'; import { kBrowserClosedError } from '../../utils/errors';
import { assert } from '../../utils/utils'; import { assert } from '../../utils/utils';
import { Browser, BrowserOptions } from '../browser'; import { Browser, BrowserOptions } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import * as network from '../network'; import * as network from '../network';
import { Page, PageBinding, PageDelegate } from '../page'; import { Page, PageBinding, PageDelegate } from '../page';
import { ConnectionTransport } from '../transport'; import { ConnectionTransport } from '../transport';
@ -76,8 +76,7 @@ export class FFBrowser extends Browser {
return !this._connection._closed; return !this._connection._closed;
} }
async newContext(options: types.BrowserContextOptions): Promise<BrowserContext> { async doCreateNewContext(options: types.BrowserContextOptions): Promise<BrowserContext> {
validateBrowserContextOptions(options, this.options);
if (options.isMobile) if (options.isMobile)
throw new Error('options.isMobile is not supported in Firefox'); throw new Error('options.isMobile is not supported in Firefox');
const { browserContextId } = await this._connection.send('Browser.createBrowserContext', { removeOnDetach: true }); const { browserContextId } = await this._connection.send('Browser.createBrowserContext', { removeOnDetach: true });

View File

@ -257,6 +257,7 @@ export type BrowserContextOptions = {
omitContent?: boolean, omitContent?: boolean,
path: string path: string
}, },
storageState?: SetStorageState,
strictSelectors?: boolean, strictSelectors?: boolean,
proxy?: ProxySettings, proxy?: ProxySettings,
baseURL?: string, baseURL?: string,

View File

@ -16,7 +16,7 @@
*/ */
import { Browser, BrowserOptions } from '../browser'; import { Browser, BrowserOptions } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextOptions, verifyGeolocation } from '../browserContext'; import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import { eventsHelper, RegisteredListener } from '../../utils/eventsHelper'; import { eventsHelper, RegisteredListener } from '../../utils/eventsHelper';
import { assert } from '../../utils/utils'; import { assert } from '../../utils/utils';
import * as network from '../network'; import * as network from '../network';
@ -79,8 +79,7 @@ export class WKBrowser extends Browser {
this._didClose(); this._didClose();
} }
async newContext(options: types.BrowserContextOptions): Promise<BrowserContext> { async doCreateNewContext(options: types.BrowserContextOptions): Promise<BrowserContext> {
validateBrowserContextOptions(options, this.options);
const createOptions = options.proxy ? { const createOptions = options.proxy ? {
proxyServer: options.proxy.server, proxyServer: options.proxy.server,
proxyBypassList: options.proxy.bypass proxyBypassList: options.proxy.bypass

View File

@ -114,6 +114,8 @@ export class WKPage implements PageDelegate {
} }
private async _initializePageProxySession() { private async _initializePageProxySession() {
if (this._page._browserContext.isSettingStorageState())
return;
const promises: Promise<any>[] = [ const promises: Promise<any>[] = [
this._pageProxySession.send('Dialog.enable'), this._pageProxySession.send('Dialog.enable'),
this._pageProxySession.send('Emulation.setActiveAndFocused', { active: true }), this._pageProxySession.send('Emulation.setActiveAndFocused', { active: true }),
@ -183,6 +185,10 @@ export class WKPage implements PageDelegate {
promises.push(session.send('Network.setInterceptionEnabled', { enabled: true })); promises.push(session.send('Network.setInterceptionEnabled', { enabled: true }));
promises.push(session.send('Network.addInterception', { url: '.*', stage: 'request', isRegex: true })); promises.push(session.send('Network.addInterception', { url: '.*', stage: 'request', isRegex: true }));
} }
if (this._page._browserContext.isSettingStorageState()) {
await Promise.all(promises);
return;
}
const contextOptions = this._browserContext._options; const contextOptions = this._browserContext._options;
if (contextOptions.userAgent) if (contextOptions.userAgent)

View File

@ -48,8 +48,8 @@ it('should capture local storage', async ({ contextFactory }) => {
}]); }]);
}); });
it('should set local storage', async ({ browser }) => { it('should set local storage', async ({ contextFactory }) => {
const context = await browser.newContext({ const context = await contextFactory({
storageState: { storageState: {
cookies: [], cookies: [],
origins: [ origins: [
@ -162,3 +162,35 @@ it('should not emit events about internal page', async ({ contextFactory }) => {
await context.storageState(); await context.storageState();
expect(events).toHaveLength(0); expect(events).toHaveLength(0);
}); });
it('should not restore localStorage twice', async ({ contextFactory }) => {
const context = await contextFactory({
storageState: {
cookies: [],
origins: [
{
origin: 'https://www.example.com',
localStorage: [{
name: 'name1',
value: 'value1'
}]
},
]
}
});
const page = await context.newPage();
await page.route('**/*', route => {
route.fulfill({ body: '<html></html>' }).catch(() => {});
});
await page.goto('https://www.example.com');
const localStorage1 = await page.evaluate('window.localStorage');
expect(localStorage1).toEqual({ name1: 'value1' });
await page.evaluate(() => window.localStorage['name1'] = 'value2');
await page.goto('https://www.example.com');
const localStorage2 = await page.evaluate('window.localStorage');
expect(localStorage2).toEqual({ name1: 'value2' });
await context.close();
});