chore: reset page/context for reuse in component tests (#13264)

This commit is contained in:
Pavel Feldman 2022-04-04 11:39:43 -08:00 committed by GitHub
parent 8232497c88
commit 1bebc28aed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 194 additions and 17 deletions

View File

@ -242,11 +242,20 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
await this._channel.addInitScript({ source });
}
async _removeInitScripts() {
await this._channel.removeInitScripts();
}
async exposeBinding(name: string, callback: (source: structs.BindingSource, ...args: any[]) => any, options: { handle?: boolean } = {}): Promise<void> {
await this._channel.exposeBinding({ name, needsHandle: options.handle });
this._bindings.set(name, callback);
}
async _removeExposedBindings() {
this._bindings.clear();
await this._channel.removeExposedBindings();
}
async exposeFunction(name: string, callback: Function): Promise<void> {
await this._channel.exposeBinding({ name });
const binding = (source: structs.BindingSource, ...args: any[]) => callback(...args);
@ -265,6 +274,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
await this._disableInterception();
}
async _unrouteAll() {
this._routes = [];
await this._disableInterception();
}
private async _disableInterception() {
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
}
@ -346,6 +360,12 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
}) {
await this._channel.recorderSupplementEnable(params);
}
async _resetForReuse() {
await this._unrouteAll();
await this._removeInitScripts();
await this._removeExposedBindings();
}
}
async function prepareStorageState(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams['storageState']> {

View File

@ -329,6 +329,11 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
this._bindings.set(name, callback);
}
async _removeExposedBindings() {
this._bindings.clear();
await this._channel.removeExposedBindings();
}
async setExtraHTTPHeaders(headers: Headers) {
validateHeaders(headers);
await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) });
@ -449,6 +454,10 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
await this._channel.addInitScript({ source });
}
async _removeInitScripts() {
await this._channel.removeInitScripts();
}
async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
this._routes.unshift(new RouteHandler(this._browserContext._options.baseURL, url, handler, options.times));
if (this._routes.length === 1)
@ -461,6 +470,11 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
await this._disableInterception();
}
async _unrouteAll() {
this._routes = [];
await this._disableInterception();
}
private async _disableInterception() {
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
}
@ -715,6 +729,12 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}
return buffer;
}
async _resetForReuse() {
await this._unrouteAll();
await this._removeInitScripts();
await this._removeExposedBindings();
}
}
export class BindingCall extends ChannelOwner<channels.BindingCallChannel> {

View File

@ -125,6 +125,10 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
});
}
async removeExposedBindings() {
await this._context.removeExposedBindings();
}
async newPage(params: channels.BrowserContextNewPageParams, metadata: CallMetadata): Promise<channels.BrowserContextNewPageResult> {
return { page: lookupDispatcher<PageDispatcher>(await this._context.newPage(metadata)) };
}
@ -169,6 +173,10 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
await this._context.addInitScript(params.source);
}
async removeInitScripts(): Promise<void> {
await this._context.removeInitScripts();
}
async setNetworkInterceptionEnabled(params: channels.BrowserContextSetNetworkInterceptionEnabledParams): Promise<void> {
if (!params.enabled) {
await this._context.setRequestInterceptor(undefined);

View File

@ -107,6 +107,10 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel> imple
});
}
async removeExposedBindings() {
await this._page.removeExposedBindings();
}
async setExtraHTTPHeaders(params: channels.PageSetExtraHTTPHeadersParams, metadata: CallMetadata): Promise<void> {
await this._page.setExtraHTTPHeaders(params.headers);
}
@ -140,6 +144,10 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel> imple
await this._page.addInitScript(params.source);
}
async removeInitScripts(): Promise<void> {
await this._page.removeInitScripts();
}
async setNetworkInterceptionEnabled(params: channels.PageSetNetworkInterceptionEnabledParams, metadata: CallMetadata): Promise<void> {
if (!params.enabled) {
await this._page.setClientRequestInterceptor(undefined);

View File

@ -1056,11 +1056,13 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
_type_BrowserContext: boolean;
addCookies(params: BrowserContextAddCookiesParams, metadata?: Metadata): Promise<BrowserContextAddCookiesResult>;
addInitScript(params: BrowserContextAddInitScriptParams, metadata?: Metadata): Promise<BrowserContextAddInitScriptResult>;
removeInitScripts(params?: BrowserContextRemoveInitScriptsParams, metadata?: Metadata): Promise<BrowserContextRemoveInitScriptsResult>;
clearCookies(params?: BrowserContextClearCookiesParams, metadata?: Metadata): Promise<BrowserContextClearCookiesResult>;
clearPermissions(params?: BrowserContextClearPermissionsParams, metadata?: Metadata): Promise<BrowserContextClearPermissionsResult>;
close(params?: BrowserContextCloseParams, metadata?: Metadata): Promise<BrowserContextCloseResult>;
cookies(params: BrowserContextCookiesParams, metadata?: Metadata): Promise<BrowserContextCookiesResult>;
exposeBinding(params: BrowserContextExposeBindingParams, metadata?: Metadata): Promise<BrowserContextExposeBindingResult>;
removeExposedBindings(params?: BrowserContextRemoveExposedBindingsParams, metadata?: Metadata): Promise<BrowserContextRemoveExposedBindingsResult>;
grantPermissions(params: BrowserContextGrantPermissionsParams, metadata?: Metadata): Promise<BrowserContextGrantPermissionsResult>;
newPage(params?: BrowserContextNewPageParams, metadata?: Metadata): Promise<BrowserContextNewPageResult>;
setDefaultNavigationTimeoutNoReply(params: BrowserContextSetDefaultNavigationTimeoutNoReplyParams, metadata?: Metadata): Promise<BrowserContextSetDefaultNavigationTimeoutNoReplyResult>;
@ -1131,6 +1133,9 @@ export type BrowserContextAddInitScriptOptions = {
};
export type BrowserContextAddInitScriptResult = void;
export type BrowserContextRemoveInitScriptsParams = {};
export type BrowserContextRemoveInitScriptsOptions = {};
export type BrowserContextRemoveInitScriptsResult = void;
export type BrowserContextClearCookiesParams = {};
export type BrowserContextClearCookiesOptions = {};
export type BrowserContextClearCookiesResult = void;
@ -1157,6 +1162,9 @@ export type BrowserContextExposeBindingOptions = {
needsHandle?: boolean,
};
export type BrowserContextExposeBindingResult = void;
export type BrowserContextRemoveExposedBindingsParams = {};
export type BrowserContextRemoveExposedBindingsOptions = {};
export type BrowserContextRemoveExposedBindingsResult = void;
export type BrowserContextGrantPermissionsParams = {
permissions: string[],
origin?: string,
@ -1337,9 +1345,11 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
setDefaultTimeoutNoReply(params: PageSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise<PageSetDefaultTimeoutNoReplyResult>;
setFileChooserInterceptedNoReply(params: PageSetFileChooserInterceptedNoReplyParams, metadata?: Metadata): Promise<PageSetFileChooserInterceptedNoReplyResult>;
addInitScript(params: PageAddInitScriptParams, metadata?: Metadata): Promise<PageAddInitScriptResult>;
removeInitScripts(params?: PageRemoveInitScriptsParams, metadata?: Metadata): Promise<PageRemoveInitScriptsResult>;
close(params: PageCloseParams, metadata?: Metadata): Promise<PageCloseResult>;
emulateMedia(params: PageEmulateMediaParams, metadata?: Metadata): Promise<PageEmulateMediaResult>;
exposeBinding(params: PageExposeBindingParams, metadata?: Metadata): Promise<PageExposeBindingResult>;
removeExposedBindings(params?: PageRemoveExposedBindingsParams, metadata?: Metadata): Promise<PageRemoveExposedBindingsResult>;
goBack(params: PageGoBackParams, metadata?: Metadata): Promise<PageGoBackResult>;
goForward(params: PageGoForwardParams, metadata?: Metadata): Promise<PageGoForwardResult>;
reload(params: PageReloadParams, metadata?: Metadata): Promise<PageReloadResult>;
@ -1439,6 +1449,9 @@ export type PageAddInitScriptOptions = {
};
export type PageAddInitScriptResult = void;
export type PageRemoveInitScriptsParams = {};
export type PageRemoveInitScriptsOptions = {};
export type PageRemoveInitScriptsResult = void;
export type PageCloseParams = {
runBeforeUnload?: boolean,
};
@ -1467,6 +1480,9 @@ export type PageExposeBindingOptions = {
needsHandle?: boolean,
};
export type PageExposeBindingResult = void;
export type PageRemoveExposedBindingsParams = {};
export type PageRemoveExposedBindingsOptions = {};
export type PageRemoveExposedBindingsResult = void;
export type PageGoBackParams = {
timeout?: number,
waitUntil?: LifecycleEvent,

View File

@ -761,6 +761,8 @@ BrowserContext:
parameters:
source: string
removeInitScripts:
clearCookies:
clearPermissions:
@ -782,6 +784,8 @@ BrowserContext:
name: string
needsHandle: boolean?
removeExposedBindings:
grantPermissions:
parameters:
permissions:
@ -873,7 +877,6 @@ BrowserContext:
returns:
writableStream: WritableStream
events:
bindingCall:
@ -960,6 +963,8 @@ Page:
parameters:
source: string
removeInitScripts:
close:
parameters:
runBeforeUnload: boolean?
@ -1001,6 +1006,8 @@ Page:
name: string
needsHandle: boolean?
removeExposedBindings:
goBack:
parameters:
timeout: number?

View File

@ -444,6 +444,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
scheme.BrowserContextAddInitScriptParams = tObject({
source: tString,
});
scheme.BrowserContextRemoveInitScriptsParams = tOptional(tObject({}));
scheme.BrowserContextClearCookiesParams = tOptional(tObject({}));
scheme.BrowserContextClearPermissionsParams = tOptional(tObject({}));
scheme.BrowserContextCloseParams = tOptional(tObject({}));
@ -454,6 +455,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
name: tString,
needsHandle: tOptional(tBoolean),
});
scheme.BrowserContextRemoveExposedBindingsParams = tOptional(tObject({}));
scheme.BrowserContextGrantPermissionsParams = tObject({
permissions: tArray(tString),
origin: tOptional(tString),
@ -519,6 +521,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
scheme.PageAddInitScriptParams = tObject({
source: tString,
});
scheme.PageRemoveInitScriptsParams = tOptional(tObject({}));
scheme.PageCloseParams = tObject({
runBeforeUnload: tOptional(tBoolean),
});
@ -532,6 +535,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
name: tString,
needsHandle: tOptional(tBoolean),
});
scheme.PageRemoveExposedBindingsParams = tOptional(tObject({}));
scheme.PageGoBackParams = tObject({
timeout: tOptional(tNumber),
waitUntil: tOptional(tType('LifecycleEvent')),

View File

@ -163,7 +163,9 @@ export abstract class BrowserContext extends SdkObject {
protected abstract doClearPermissions(): Promise<void>;
protected abstract doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void>;
protected abstract doAddInitScript(expression: string): Promise<void>;
protected abstract doRemoveInitScripts(): Promise<void>;
protected abstract doExposeBinding(binding: PageBinding): Promise<void>;
protected abstract doRemoveExposedBindings(): Promise<void>;
protected abstract doUpdateRequestInterception(): Promise<void>;
protected abstract doClose(): Promise<void>;
protected abstract onClosePersistent(): void;
@ -190,6 +192,11 @@ export abstract class BrowserContext extends SdkObject {
await this.doExposeBinding(binding);
}
async removeExposedBindings() {
this._pageBindings.clear();
await this.doRemoveExposedBindings();
}
async grantPermissions(permissions: string[], origin?: string) {
let resolvedOrigin = '*';
if (origin) {
@ -270,6 +277,11 @@ export abstract class BrowserContext extends SdkObject {
await this.doAddInitScript(script);
}
async removeInitScripts(): Promise<void> {
this.initScripts.splice(0, this.initScripts.length);
await this.doRemoveInitScripts();
}
async setRequestInterceptor(handler: network.RouteHandler | undefined): Promise<void> {
this._requestInterceptor = handler;
await this.doUpdateRequestInterception();

View File

@ -464,11 +464,21 @@ export class CRBrowserContext extends BrowserContext {
await (page._delegate as CRPage).addInitScript(source);
}
async doRemoveInitScripts() {
for (const page of this.pages())
await (page._delegate as CRPage).removeInitScripts();
}
async doExposeBinding(binding: PageBinding) {
for (const page of this.pages())
await (page._delegate as CRPage).exposeBinding(binding);
}
async doRemoveExposedBindings() {
for (const page of this.pages())
await (page._delegate as CRPage).removeExposedBindings();
}
async doUpdateRequestInterception(): Promise<void> {
for (const page of this.pages())
await (page._delegate as CRPage).updateRequestInterception();

View File

@ -179,6 +179,10 @@ export class CRPage implements PageDelegate {
await Promise.all(this._page.frames().map(frame => frame.evaluateExpression(binding.source, false, {}).catch(e => {})));
}
async removeExposedBindings() {
await this._forAllFrameSessions(frame => frame._removeExposedBindings());
}
async updateExtraHTTPHeaders(): Promise<void> {
await this._forAllFrameSessions(frame => frame._updateExtraHTTPHeaders(false));
}
@ -241,6 +245,10 @@ export class CRPage implements PageDelegate {
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(source, world));
}
async removeInitScripts() {
await this._forAllFrameSessions(frame => frame._removeEvaluatesOnNewDocument());
}
async closePage(runBeforeUnload: boolean): Promise<void> {
if (runBeforeUnload)
await this._mainFrameSession._client.send('Page.close');
@ -388,6 +396,8 @@ class FrameSession {
private _videoRecorder: VideoRecorder | null = null;
private _screencastId: string | null = null;
private _screencastClients = new Set<any>();
private _evaluateOnNewDocumentIdentifiers: string[] = [];
private _exposedBindingNames: string[] = [];
constructor(crPage: CRPage, client: CRSession, targetId: string, parentSession: FrameSession | null) {
this._client = client;
@ -798,10 +808,18 @@ class FrameSession {
}
async _initBinding(binding: PageBinding) {
await Promise.all([
const [ , response ] = await Promise.all([
this._client.send('Runtime.addBinding', { name: binding.name }),
this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: binding.source })
]);
this._exposedBindingNames.push(binding.name);
this._evaluateOnNewDocumentIdentifiers.push(response.identifier);
}
async _removeExposedBindings() {
const names = this._exposedBindingNames;
this._exposedBindingNames = [];
await Promise.all(names.map(name => this._client.send('Runtime.removeBinding', { name })));
}
async _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) {
@ -1054,7 +1072,14 @@ class FrameSession {
async _evaluateOnNewDocument(source: string, world: types.World): Promise<void> {
const worldName = world === 'utility' ? UTILITY_WORLD_NAME : undefined;
await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source, worldName });
const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source, worldName });
this._evaluateOnNewDocumentIdentifiers.push(identifier);
}
async _removeEvaluatesOnNewDocument(): Promise<void> {
const identifiers = this._evaluateOnNewDocumentIdentifiers;
this._evaluateOnNewDocumentIdentifiers = [];
await Promise.all(identifiers.map(identifier => this._client.send('Page.removeScriptToEvaluateOnNewDocument', { identifier })));
}
async _getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {

View File

@ -324,10 +324,18 @@ export class FFBrowserContext extends BrowserContext {
await this._browser._connection.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: this.initScripts.map(script => ({ script })) });
}
async doRemoveInitScripts() {
await this._browser._connection.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: [] });
}
async doExposeBinding(binding: PageBinding) {
await this._browser._connection.send('Browser.addBinding', { browserContextId: this._browserContextId, name: binding.name, script: binding.source });
}
async doRemoveExposedBindings() {
// TODO: implement me.
}
async doUpdateRequestInterception(): Promise<void> {
await this._browser._connection.send('Browser.setRequestInterception', { browserContextId: this._browserContextId, enabled: !!this._requestInterceptor });
}

View File

@ -333,6 +333,10 @@ export class FFPage implements PageDelegate {
await this._session.send('Page.addBinding', { name: binding.name, script: binding.source });
}
async removeExposedBindings() {
// TODO: implement me.
}
didClose() {
this._session.dispose();
eventsHelper.removeEventListeners(this._eventListeners);
@ -403,6 +407,11 @@ export class FFPage implements PageDelegate {
await this._session.send('Page.setInitScripts', { scripts: this._initScripts });
}
async removeInitScripts() {
this._initScripts = [];
await this._session.send('Page.setInitScripts', { scripts: [] });
}
async closePage(runBeforeUnload: boolean): Promise<void> {
await this._session.send('Page.close', { runBeforeUnload });
}

View File

@ -47,7 +47,9 @@ export interface PageDelegate {
goBack(): Promise<boolean>;
goForward(): Promise<boolean>;
exposeBinding(binding: PageBinding): Promise<void>;
removeExposedBindings(): Promise<void>;
addInitScript(source: string): Promise<void>;
removeInitScripts(): Promise<void>;
closePage(runBeforeUnload: boolean): Promise<void>;
potentiallyUninitializedPage(): Page;
pageOrError(): Promise<Page | Error>;
@ -308,6 +310,11 @@ export class Page extends SdkObject {
await this._delegate.exposeBinding(binding);
}
async removeExposedBindings() {
this._pageBindings.clear();
await this._delegate.removeExposedBindings();
}
setExtraHTTPHeaders(headers: types.HeadersArray) {
this._state.extraHTTPHeaders = headers;
return this._delegate.updateExtraHTTPHeaders();
@ -416,6 +423,11 @@ export class Page extends SdkObject {
await this._delegate.addInitScript(source);
}
async removeInitScripts() {
this.initScripts.splice(0, this.initScripts.length);
await this._delegate.removeInitScripts();
}
_needsRequestInterception(): boolean {
return !!this._clientRequestInterceptor || !!this._serverRequestInterceptor || !!this._browserContext._requestInterceptor;
}

View File

@ -307,11 +307,21 @@ export class WKBrowserContext extends BrowserContext {
await (page._delegate as WKPage)._updateBootstrapScript();
}
async doRemoveInitScripts() {
for (const page of this.pages())
await (page._delegate as WKPage)._updateBootstrapScript();
}
async doExposeBinding(binding: PageBinding) {
for (const page of this.pages())
await (page._delegate as WKPage).exposeBinding(binding);
}
async doRemoveExposedBindings() {
for (const page of this.pages())
await (page._delegate as WKPage).removeExposedBindings();
}
async doUpdateRequestInterception(): Promise<void> {
for (const page of this.pages())
await (page._delegate as WKPage).updateRequestInterception();

View File

@ -753,6 +753,10 @@ export class WKPage implements PageDelegate {
await this._evaluateBindingScript(binding);
}
async removeExposedBindings(): Promise<void> {
await this._updateBootstrapScript();
}
private async _evaluateBindingScript(binding: PageBinding): Promise<void> {
const script = this._bindingToScript(binding);
await Promise.all(this._page.frames().map(frame => frame.evaluateExpression(script, false, {}).catch(e => {})));
@ -762,6 +766,10 @@ export class WKPage implements PageDelegate {
await this._updateBootstrapScript();
}
async removeInitScripts() {
await this._updateBootstrapScript();
}
private _bindingToScript(binding: PageBinding): string {
return `self.${binding.name} = (param) => console.debug('${BINDING_CALL_MESSAGE}', {}, param); ${binding.source}`;
}

View File

@ -29,14 +29,12 @@ const test = baseTest.extend({
},
page: async ({ _workerPage }, use) => {
await _workerPage.goto('about:blank');
await use(_workerPage);
},
mount: async ({ page, baseURL }, use) => {
mount: async ({ page, baseURL, viewport }, use) => {
await use(async (component, options) => {
await page.goto(baseURL);
const selector = await mount(page, component, options);
const selector = await mount(page, component, options, baseURL, viewport);
return page.locator(selector);
});
},

View File

@ -29,14 +29,12 @@ const test = baseTest.extend({
},
page: async ({ _workerPage }, use) => {
await _workerPage.goto('about:blank');
await use(_workerPage);
},
mount: async ({ page, baseURL }, use) => {
mount: async ({ page, baseURL, viewport }, use) => {
await use(async (component, options) => {
await page.goto(baseURL);
const selector = await mount(page, component, options);
const selector = await mount(page, component, options, baseURL, viewport);
return page.locator(selector);
});
},

View File

@ -29,14 +29,12 @@ const test = baseTest.extend({
},
page: async ({ _workerPage }, use) => {
await _workerPage.goto('about:blank');
await use(_workerPage);
},
mount: async ({ page, baseURL }, use) => {
mount: async ({ page, baseURL, viewport }, use) => {
await use(async (component, options) => {
await page.goto(baseURL);
const selector = await mount(page, component, options);
const selector = await mount(page, component, options, baseURL, viewport);
return page.locator(selector);
});
},

View File

@ -14,10 +14,16 @@
* limitations under the License.
*/
import type { Page } from '@playwright/test';
import type { Page, ViewportSize } from '@playwright/test';
import { createGuid } from 'playwright-core/lib/utils/utils';
export async function mount(page: Page, jsxOrType: any, options: any): Promise<string> {
export async function mount(page: Page, jsxOrType: any, options: any, baseURL: string, viewport: ViewportSize): Promise<string> {
await page.goto('about:blank');
await (page as any)._resetForReuse();
await (page.context() as any)._resetForReuse();
await page.setViewportSize(viewport);
await page.goto(baseURL);
let component;
if (typeof jsxOrType === 'string')
component = { kind: 'object', type: jsxOrType, options };