From cab52cded9a23e15af8c0b3647754bd093f05e94 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 25 Jan 2023 14:11:53 -0800 Subject: [PATCH] chore: consolidate route handling logic in NetworkRouter (#20353) References #19607. --- .../src/client/browserContext.ts | 33 +++--------- .../playwright-core/src/client/harRouter.ts | 24 ++------- .../playwright-core/src/client/network.ts | 53 ++++++++++++++++++- packages/playwright-core/src/client/page.ts | 37 ++++--------- 4 files changed, 72 insertions(+), 75 deletions(-) diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index f39f74406a..a04bdcfe16 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -40,11 +40,10 @@ import { Artifact } from './artifact'; import { APIRequestContext } from './fetch'; import { createInstrumentation } from './clientInstrumentation'; import { rewriteErrorMessage } from '../utils/stackTrace'; -import { HarRouter } from './harRouter'; export class BrowserContext extends ChannelOwner implements api.BrowserContext { _pages = new Set(); - private _routes: network.RouteHandler[] = []; + private _router: network.NetworkRouter; readonly _browser: Browser | null = null; private _browserType: BrowserType | undefined; readonly _bindings = new Map any>(); @@ -73,6 +72,7 @@ export class BrowserContext extends ChannelOwner if (parent instanceof Browser) this._browser = parent; this._isChromium = this._browser?._name === 'chromium'; + this._router = new network.NetworkRouter(this, this._options.baseURL); this.tracing = Tracing.from(initializer.tracing); this.request = APIRequestContext.from(initializer.requestContext); @@ -153,18 +153,8 @@ export class BrowserContext extends ChannelOwner } async _onRoute(route: network.Route) { - const routeHandlers = this._routes.slice(); - for (const routeHandler of routeHandlers) { - if (!routeHandler.matches(route.request().url())) - continue; - if (routeHandler.willExpire()) - this._routes.splice(this._routes.indexOf(routeHandler), 1); - const handled = await routeHandler.handle(route); - if (!this._routes.length) - this._wrapApiCall(() => this._disableInterception(), true).catch(() => {}); - if (handled) - return; - } + if (await this._router.handleRoute(route)) + return; await route._innerContinue(true); } @@ -261,9 +251,7 @@ export class BrowserContext extends ChannelOwner } async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise { - this._routes.unshift(new network.RouteHandler(this._options.baseURL, url, handler, options.times)); - if (this._routes.length === 1) - await this._channel.setNetworkInterceptionEnabled({ enabled: true }); + await this._router.route(url, handler, options); } async _recordIntoHAR(har: string, page: Page | null, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean } = {}): Promise { @@ -284,18 +272,11 @@ export class BrowserContext extends ChannelOwner await this._recordIntoHAR(har, null, options); return; } - const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url }); - harRouter.addContextRoute(this); + await this._router.routeFromHAR(har, options); } async unroute(url: URLMatch, handler?: network.RouteHandlerCallback): Promise { - this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); - if (!this._routes.length) - await this._disableInterception(); - } - - private async _disableInterception() { - await this._channel.setNetworkInterceptionEnabled({ enabled: false }); + await this._router.unroute(url, handler); } async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise { diff --git a/packages/playwright-core/src/client/harRouter.ts b/packages/playwright-core/src/client/harRouter.ts index 5893b9000d..96e4e5896b 100644 --- a/packages/playwright-core/src/client/harRouter.ts +++ b/packages/playwright-core/src/client/harRouter.ts @@ -15,12 +15,8 @@ */ import { debugLogger } from '../common/debugLogger'; -import type { BrowserContext } from './browserContext'; -import { Events } from './events'; import type { LocalUtils } from './localUtils'; import type { Route } from './network'; -import type { URLMatch } from './types'; -import type { Page } from './page'; type HarNotFoundAction = 'abort' | 'fallback'; @@ -28,23 +24,21 @@ export class HarRouter { private _localUtils: LocalUtils; private _harId: string; private _notFoundAction: HarNotFoundAction; - private _options: { urlMatch?: URLMatch; baseURL?: string; }; - static async create(localUtils: LocalUtils, file: string, notFoundAction: HarNotFoundAction, options: { urlMatch?: URLMatch }): Promise { + static async create(localUtils: LocalUtils, file: string, notFoundAction: HarNotFoundAction): Promise { const { harId, error } = await localUtils._channel.harOpen({ file }); if (error) throw new Error(error); - return new HarRouter(localUtils, harId!, notFoundAction, options); + return new HarRouter(localUtils, harId!, notFoundAction); } - constructor(localUtils: LocalUtils, harId: string, notFoundAction: HarNotFoundAction, options: { urlMatch?: URLMatch }) { + private constructor(localUtils: LocalUtils, harId: string, notFoundAction: HarNotFoundAction) { this._localUtils = localUtils; this._harId = harId; - this._options = options; this._notFoundAction = notFoundAction; } - private async _handle(route: Route) { + async handleRoute(route: Route) { const request = route.request(); const response = await this._localUtils._channel.harLookup({ @@ -83,16 +77,6 @@ export class HarRouter { await route.fallback(); } - async addContextRoute(context: BrowserContext) { - await context.route(this._options.urlMatch || '**/*', route => this._handle(route)); - context.once(Events.BrowserContext.Close, () => this.dispose()); - } - - async addPageRoute(page: Page) { - await page.route(this._options.urlMatch || '**/*', route => this._handle(route)); - page.once(Events.Page.Close, () => this.dispose()); - } - dispose() { this._localUtils._channel.harClose({ harId: this._harId }).catch(() => {}); } diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 77d89ecddd..a0b4e8df4e 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -33,6 +33,8 @@ import { urlMatches } from '../utils/network'; import { MultiMap } from '../utils/multimap'; import { APIResponse } from './fetch'; import type { Serializable } from '../../types/structs'; +import type { BrowserContext } from './browserContext'; +import { HarRouter } from './harRouter'; export type NetworkCookie = { name: string, @@ -617,7 +619,56 @@ export function validateHeaders(headers: Headers) { } } -export class RouteHandler { +export class NetworkRouter { + private _owner: Page | BrowserContext; + private _baseURL: string | undefined; + private _routes: RouteHandler[] = []; + + constructor(owner: Page | BrowserContext, baseURL: string | undefined) { + this._owner = owner; + this._baseURL = baseURL; + } + + async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise { + this._routes.unshift(new RouteHandler(this._baseURL, url, handler, options.times)); + if (this._routes.length === 1) + await this._owner._channel.setNetworkInterceptionEnabled({ enabled: true }); + } + + async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback' } = {}): Promise { + const harRouter = await HarRouter.create(this._owner._connection.localUtils(), har, options.notFound || 'abort'); + await this.route(options.url || '**/*', route => harRouter.handleRoute(route)); + this._owner.once('close', () => harRouter.dispose()); + } + + async unroute(url: URLMatch, handler?: RouteHandlerCallback): Promise { + this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); + if (!this._routes.length) + await this._disableInterception(); + } + + async handleRoute(route: Route) { + const routeHandlers = this._routes.slice(); + for (const routeHandler of routeHandlers) { + if (!routeHandler.matches(route.request().url())) + continue; + if (routeHandler.willExpire()) + this._routes.splice(this._routes.indexOf(routeHandler), 1); + const handled = await routeHandler.handle(route); + if (!this._routes.length) + this._owner._wrapApiCall(() => this._disableInterception(), true).catch(() => {}); + if (handled) + return true; + } + return false; + } + + private async _disableInterception() { + await this._owner._channel.setNetworkInterceptionEnabled({ enabled: false }); + } +} + +class RouteHandler { private handledCount = 0; private readonly _baseURL: string | undefined; private readonly _times: number; diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index de57040960..ca89d7ae1e 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -42,13 +42,12 @@ import type { APIRequestContext } from './fetch'; import { FileChooser } from './fileChooser'; import type { WaitForNavigationOptions } from './frame'; import { Frame, verifyLoadState } from './frame'; -import { HarRouter } from './harRouter'; import { Keyboard, Mouse, Touchscreen } from './input'; import { assertMaxArguments, JSHandle, parseResult, serializeArgument } from './jsHandle'; import type { FrameLocator, Locator, LocatorOptions } from './locator'; import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils'; -import type { RouteHandlerCallback } from './network'; -import { Response, Route, RouteHandler, validateHeaders, WebSocket } from './network'; +import { NetworkRouter, type RouteHandlerCallback } from './network'; +import { Response, Route, validateHeaders, WebSocket } from './network'; import type { Request } from './network'; import type { FilePayload, Headers, LifecycleEvent, SelectOption, SelectOptionOptions, Size, URLMatch, WaitForEventOptions, WaitForFunctionOptions } from './types'; import { Video } from './video'; @@ -85,7 +84,7 @@ export class Page extends ChannelOwner implements api.Page private _closed = false; _closedOrCrashedPromise: Promise; private _viewportSize: Size | null; - private _routes: RouteHandler[] = []; + private _router: NetworkRouter; readonly accessibility: Accessibility; readonly coverage: Coverage; @@ -111,6 +110,7 @@ export class Page extends ChannelOwner implements api.Page super(parent, type, guid, initializer); this._browserContext = parent as unknown as BrowserContext; this._timeoutSettings = new TimeoutSettings(this._browserContext._timeoutSettings); + this._router = new NetworkRouter(this, this._browserContext._options.baseURL); this.accessibility = new Accessibility(this._channel); this.keyboard = new Keyboard(this); @@ -187,18 +187,8 @@ export class Page extends ChannelOwner implements api.Page } private async _onRoute(route: Route) { - const routeHandlers = this._routes.slice(); - for (const routeHandler of routeHandlers) { - if (!routeHandler.matches(route.request().url())) - continue; - if (routeHandler.willExpire()) - this._routes.splice(this._routes.indexOf(routeHandler), 1); - const handled = await routeHandler.handle(route); - if (!this._routes.length) - this._wrapApiCall(() => this._disableInterception(), true).catch(() => {}); - if (handled) - return; - } + if (await this._router.handleRoute(route)) + return; await this._browserContext._onRoute(route); } @@ -459,9 +449,7 @@ export class Page extends ChannelOwner implements api.Page } async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise { - this._routes.unshift(new RouteHandler(this._browserContext._options.baseURL, url, handler, options.times)); - if (this._routes.length === 1) - await this._channel.setNetworkInterceptionEnabled({ enabled: true }); + await this._router.route(url, handler, options); } async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean } = {}): Promise { @@ -469,18 +457,11 @@ export class Page extends ChannelOwner implements api.Page await this._browserContext._recordIntoHAR(har, this, options); return; } - const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url }); - harRouter.addPageRoute(this); + await this._router.routeFromHAR(har, options); } async unroute(url: URLMatch, handler?: RouteHandlerCallback): Promise { - this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); - if (!this._routes.length) - await this._disableInterception(); - } - - private async _disableInterception() { - await this._channel.setNetworkInterceptionEnabled({ enabled: false }); + await this._router.unroute(url, handler); } async screenshot(options: Omit & { path?: string, mask?: Locator[] } = {}): Promise {