chore: experimental resetForReuse (#15432)
This commit is contained in:
parent
a5571c9981
commit
5fc637e44a
|
|
@ -59,9 +59,23 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
||||||
}
|
}
|
||||||
|
|
||||||
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
|
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
|
||||||
|
return await this._innerNewContext(options, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _newContextForReuse(options: BrowserContextOptions = {}): Promise<BrowserContext> {
|
||||||
|
for (const context of this._contexts) {
|
||||||
|
await this._browserType._onWillCloseContext?.(context);
|
||||||
|
context._onClose();
|
||||||
|
}
|
||||||
|
this._contexts.clear();
|
||||||
|
return await this._innerNewContext(options, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _innerNewContext(options: BrowserContextOptions = {}, forReuse: boolean): Promise<BrowserContext> {
|
||||||
options = { ...this._browserType._defaultContextOptions, ...options };
|
options = { ...this._browserType._defaultContextOptions, ...options };
|
||||||
const contextOptions = await prepareBrowserContextParams(options);
|
const contextOptions = await prepareBrowserContextParams(options);
|
||||||
const context = BrowserContext.from((await this._channel.newContext(contextOptions)).context);
|
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
|
||||||
|
const context = BrowserContext.from(response.context);
|
||||||
context._options = contextOptions;
|
context._options = contextOptions;
|
||||||
this._contexts.add(context);
|
this._contexts.add(context);
|
||||||
context._logger = options.logger || this._logger;
|
context._logger = options.logger || this._logger;
|
||||||
|
|
|
||||||
|
|
@ -173,14 +173,14 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
setDefaultNavigationTimeout(timeout: number) {
|
setDefaultNavigationTimeout(timeout: number) {
|
||||||
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
|
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
|
||||||
this._wrapApiCall(async () => {
|
this._wrapApiCall(async () => {
|
||||||
this._channel.setDefaultNavigationTimeoutNoReply({ timeout });
|
this._channel.setDefaultNavigationTimeoutNoReply({ timeout }).catch(() => {});
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDefaultTimeout(timeout: number) {
|
setDefaultTimeout(timeout: number) {
|
||||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||||
this._wrapApiCall(async () => {
|
this._wrapApiCall(async () => {
|
||||||
this._channel.setDefaultTimeoutNoReply({ timeout });
|
this._channel.setDefaultTimeoutNoReply({ timeout }).catch(() => {});
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -251,14 +251,14 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||||
setDefaultNavigationTimeout(timeout: number) {
|
setDefaultNavigationTimeout(timeout: number) {
|
||||||
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
|
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
|
||||||
this._wrapApiCall(async () => {
|
this._wrapApiCall(async () => {
|
||||||
this._channel.setDefaultNavigationTimeoutNoReply({ timeout });
|
this._channel.setDefaultNavigationTimeoutNoReply({ timeout }).catch(() => {});
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDefaultTimeout(timeout: number) {
|
setDefaultTimeout(timeout: number) {
|
||||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||||
this._wrapApiCall(async () => {
|
this._wrapApiCall(async () => {
|
||||||
this._channel.setDefaultTimeoutNoReply({ timeout });
|
this._channel.setDefaultTimeoutNoReply({ timeout }).catch(() => {});
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -905,6 +905,7 @@ export interface BrowserChannel extends BrowserEventTarget, Channel {
|
||||||
close(params?: BrowserCloseParams, metadata?: Metadata): Promise<BrowserCloseResult>;
|
close(params?: BrowserCloseParams, metadata?: Metadata): Promise<BrowserCloseResult>;
|
||||||
killForTests(params?: BrowserKillForTestsParams, metadata?: Metadata): Promise<BrowserKillForTestsResult>;
|
killForTests(params?: BrowserKillForTestsParams, metadata?: Metadata): Promise<BrowserKillForTestsResult>;
|
||||||
newContext(params: BrowserNewContextParams, metadata?: Metadata): Promise<BrowserNewContextResult>;
|
newContext(params: BrowserNewContextParams, metadata?: Metadata): Promise<BrowserNewContextResult>;
|
||||||
|
newContextForReuse(params: BrowserNewContextForReuseParams, metadata?: Metadata): Promise<BrowserNewContextForReuseResult>;
|
||||||
newBrowserCDPSession(params?: BrowserNewBrowserCDPSessionParams, metadata?: Metadata): Promise<BrowserNewBrowserCDPSessionResult>;
|
newBrowserCDPSession(params?: BrowserNewBrowserCDPSessionParams, metadata?: Metadata): Promise<BrowserNewBrowserCDPSessionResult>;
|
||||||
startTracing(params: BrowserStartTracingParams, metadata?: Metadata): Promise<BrowserStartTracingResult>;
|
startTracing(params: BrowserStartTracingParams, metadata?: Metadata): Promise<BrowserStartTracingResult>;
|
||||||
stopTracing(params?: BrowserStopTracingParams, metadata?: Metadata): Promise<BrowserStopTracingResult>;
|
stopTracing(params?: BrowserStopTracingParams, metadata?: Metadata): Promise<BrowserStopTracingResult>;
|
||||||
|
|
@ -1033,6 +1034,123 @@ export type BrowserNewContextOptions = {
|
||||||
export type BrowserNewContextResult = {
|
export type BrowserNewContextResult = {
|
||||||
context: BrowserContextChannel,
|
context: BrowserContextChannel,
|
||||||
};
|
};
|
||||||
|
export type BrowserNewContextForReuseParams = {
|
||||||
|
noDefaultViewport?: boolean,
|
||||||
|
viewport?: {
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
},
|
||||||
|
screen?: {
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
},
|
||||||
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
javaScriptEnabled?: boolean,
|
||||||
|
bypassCSP?: boolean,
|
||||||
|
userAgent?: string,
|
||||||
|
locale?: string,
|
||||||
|
timezoneId?: string,
|
||||||
|
geolocation?: {
|
||||||
|
longitude: number,
|
||||||
|
latitude: number,
|
||||||
|
accuracy?: number,
|
||||||
|
},
|
||||||
|
permissions?: string[],
|
||||||
|
extraHTTPHeaders?: NameValue[],
|
||||||
|
offline?: boolean,
|
||||||
|
httpCredentials?: {
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
},
|
||||||
|
deviceScaleFactor?: number,
|
||||||
|
isMobile?: boolean,
|
||||||
|
hasTouch?: boolean,
|
||||||
|
colorScheme?: 'dark' | 'light' | 'no-preference',
|
||||||
|
reducedMotion?: 'reduce' | 'no-preference',
|
||||||
|
forcedColors?: 'active' | 'none',
|
||||||
|
acceptDownloads?: boolean,
|
||||||
|
baseURL?: string,
|
||||||
|
recordVideo?: {
|
||||||
|
dir: string,
|
||||||
|
size?: {
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recordHar?: RecordHarOptions,
|
||||||
|
strictSelectors?: boolean,
|
||||||
|
serviceWorkers?: 'allow' | 'block',
|
||||||
|
proxy?: {
|
||||||
|
server: string,
|
||||||
|
bypass?: string,
|
||||||
|
username?: string,
|
||||||
|
password?: string,
|
||||||
|
},
|
||||||
|
storageState?: {
|
||||||
|
cookies?: SetNetworkCookie[],
|
||||||
|
origins?: OriginStorage[],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export type BrowserNewContextForReuseOptions = {
|
||||||
|
noDefaultViewport?: boolean,
|
||||||
|
viewport?: {
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
},
|
||||||
|
screen?: {
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
},
|
||||||
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
javaScriptEnabled?: boolean,
|
||||||
|
bypassCSP?: boolean,
|
||||||
|
userAgent?: string,
|
||||||
|
locale?: string,
|
||||||
|
timezoneId?: string,
|
||||||
|
geolocation?: {
|
||||||
|
longitude: number,
|
||||||
|
latitude: number,
|
||||||
|
accuracy?: number,
|
||||||
|
},
|
||||||
|
permissions?: string[],
|
||||||
|
extraHTTPHeaders?: NameValue[],
|
||||||
|
offline?: boolean,
|
||||||
|
httpCredentials?: {
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
},
|
||||||
|
deviceScaleFactor?: number,
|
||||||
|
isMobile?: boolean,
|
||||||
|
hasTouch?: boolean,
|
||||||
|
colorScheme?: 'dark' | 'light' | 'no-preference',
|
||||||
|
reducedMotion?: 'reduce' | 'no-preference',
|
||||||
|
forcedColors?: 'active' | 'none',
|
||||||
|
acceptDownloads?: boolean,
|
||||||
|
baseURL?: string,
|
||||||
|
recordVideo?: {
|
||||||
|
dir: string,
|
||||||
|
size?: {
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recordHar?: RecordHarOptions,
|
||||||
|
strictSelectors?: boolean,
|
||||||
|
serviceWorkers?: 'allow' | 'block',
|
||||||
|
proxy?: {
|
||||||
|
server: string,
|
||||||
|
bypass?: string,
|
||||||
|
username?: string,
|
||||||
|
password?: string,
|
||||||
|
},
|
||||||
|
storageState?: {
|
||||||
|
cookies?: SetNetworkCookie[],
|
||||||
|
origins?: OriginStorage[],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export type BrowserNewContextForReuseResult = {
|
||||||
|
context: BrowserContextChannel,
|
||||||
|
};
|
||||||
export type BrowserNewBrowserCDPSessionParams = {};
|
export type BrowserNewBrowserCDPSessionParams = {};
|
||||||
export type BrowserNewBrowserCDPSessionOptions = {};
|
export type BrowserNewBrowserCDPSessionOptions = {};
|
||||||
export type BrowserNewBrowserCDPSessionResult = {
|
export type BrowserNewBrowserCDPSessionResult = {
|
||||||
|
|
|
||||||
|
|
@ -761,6 +761,28 @@ Browser:
|
||||||
returns:
|
returns:
|
||||||
context: BrowserContext
|
context: BrowserContext
|
||||||
|
|
||||||
|
newContextForReuse:
|
||||||
|
parameters:
|
||||||
|
$mixin: ContextOptions
|
||||||
|
proxy:
|
||||||
|
type: object?
|
||||||
|
properties:
|
||||||
|
server: string
|
||||||
|
bypass: string?
|
||||||
|
username: string?
|
||||||
|
password: string?
|
||||||
|
storageState:
|
||||||
|
type: object?
|
||||||
|
properties:
|
||||||
|
cookies:
|
||||||
|
type: array?
|
||||||
|
items: SetNetworkCookie
|
||||||
|
origins:
|
||||||
|
type: array?
|
||||||
|
items: OriginStorage
|
||||||
|
returns:
|
||||||
|
context: BrowserContext
|
||||||
|
|
||||||
newBrowserCDPSession:
|
newBrowserCDPSession:
|
||||||
returns:
|
returns:
|
||||||
session: CDPSession
|
session: CDPSession
|
||||||
|
|
@ -778,6 +800,7 @@ Browser:
|
||||||
returns:
|
returns:
|
||||||
binary: binary
|
binary: binary
|
||||||
|
|
||||||
|
|
||||||
events:
|
events:
|
||||||
|
|
||||||
close:
|
close:
|
||||||
|
|
|
||||||
|
|
@ -546,6 +546,66 @@ scheme.BrowserNewContextParams = tObject({
|
||||||
scheme.BrowserNewContextResult = tObject({
|
scheme.BrowserNewContextResult = tObject({
|
||||||
context: tChannel(['BrowserContext']),
|
context: tChannel(['BrowserContext']),
|
||||||
});
|
});
|
||||||
|
scheme.BrowserNewContextForReuseParams = tObject({
|
||||||
|
noDefaultViewport: tOptional(tBoolean),
|
||||||
|
viewport: tOptional(tObject({
|
||||||
|
width: tNumber,
|
||||||
|
height: tNumber,
|
||||||
|
})),
|
||||||
|
screen: tOptional(tObject({
|
||||||
|
width: tNumber,
|
||||||
|
height: tNumber,
|
||||||
|
})),
|
||||||
|
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||||
|
javaScriptEnabled: tOptional(tBoolean),
|
||||||
|
bypassCSP: tOptional(tBoolean),
|
||||||
|
userAgent: tOptional(tString),
|
||||||
|
locale: tOptional(tString),
|
||||||
|
timezoneId: tOptional(tString),
|
||||||
|
geolocation: tOptional(tObject({
|
||||||
|
longitude: tNumber,
|
||||||
|
latitude: tNumber,
|
||||||
|
accuracy: tOptional(tNumber),
|
||||||
|
})),
|
||||||
|
permissions: tOptional(tArray(tString)),
|
||||||
|
extraHTTPHeaders: tOptional(tArray(tType('NameValue'))),
|
||||||
|
offline: tOptional(tBoolean),
|
||||||
|
httpCredentials: tOptional(tObject({
|
||||||
|
username: tString,
|
||||||
|
password: tString,
|
||||||
|
})),
|
||||||
|
deviceScaleFactor: tOptional(tNumber),
|
||||||
|
isMobile: tOptional(tBoolean),
|
||||||
|
hasTouch: tOptional(tBoolean),
|
||||||
|
colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference'])),
|
||||||
|
reducedMotion: tOptional(tEnum(['reduce', 'no-preference'])),
|
||||||
|
forcedColors: tOptional(tEnum(['active', 'none'])),
|
||||||
|
acceptDownloads: tOptional(tBoolean),
|
||||||
|
baseURL: tOptional(tString),
|
||||||
|
recordVideo: tOptional(tObject({
|
||||||
|
dir: tString,
|
||||||
|
size: tOptional(tObject({
|
||||||
|
width: tNumber,
|
||||||
|
height: tNumber,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
recordHar: tOptional(tType('RecordHarOptions')),
|
||||||
|
strictSelectors: tOptional(tBoolean),
|
||||||
|
serviceWorkers: tOptional(tEnum(['allow', 'block'])),
|
||||||
|
proxy: tOptional(tObject({
|
||||||
|
server: tString,
|
||||||
|
bypass: tOptional(tString),
|
||||||
|
username: tOptional(tString),
|
||||||
|
password: tOptional(tString),
|
||||||
|
})),
|
||||||
|
storageState: tOptional(tObject({
|
||||||
|
cookies: tOptional(tArray(tType('SetNetworkCookie'))),
|
||||||
|
origins: tOptional(tArray(tType('OriginStorage'))),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
scheme.BrowserNewContextForReuseResult = tObject({
|
||||||
|
context: tChannel(['BrowserContext']),
|
||||||
|
});
|
||||||
scheme.BrowserNewBrowserCDPSessionParams = tOptional(tObject({}));
|
scheme.BrowserNewBrowserCDPSessionParams = tOptional(tObject({}));
|
||||||
scheme.BrowserNewBrowserCDPSessionResult = tObject({
|
scheme.BrowserNewBrowserCDPSessionResult = tObject({
|
||||||
session: tChannel(['CDPSession']),
|
session: tChannel(['CDPSession']),
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,40 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
await mkdirIfNeeded(path.join(this._options.recordVideo.dir, 'dummy'));
|
await mkdirIfNeeded(path.join(this._options.recordVideo.dir, 'dummy'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canResetForReuse(): boolean {
|
||||||
|
if (this._closedStatus !== 'open')
|
||||||
|
return false;
|
||||||
|
if (this.pages().length < 1)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetForReuse(metadata: CallMetadata) {
|
||||||
|
this.setDefaultNavigationTimeout(undefined);
|
||||||
|
this.setDefaultTimeout(undefined);
|
||||||
|
|
||||||
|
await this._cancelAllRoutesInFlight();
|
||||||
|
|
||||||
|
const [page, ...otherPages] = this.pages();
|
||||||
|
for (const page of otherPages)
|
||||||
|
await page.close(metadata);
|
||||||
|
// Unless I do this early, setting extra http headers below does not respond.
|
||||||
|
await page._frameManager.closeOpenDialogs();
|
||||||
|
await page.mainFrame().goto(metadata, 'about:blank', { timeout: 0 });
|
||||||
|
await this.removeExposedBindings();
|
||||||
|
await this.removeInitScripts();
|
||||||
|
// TODO: following can be optimized to not perform noops.
|
||||||
|
if (this._options.permissions)
|
||||||
|
await this.grantPermissions(this._options.permissions);
|
||||||
|
else
|
||||||
|
await this.clearPermissions();
|
||||||
|
await this.setExtraHTTPHeaders(this._options.extraHTTPHeaders || []);
|
||||||
|
await this.setGeolocation(this._options.geolocation);
|
||||||
|
await this.setOffline(!!this._options.offline);
|
||||||
|
|
||||||
|
await page.resetForReuse(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
_browserClosed() {
|
_browserClosed() {
|
||||||
for (const page of this.pages())
|
for (const page of this.pages())
|
||||||
page._didClose();
|
page._didClose();
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ export class CRPage implements PageDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEmulateMedia(): Promise<void> {
|
async updateEmulateMedia(): Promise<void> {
|
||||||
await this._forAllFrameSessions(frame => frame._updateEmulateMedia(false));
|
await this._forAllFrameSessions(frame => frame._updateEmulateMedia());
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateRequestInterception(): Promise<void> {
|
async updateRequestInterception(): Promise<void> {
|
||||||
|
|
@ -554,7 +554,7 @@ class FrameSession {
|
||||||
promises.push(this._updateRequestInterception());
|
promises.push(this._updateRequestInterception());
|
||||||
promises.push(this._updateOffline(true));
|
promises.push(this._updateOffline(true));
|
||||||
promises.push(this._updateHttpCredentials(true));
|
promises.push(this._updateHttpCredentials(true));
|
||||||
promises.push(this._updateEmulateMedia(true));
|
promises.push(this._updateEmulateMedia());
|
||||||
promises.push(this._updateFileChooserInterception(true));
|
promises.push(this._updateFileChooserInterception(true));
|
||||||
for (const binding of this._crPage._page.allBindings())
|
for (const binding of this._crPage._page.allBindings())
|
||||||
promises.push(this._initBinding(binding));
|
promises.push(this._initBinding(binding));
|
||||||
|
|
@ -1057,7 +1057,7 @@ class FrameSession {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _updateEmulateMedia(initial: boolean): Promise<void> {
|
async _updateEmulateMedia(): Promise<void> {
|
||||||
const emulatedMedia = this._page.emulatedMedia();
|
const emulatedMedia = this._page.emulatedMedia();
|
||||||
const colorScheme = emulatedMedia.colorScheme === null ? '' : emulatedMedia.colorScheme;
|
const colorScheme = emulatedMedia.colorScheme === null ? '' : emulatedMedia.colorScheme;
|
||||||
const reducedMotion = emulatedMedia.reducedMotion === null ? '' : emulatedMedia.reducedMotion;
|
const reducedMotion = emulatedMedia.reducedMotion === null ? '' : emulatedMedia.reducedMotion;
|
||||||
|
|
@ -1084,7 +1084,7 @@ class FrameSession {
|
||||||
const enabled = this._page.fileChooserIntercepted();
|
const enabled = this._page.fileChooserIntercepted();
|
||||||
if (initial && !enabled)
|
if (initial && !enabled)
|
||||||
return;
|
return;
|
||||||
await this._client.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => {}); // target can be closed.
|
await this._client.send('Page.setInterceptFileChooserDialog', { enabled }).catch(() => {}); // target can be closed.
|
||||||
}
|
}
|
||||||
|
|
||||||
async _evaluateOnNewDocument(source: string, world: types.World): Promise<void> {
|
async _evaluateOnNewDocument(source: string, world: types.World): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -224,4 +224,9 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||||
throw new Error('No HAR artifact. Ensure record.harPath is set.');
|
throw new Error('No HAR artifact. Ensure record.harPath is set.');
|
||||||
return { artifact: new ArtifactDispatcher(this._scope, artifact) };
|
return { artifact: new ArtifactDispatcher(this._scope, artifact) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override _dispose() {
|
||||||
|
super._dispose();
|
||||||
|
this._context.setRequestInterceptor(undefined).catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { Browser } from '../browser';
|
||||||
import type * as channels from '../../protocol/channels';
|
import type * as channels from '../../protocol/channels';
|
||||||
import { BrowserContextDispatcher } from './browserContextDispatcher';
|
import { BrowserContextDispatcher } from './browserContextDispatcher';
|
||||||
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
|
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
|
||||||
|
import { existingDispatcher } from './dispatcher';
|
||||||
import type { DispatcherScope } from './dispatcher';
|
import type { DispatcherScope } from './dispatcher';
|
||||||
import { Dispatcher } from './dispatcher';
|
import { Dispatcher } from './dispatcher';
|
||||||
import type { CRBrowser } from '../chromium/crBrowser';
|
import type { CRBrowser } from '../chromium/crBrowser';
|
||||||
|
|
@ -29,6 +30,8 @@ import { Selectors } from '../selectors';
|
||||||
|
|
||||||
export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserChannel> implements channels.BrowserChannel {
|
export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserChannel> implements channels.BrowserChannel {
|
||||||
_type_Browser = true;
|
_type_Browser = true;
|
||||||
|
private _contextForReuse: { context: BrowserContext, hash: string } | undefined;
|
||||||
|
|
||||||
constructor(scope: DispatcherScope, browser: Browser) {
|
constructor(scope: DispatcherScope, browser: Browser) {
|
||||||
super(scope, browser, 'Browser', { version: browser.version(), name: browser.options.name }, true);
|
super(scope, browser, 'Browser', { version: browser.version(), name: browser.options.name }, true);
|
||||||
this.addObjectListener(Browser.Events.Disconnected, () => this._didClose());
|
this.addObjectListener(Browser.Events.Disconnected, () => this._didClose());
|
||||||
|
|
@ -44,6 +47,24 @@ export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserChann
|
||||||
return { context: new BrowserContextDispatcher(this._scope, context) };
|
return { context: new BrowserContextDispatcher(this._scope, context) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for inner loop scenarios where user would like to preserve the browser window, opened page and devtools instance.
|
||||||
|
*/
|
||||||
|
async newContextForReuse(params: channels.BrowserNewContextForReuseParams, metadata: CallMetadata): Promise<channels.BrowserNewContextForReuseResult> {
|
||||||
|
const hash = JSON.stringify(params);
|
||||||
|
if (!this._contextForReuse || hash !== this._contextForReuse.hash || !this._contextForReuse.context.canResetForReuse()) {
|
||||||
|
if (this._contextForReuse)
|
||||||
|
await this._contextForReuse.context.close(metadata);
|
||||||
|
this._contextForReuse = { context: await this._object.newContext(metadata, params), hash };
|
||||||
|
} else {
|
||||||
|
const oldContextDispatcher = existingDispatcher<BrowserContextDispatcher>(this._contextForReuse.context);
|
||||||
|
oldContextDispatcher._dispose();
|
||||||
|
await this._contextForReuse.context.resetForReuse(metadata);
|
||||||
|
}
|
||||||
|
const context = new BrowserContextDispatcher(this._scope, this._contextForReuse.context);
|
||||||
|
return { context };
|
||||||
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
await this._object.close();
|
await this._object.close();
|
||||||
}
|
}
|
||||||
|
|
@ -97,6 +118,10 @@ export class ConnectedBrowserDispatcher extends Dispatcher<Browser, channels.Bro
|
||||||
return { context: new BrowserContextDispatcher(this._scope, context) };
|
return { context: new BrowserContextDispatcher(this._scope, context) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newContextForReuse(params: channels.BrowserNewContextForReuseParams, metadata?: channels.Metadata | undefined): Promise<channels.BrowserNewContextForReuseResult> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
// Client should not send us Browser.close.
|
// Client should not send us Browser.close.
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ export class Dispatcher<Type extends { guid: string }, ChannelType> extends Even
|
||||||
this._connection.sendEvent(this, method as string, params, sdkObject);
|
this._connection.sendEvent(this, method as string, params, sdkObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _dispose() {
|
_dispose() {
|
||||||
assert(!this._disposed);
|
assert(!this._disposed);
|
||||||
this._disposed = true;
|
this._disposed = true;
|
||||||
eventsHelper.removeEventListeners(this._eventListeners);
|
eventsHelper.removeEventListeners(this._eventListeners);
|
||||||
|
|
@ -116,6 +116,7 @@ export class Dispatcher<Type extends { guid: string }, ChannelType> extends Even
|
||||||
|
|
||||||
if (this._isScope)
|
if (this._isScope)
|
||||||
this._connection.sendDispose(this);
|
this._connection.sendDispose(this);
|
||||||
|
delete (this._object as any)[dispatcherSymbol];
|
||||||
}
|
}
|
||||||
|
|
||||||
_debugScopeState(): any {
|
_debugScopeState(): any {
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,11 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel> imple
|
||||||
_onFrameDetached(frame: Frame) {
|
_onFrameDetached(frame: Frame) {
|
||||||
this._dispatchEvent('frameDetached', { frame: lookupDispatcher<FrameDispatcher>(frame) });
|
this._dispatchEvent('frameDetached', { frame: lookupDispatcher<FrameDispatcher>(frame) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override _dispose() {
|
||||||
|
super._dispose();
|
||||||
|
this._page.setClientRequestInterceptor(undefined).catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -381,8 +381,9 @@ export class FFPage implements PageDelegate {
|
||||||
await this._networkManager.setRequestInterception(this._page.needsRequestInterception());
|
await this._networkManager.setRequestInterception(this._page.needsRequestInterception());
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFileChooserInterception(enabled: boolean) {
|
async updateFileChooserInterception() {
|
||||||
await this._session.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => {}); // target can be closed.
|
const enabled = this._page.fileChooserIntercepted();
|
||||||
|
await this._session.send('Page.setInterceptFileChooserDialog', { enabled }).catch(() => {}); // target can be closed.
|
||||||
}
|
}
|
||||||
|
|
||||||
async reload(): Promise<void> {
|
async reload(): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -359,6 +359,11 @@ export class FrameManager {
|
||||||
this._openedDialogs.delete(dialog);
|
this._openedDialogs.delete(dialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async closeOpenDialogs() {
|
||||||
|
await Promise.all([...this._openedDialogs].map(dialog => dialog.dismiss())).catch(() => {});
|
||||||
|
this._openedDialogs.clear();
|
||||||
|
}
|
||||||
|
|
||||||
removeChildFramesRecursively(frame: Frame) {
|
removeChildFramesRecursively(frame: Frame) {
|
||||||
for (const child of frame.childFrames())
|
for (const child of frame.childFrames())
|
||||||
this._removeFramesRecursively(child);
|
this._removeFramesRecursively(child);
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export interface PageDelegate {
|
||||||
updateEmulatedViewportSize(): Promise<void>;
|
updateEmulatedViewportSize(): Promise<void>;
|
||||||
updateEmulateMedia(): Promise<void>;
|
updateEmulateMedia(): Promise<void>;
|
||||||
updateRequestInterception(): Promise<void>;
|
updateRequestInterception(): Promise<void>;
|
||||||
updateFileChooserInterception(enabled: boolean): Promise<void>;
|
updateFileChooserInterception(): Promise<void>;
|
||||||
bringToFront(): Promise<void>;
|
bringToFront(): Promise<void>;
|
||||||
|
|
||||||
setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void>;
|
setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void>;
|
||||||
|
|
@ -227,6 +227,29 @@ export class Page extends SdkObject {
|
||||||
this._browserContext.emit(event, ...args);
|
this._browserContext.emit(event, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async resetForReuse(metadata: CallMetadata) {
|
||||||
|
this.setDefaultNavigationTimeout(undefined);
|
||||||
|
this.setDefaultTimeout(undefined);
|
||||||
|
|
||||||
|
// To this first in order to unfreeze evaluates.
|
||||||
|
await this._frameManager.closeOpenDialogs();
|
||||||
|
|
||||||
|
await this.removeExposedBindings();
|
||||||
|
await this.removeInitScripts();
|
||||||
|
await this._setServerRequestInterceptor(undefined);
|
||||||
|
await this.setFileChooserIntercepted(false);
|
||||||
|
await this.mainFrame().goto(metadata, 'about:blank');
|
||||||
|
this._emulatedSize = undefined;
|
||||||
|
this._emulatedMedia = {};
|
||||||
|
this._extraHTTPHeaders = undefined;
|
||||||
|
this._interceptFileChooser = false;
|
||||||
|
|
||||||
|
await this._delegate.updateEmulatedViewportSize();
|
||||||
|
await this._delegate.updateEmulateMedia();
|
||||||
|
await this._delegate.updateExtraHTTPHeaders();
|
||||||
|
await this._delegate.updateFileChooserInterception();
|
||||||
|
}
|
||||||
|
|
||||||
async _doSlowMo() {
|
async _doSlowMo() {
|
||||||
const slowMo = this._browserContext._browser.options.slowMo;
|
const slowMo = this._browserContext._browser.options.slowMo;
|
||||||
if (!slowMo)
|
if (!slowMo)
|
||||||
|
|
@ -619,7 +642,7 @@ export class Page extends SdkObject {
|
||||||
|
|
||||||
async setFileChooserIntercepted(enabled: boolean): Promise<void> {
|
async setFileChooserIntercepted(enabled: boolean): Promise<void> {
|
||||||
this._interceptFileChooser = enabled;
|
this._interceptFileChooser = enabled;
|
||||||
await this._delegate.updateFileChooserInterception(enabled);
|
await this._delegate.updateFileChooserInterception();
|
||||||
}
|
}
|
||||||
|
|
||||||
fileChooserIntercepted() {
|
fileChooserIntercepted() {
|
||||||
|
|
|
||||||
|
|
@ -730,7 +730,7 @@ export class WKPage implements PageDelegate {
|
||||||
|
|
||||||
async updateFileChooserInterception() {
|
async updateFileChooserInterception() {
|
||||||
const enabled = this._page.fileChooserIntercepted();
|
const enabled = this._page.fileChooserIntercepted();
|
||||||
await this._session.send('Page.setInterceptFileChooserDialog', { enabled }).catch(e => {}); // target can be closed.
|
await this._session.send('Page.setInterceptFileChooserDialog', { enabled }).catch(() => {}); // target can be closed.
|
||||||
}
|
}
|
||||||
|
|
||||||
async reload(): Promise<void> {
|
async reload(): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ if ((process as any)['__pw_initiator__']) {
|
||||||
|
|
||||||
type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
|
type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
|
||||||
_combinedContextOptions: BrowserContextOptions,
|
_combinedContextOptions: BrowserContextOptions,
|
||||||
|
_contextReuseEnabled: boolean,
|
||||||
_setupContextOptionsAndArtifacts: void;
|
_setupContextOptionsAndArtifacts: void;
|
||||||
_contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
|
_contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
|
||||||
};
|
};
|
||||||
|
|
@ -249,7 +250,6 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
||||||
const captureTrace = shouldCaptureTrace(traceMode, testInfo);
|
const captureTrace = shouldCaptureTrace(traceMode, testInfo);
|
||||||
const temporaryTraceFiles: string[] = [];
|
const temporaryTraceFiles: string[] = [];
|
||||||
const temporaryScreenshots: string[] = [];
|
const temporaryScreenshots: string[] = [];
|
||||||
const createdContexts = new Set<BrowserContext>();
|
|
||||||
const testInfoImpl = testInfo as TestInfoImpl;
|
const testInfoImpl = testInfo as TestInfoImpl;
|
||||||
|
|
||||||
const createInstrumentationListener = (context?: BrowserContext) => {
|
const createInstrumentationListener = (context?: BrowserContext) => {
|
||||||
|
|
@ -288,13 +288,14 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
||||||
await tracing.startChunk({ title });
|
await tracing.startChunk({ title });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(tracing as any)[kTracingStarted] = false;
|
if ((tracing as any)[kTracingStarted]) {
|
||||||
await tracing.stop();
|
(tracing as any)[kTracingStarted] = false;
|
||||||
|
await tracing.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDidCreateBrowserContext = async (context: BrowserContext) => {
|
const onDidCreateBrowserContext = async (context: BrowserContext) => {
|
||||||
createdContexts.add(context);
|
|
||||||
context.setDefaultTimeout(actionTimeout || 0);
|
context.setDefaultTimeout(actionTimeout || 0);
|
||||||
context.setDefaultNavigationTimeout(navigationTimeout || actionTimeout || 0);
|
context.setDefaultNavigationTimeout(navigationTimeout || actionTimeout || 0);
|
||||||
await startTracing(context.tracing);
|
await startTracing(context.tracing);
|
||||||
|
|
@ -505,12 +506,33 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
||||||
testInfo.errors.push({ message: prependToError });
|
testInfo.errors.push({ message: prependToError });
|
||||||
}, { scope: 'test', _title: 'context' } as any],
|
}, { scope: 'test', _title: 'context' } as any],
|
||||||
|
|
||||||
context: async ({ _contextFactory }, use) => {
|
_contextReuseEnabled: async ({ video, trace }, use, testInfo) => {
|
||||||
await use(await _contextFactory());
|
const reuse = !!process.env.PW_REUSE_CONTEXT && !shouldCaptureVideo(normalizeVideoMode(video), testInfo) && !shouldCaptureTrace(normalizeTraceMode(trace), testInfo);
|
||||||
|
await use(reuse);
|
||||||
},
|
},
|
||||||
|
|
||||||
page: async ({ context }, use) => {
|
context: async ({ playwright, browser, _contextReuseEnabled, _contextFactory }, use, testInfo) => {
|
||||||
await use(await context.newPage());
|
if (!_contextReuseEnabled) {
|
||||||
|
await use(await _contextFactory());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultContextOptions = (playwright.chromium as any)._defaultContextOptions as BrowserContextOptions;
|
||||||
|
const context = await (browser as any)._newContextForReuse(defaultContextOptions);
|
||||||
|
await use(context);
|
||||||
|
},
|
||||||
|
|
||||||
|
page: async ({ context, _contextReuseEnabled }, use) => {
|
||||||
|
if (!_contextReuseEnabled) {
|
||||||
|
await use(await context.newPage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First time we are reusing the context, we should create the page.
|
||||||
|
let [page] = context.pages();
|
||||||
|
if (!page)
|
||||||
|
page = await context.newPage();
|
||||||
|
await use(page);
|
||||||
},
|
},
|
||||||
|
|
||||||
request: async ({ playwright, _combinedContextOptions }, use) => {
|
request: async ({ playwright, _combinedContextOptions }, use) => {
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,9 @@ it('should log @smoke', async ({ browserType }) => {
|
||||||
} });
|
} });
|
||||||
await browser.newContext();
|
await browser.newContext();
|
||||||
await browser.close();
|
await browser.close();
|
||||||
expect(log.length > 0).toBeTruthy();
|
expect(log.find(item => item.severity === 'info')).toBeTruthy();
|
||||||
expect(log.filter(item => item.severity === 'info').length > 0).toBeTruthy();
|
expect(log.find(item => item.message.includes('browser.newContext started'))).toBeTruthy();
|
||||||
expect(log.filter(item => item.message.includes('browser.newContext started')).length > 0).toBeTruthy();
|
expect(log.find(item => item.message.includes('browser.newContext succeeded'))).toBeTruthy();
|
||||||
expect(log.filter(item => item.message.includes('browser.newContext succeeded')).length > 0).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should log context-level', async ({ browserType }) => {
|
it('should log context-level', async ({ browserType }) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue