diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 9bd0c3d666..5d9857d61a 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -70,7 +70,7 @@ export abstract class BrowserContext extends SdkObject { readonly _pageBindings = new Map(); readonly _activeProgressControllers = new Set(); readonly _options: types.BrowserContextOptions; - _requestInterceptor?: network.RouteHandler; + readonly requestInterceptors: network.RouteHandler[] = []; private _isPersistentContext: boolean; private _closedStatus: 'open' | 'closing' | 'closed' = 'open'; readonly _closePromise: Promise; @@ -200,8 +200,6 @@ export abstract class BrowserContext extends SdkObject { this.selectors().setTestIdAttributeName(params.testIdAttributeName); } - await this._cancelAllRoutesInFlight(); - // Close extra pages early. let page: Page | undefined = this.pages()[0]; const [, ...otherPages] = this.pages(); @@ -439,8 +437,17 @@ export abstract class BrowserContext extends SdkObject { await this.doRemoveInitScripts(initScripts); } - async setRequestInterceptor(handler: network.RouteHandler | undefined): Promise { - this._requestInterceptor = handler; + async addRequestInterceptor(handler: network.RouteHandler): Promise { + this.requestInterceptors.push(handler); + await this.doUpdateRequestInterception(); + } + + async removeRequestInterceptor(handler: network.RouteHandler): Promise { + const index = this.requestInterceptors.indexOf(handler); + if (index === -1) + return; + this.requestInterceptors.splice(index, 1); + await this.notifyRoutesInFlightAboutRemovedHandler(handler); await this.doUpdateRequestInterception(); } @@ -547,10 +554,9 @@ export abstract class BrowserContext extends SdkObject { if (originsToSave.size) { const internalMetadata = serverSideCallMetadata(); const page = await this.newPage(internalMetadata); - await page.setServerRequestInterceptor(handler => { - handler.fulfill({ body: '' }).catch(() => {}); - return true; - }); + page.addRequestInterceptor(route => { + route.fulfill({ body: '' }).catch(() => {}); + }, 'prepend'); for (const origin of originsToSave) { const frame = page.mainFrame(); await frame.goto(internalMetadata, origin, { timeout: 0 }); @@ -577,10 +583,10 @@ export abstract class BrowserContext extends SdkObject { // as a user-visible page. isServerSide: false, }); - await page.setServerRequestInterceptor(handler => { - handler.fulfill({ body: '' }).catch(() => {}); - return true; - }); + const interceptor = (route: network.Route) => { + route.fulfill({ body: '' }).catch(() => {}); + }; + await page.addRequestInterceptor(interceptor, 'prepend'); for (const origin of new Set([...oldOrigins, ...newOrigins.keys()])) { const frame = page.mainFrame(); @@ -588,7 +594,7 @@ export abstract class BrowserContext extends SdkObject { await frame.resetStorageForCurrentOriginBestEffort(newOrigins.get(origin)); } - await page.setServerRequestInterceptor(undefined); + await page.removeRequestInterceptor(interceptor); this._origins = new Set([...newOrigins.keys()]); // It is safe to not restore the URL to about:blank since we are doing it in Page::resetForReuse. @@ -612,10 +618,9 @@ export abstract class BrowserContext extends SdkObject { if (state.origins && state.origins.length) { const internalMetadata = serverSideCallMetadata(); const page = await this.newPage(internalMetadata); - await page.setServerRequestInterceptor(handler => { - handler.fulfill({ body: '' }).catch(() => {}); - return true; - }); + await page.addRequestInterceptor(route => { + route.fulfill({ body: '' }).catch(() => {}); + }, 'prepend'); for (const originState of state.origins) { const frame = page.mainFrame(); await frame.goto(metadata, originState.origin, { timeout: 0 }); @@ -667,9 +672,8 @@ export abstract class BrowserContext extends SdkObject { this._routesInFlight.delete(route); } - async _cancelAllRoutesInFlight() { - await Promise.all([...this._routesInFlight].map(r => r.abort())).catch(() => {}); - this._routesInFlight.clear(); + async notifyRoutesInFlightAboutRemovedHandler(handler: network.RouteHandler): Promise { + await Promise.all([...this._routesInFlight].map(route => route.removeHandler(handler))); } } diff --git a/packages/playwright-core/src/server/chromium/crServiceWorker.ts b/packages/playwright-core/src/server/chromium/crServiceWorker.ts index baa8c15271..23099867c5 100644 --- a/packages/playwright-core/src/server/chromium/crServiceWorker.ts +++ b/packages/playwright-core/src/server/chromium/crServiceWorker.ts @@ -84,7 +84,7 @@ export class CRServiceWorker extends Worker { } needsRequestInterception(): boolean { - return this._isNetworkInspectionEnabled() && !!this.browserContext._requestInterceptor; + return this._isNetworkInspectionEnabled() && this.browserContext.requestInterceptors.length > 0; } reportRequestFinished(request: network.Request, response: network.Response | null) { @@ -101,12 +101,8 @@ export class CRServiceWorker extends Worker { requestStarted(request: network.Request, route?: network.RouteDelegate) { this.browserContext.emit(BrowserContext.Events.Request, request); - if (route) { - const r = new network.Route(request, route); - if (this.browserContext._requestInterceptor?.(r, request)) - return; - r.continue({ isFallback: true }).catch(() => {}); - } + if (route) + new network.Route(request, route).handle(this.browserContext.requestInterceptors); } private _isNetworkInspectionEnabled(): boolean { diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 1fcde59bb3..1a02de9a07 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -39,7 +39,7 @@ import type { Artifact } from '../artifact'; import type { ConsoleMessage } from '../console'; import type { Dialog } from '../dialog'; import type { CallMetadata } from '../instrumentation'; -import type { Request, Response } from '../network'; +import type { Request, Response, RouteHandler } from '../network'; import type { InitScript, Page, PageBinding } from '../page'; import type { DispatcherScope } from './dispatcher'; import type { FrameDispatcher } from './frameDispatcher'; @@ -55,6 +55,8 @@ export class BrowserContextDispatcher extends Dispatcher boolean; private _clockPaused = false; + private _requestInterceptor: RouteHandler; + private _interceptionUrlMatchers: (string | RegExp)[] = []; static from(parentScope: DispatcherScope, context: BrowserContext): BrowserContextDispatcher { const result = parentScope.connection.existingDispatcher(context); @@ -76,9 +78,21 @@ export class BrowserContextDispatcher extends Dispatcher { + const matchesSome = this._interceptionUrlMatchers.some(urlMatch => urlMatches(this._context._options.baseURL, request.url(), urlMatch)); + // If there is already a dispatcher, that means we've already routed this request through page. + // Client expects a single `route` event, either on the page or on the context, so we can just fallback here. + const routeDispatcher = this.connection.existingDispatcher(route); + if (!matchesSome || routeDispatcher) { + route.continue({ isFallback: true }).catch(() => {}); + return; + } + this._dispatchEvent('route', { route: new RouteDispatcher(RequestDispatcher.from(this, request), route) }); + }; + this._context = context; - // Note: when launching persistent context, dispatcher is created very late, - // so we can already have pages, videos and everything else. + // Note: when launching persistent context, or connecting to an existing browser, + // dispatcher is created very late, so we can already have pages, videos and everything else. const onVideo = (artifact: Artifact) => { // Note: Video must outlive Page and BrowserContext, so that client can saveAs it @@ -276,18 +290,18 @@ export class BrowserContextDispatcher extends Dispatcher { + const hadMatchers = this._interceptionUrlMatchers.length > 0; if (!params.patterns.length) { - await this._context.setRequestInterceptor(undefined); - return; + // Note: it is important to remove the interceptor when there are no patterns, + // because that disables the slow-path interception in the browser itself. + if (hadMatchers) + await this._context.removeRequestInterceptor(this._requestInterceptor); + this._interceptionUrlMatchers = []; + } else { + this._interceptionUrlMatchers = params.patterns.map(pattern => pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags!) : pattern.glob!); + if (!hadMatchers) + await this._context.addRequestInterceptor(this._requestInterceptor); } - const urlMatchers = params.patterns.map(pattern => pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags!) : pattern.glob!); - await this._context.setRequestInterceptor((route, request) => { - const matchesSome = urlMatchers.some(urlMatch => urlMatches(this._context._options.baseURL, request.url(), urlMatch)); - if (!matchesSome) - return false; - this._dispatchEvent('route', { route: RouteDispatcher.from(RequestDispatcher.from(this, request), route) }); - return true; - }); } async setWebSocketInterceptionPatterns(params: channels.PageSetWebSocketInterceptionPatternsParams, metadata: CallMetadata): Promise { @@ -383,8 +397,11 @@ export class BrowserContextDispatcher extends Dispatcher {}); + this._interceptionUrlMatchers = []; + this._context.removeRequestInterceptor(this._requestInterceptor).catch(() => {}); this._context.removeExposedBindings(this._bindings).catch(() => {}); this._bindings = []; this._context.removeInitScripts(this._initScritps).catch(() => {}); diff --git a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts index 0c28fa688b..205ff2d71e 100644 --- a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts +++ b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts @@ -123,19 +123,23 @@ export class ResponseDispatcher extends Dispatcher implements channels.RouteChannel { _type_Route = true; - static from(scope: RequestDispatcher, route: Route): RouteDispatcher { - const result = scope.connection.existingDispatcher(route); - return result || new RouteDispatcher(scope, route); - } + private _handled = false; - private constructor(scope: RequestDispatcher, route: Route) { + constructor(scope: RequestDispatcher, route: Route) { super(scope, route, 'Route', { // Context route can point to a non-reported request, so we send the request in the initializer. request: scope }); } + private _checkNotHandled() { + if (this._handled) + throw new Error('Route is already handled!'); + this._handled = true; + } + async continue(params: channels.RouteContinueParams, metadata: CallMetadata): Promise { + this._checkNotHandled(); await this._object.continue({ url: params.url, method: params.method, @@ -146,14 +150,17 @@ export class RouteDispatcher extends Dispatcher { + this._checkNotHandled(); await this._object.fulfill(params); } async abort(params: channels.RouteAbortParams, metadata: CallMetadata): Promise { + this._checkNotHandled(); await this._object.abort(params.errorCode || 'failed'); } async redirectNavigationRequest(params: channels.RouteRedirectNavigationRequestParams): Promise { + this._checkNotHandled(); await this._object.redirectNavigationRequest(params.url); } } diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 950f327730..8212696777 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -37,6 +37,7 @@ import type { CallMetadata } from '../instrumentation'; import type { JSHandle } from '../javascript'; import type { BrowserContextDispatcher } from './browserContextDispatcher'; import type { Frame } from '../frames'; +import type { RouteHandler } from '../network'; import type { InitScript, PageBinding } from '../page'; import type * as channels from '@protocol/channels'; @@ -48,6 +49,8 @@ export class PageDispatcher extends Dispatcher(); private _jsCoverageActive = false; private _cssCoverageActive = false; @@ -80,6 +83,15 @@ export class PageDispatcher extends Dispatcher { + const matchesSome = this._interceptionUrlMatchers.some(urlMatch => urlMatches(this._page.browserContext._options.baseURL, request.url(), urlMatch)); + if (!matchesSome) { + route.continue({ isFallback: true }).catch(() => {}); + return; + } + this._dispatchEvent('route', { route: new RouteDispatcher(RequestDispatcher.from(this.parentScope(), request), route) }); + }; + this.addObjectListener(Page.Events.Close, () => { this._dispatchEvent('close'); this._dispose(); @@ -179,18 +191,18 @@ export class PageDispatcher extends Dispatcher { + const hadMatchers = this._interceptionUrlMatchers.length > 0; if (!params.patterns.length) { - await this._page.setClientRequestInterceptor(undefined); - return; + // Note: it is important to remove the interceptor when there are no patterns, + // because that disables the slow-path interception in the browser itself. + if (hadMatchers) + await this._page.removeRequestInterceptor(this._requestInterceptor); + this._interceptionUrlMatchers = []; + } else { + this._interceptionUrlMatchers = params.patterns.map(pattern => pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags!) : pattern.glob!); + if (!hadMatchers) + await this._page.addRequestInterceptor(this._requestInterceptor); } - const urlMatchers = params.patterns.map(pattern => pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags!) : pattern.glob!); - await this._page.setClientRequestInterceptor((route, request) => { - const matchesSome = urlMatchers.some(urlMatch => urlMatches(this._page.browserContext._options.baseURL, request.url(), urlMatch)); - if (!matchesSome) - return false; - this._dispatchEvent('route', { route: RouteDispatcher.from(RequestDispatcher.from(this.parentScope(), request), route) }); - return true; - }); } async setWebSocketInterceptionPatterns(params: channels.PageSetWebSocketInterceptionPatternsParams, metadata: CallMetadata): Promise { @@ -343,7 +355,10 @@ export class PageDispatcher extends Dispatcher {}); + + // Cleanup properly and leave the page in a good state. Other clients may still connect and use it. + this._interceptionUrlMatchers = []; + this._page.removeRequestInterceptor(this._requestInterceptor).catch(() => {}); this._page.removeExposedBindings(this._bindings).catch(() => {}); this._bindings = []; this._page.removeInitScripts(this._initScripts).catch(() => {}); diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index a62442e8f1..6acfa8aa4e 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -387,8 +387,8 @@ export class FFBrowserContext extends BrowserContext { async doUpdateRequestInterception(): Promise { await Promise.all([ - this._browser.session.send('Browser.setRequestInterception', { browserContextId: this._browserContextId, enabled: !!this._requestInterceptor }), - this._browser.session.send('Browser.setCacheDisabled', { browserContextId: this._browserContextId, cacheDisabled: !!this._requestInterceptor }), + this._browser.session.send('Browser.setRequestInterception', { browserContextId: this._browserContextId, enabled: this.requestInterceptors.length > 0 }), + this._browser.session.send('Browser.setCacheDisabled', { browserContextId: this._browserContextId, cacheDisabled: this.requestInterceptors.length > 0 }), ]); } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index e5d71f8bc1..c6f4791410 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -301,16 +301,8 @@ export class FrameManager { return; } this._page.emitOnContext(BrowserContext.Events.Request, request); - if (route) { - const r = new network.Route(request, route); - if (this._page.serverRequestInterceptor?.(r, request)) - return; - if (this._page.clientRequestInterceptor?.(r, request)) - return; - if (this._page.browserContext._requestInterceptor?.(r, request)) - return; - r.continue({ isFallback: true }).catch(() => {}); - } + if (route) + new network.Route(request, route).handle([...this._page.requestInterceptors, ...this._page.browserContext.requestInterceptors]); } requestReceivedResponse(response: network.Response) { diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index f2421916e2..dc70ad2079 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -292,7 +292,7 @@ export class HarTracer { } private _recordRequestOverrides(harEntry: har.Entry, request: network.Request) { - if (!request._hasOverrides() || !this._options.recordRequestOverrides) + if (!request.overrides() || !this._options.recordRequestOverrides) return; harEntry.request.method = request.method(); harEntry.request.url = request.url(); diff --git a/packages/playwright-core/src/server/network.ts b/packages/playwright-core/src/server/network.ts index edf9053158..d00c8ca743 100644 --- a/packages/playwright-core/src/server/network.ts +++ b/packages/playwright-core/src/server/network.ts @@ -137,9 +137,10 @@ export class Request extends SdkObject { this._waitForResponsePromise.resolve(null); } - _setOverrides(overrides: types.NormalizedContinueOverrides) { - this._overrides = overrides; + _applyOverrides(overrides: types.NormalizedContinueOverrides) { + this._overrides = { ...this._overrides, ...overrides }; this._updateHeadersMap(); + return this._overrides; } private _updateHeadersMap() { @@ -147,8 +148,8 @@ export class Request extends SdkObject { this._headersMap.set(name.toLowerCase(), value); } - _hasOverrides() { - return !!this._overrides; + overrides() { + return this._overrides; } url(): string { @@ -251,6 +252,8 @@ export class Route extends SdkObject { private readonly _request: Request; private readonly _delegate: RouteDelegate; private _handled = false; + private _currentHandler: RouteHandler | undefined; + private _futureHandlers: RouteHandler[] = []; constructor(request: Request, delegate: RouteDelegate) { super(request._frame || request._context, 'route'); @@ -259,6 +262,19 @@ export class Route extends SdkObject { this._request._context.addRouteInFlight(this); } + handle(handlers: RouteHandler[]) { + this._futureHandlers = [...handlers]; + this.continue({ isFallback: true }).catch(() => {}); + } + + async removeHandler(handler: RouteHandler) { + this._futureHandlers = this._futureHandlers.filter(h => h !== handler); + if (handler === this._currentHandler) { + await this.continue({ isFallback: true }).catch(() => {}); + return; + } + } + request(): Request { return this._request; } @@ -274,6 +290,7 @@ export class Route extends SdkObject { this._startHandling(); assert(this._request.isNavigationRequest()); this._request.frame()!.redirectNavigation(url, this._request._documentId!, this._request.headerValue('referer')); + this._endHandling(); } async fulfill(overrides: channels.RouteFulfillParams) { @@ -322,7 +339,6 @@ export class Route extends SdkObject { } async continue(overrides: types.NormalizedContinueOverrides) { - this._startHandling(); if (overrides.url) { const newUrl = new URL(overrides.url); const oldUrl = new URL(this._request.url()); @@ -331,9 +347,18 @@ export class Route extends SdkObject { } if (overrides.headers) overrides.headers = overrides.headers?.filter(header => header.name.toLowerCase() !== 'cookie'); - this._request._setOverrides(overrides); + overrides = this._request._applyOverrides(overrides); + + const nextHandler = this._futureHandlers.shift(); + if (nextHandler) { + this._currentHandler = nextHandler; + nextHandler(this, this._request); + return; + } + if (!overrides.isFallback) this._request._context.emit(BrowserContext.Events.RequestContinued, this._request); + this._startHandling(); await this._delegate.continue(overrides); this._endHandling(); } @@ -341,14 +366,17 @@ export class Route extends SdkObject { private _startHandling() { assert(!this._handled, 'Route is already handled!'); this._handled = true; + this._currentHandler = undefined; } private _endHandling() { + this._futureHandlers = []; + this._currentHandler = undefined; this._request._context.removeRouteInFlight(this); } } -export type RouteHandler = (route: Route, request: Request) => boolean; +export type RouteHandler = (route: Route, request: Request) => void; type GetResponseBodyCallback = () => Promise; diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 8716b63756..a6952796aa 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -160,8 +160,7 @@ export class Page extends SdkObject { private _workers = new Map(); readonly pdf: ((options: channels.PagePdfParams) => Promise) | undefined; readonly coverage: any; - clientRequestInterceptor: network.RouteHandler | undefined; - serverRequestInterceptor: network.RouteHandler | undefined; + readonly requestInterceptors: network.RouteHandler[] = []; video: Artifact | null = null; private _opener: Page | undefined; private _isServerSideOnly = false; @@ -258,8 +257,6 @@ export class Page extends SdkObject { async resetForReuse(metadata: CallMetadata) { this._locatorHandlers.clear(); - await this.setClientRequestInterceptor(undefined); - await this.setServerRequestInterceptor(undefined); // Re-navigate once init scripts are gone. // TODO: we should have a timeout for `resetForReuse`. await this.mainFrame().goto(metadata, 'about:blank', { timeout: 0 }); @@ -571,16 +568,23 @@ export class Page extends SdkObject { } needsRequestInterception(): boolean { - return !!this.clientRequestInterceptor || !!this.serverRequestInterceptor || !!this.browserContext._requestInterceptor; + return this.requestInterceptors.length > 0 || this.browserContext.requestInterceptors.length > 0; } - async setClientRequestInterceptor(handler: network.RouteHandler | undefined): Promise { - this.clientRequestInterceptor = handler; + async addRequestInterceptor(handler: network.RouteHandler, prepend?: 'prepend'): Promise { + if (prepend) + this.requestInterceptors.unshift(handler); + else + this.requestInterceptors.push(handler); await this.delegate.updateRequestInterception(); } - async setServerRequestInterceptor(handler: network.RouteHandler | undefined): Promise { - this.serverRequestInterceptor = handler; + async removeRequestInterceptor(handler: network.RouteHandler): Promise { + const index = this.requestInterceptors.indexOf(handler); + if (index === -1) + return; + this.requestInterceptors.splice(index, 1); + await this.browserContext.notifyRoutesInFlightAboutRemovedHandler(handler); await this.delegate.updateRequestInterception(); } diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index 4276951f33..ca4eb042d8 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -63,9 +63,11 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { private async _init() { await syncLocalStorageWithSettings(this._page, 'recorder'); - await this._page.setServerRequestInterceptor(route => { - if (!route.request().url().startsWith('https://playwright/')) - return false; + await this._page.addRequestInterceptor(route => { + if (!route.request().url().startsWith('https://playwright/')) { + route.continue({ isFallback: true }).catch(() => {}); + return; + } const uri = route.request().url().substring('https://playwright/'.length); const file = require.resolve('../../vite/recorder/' + uri); @@ -79,7 +81,6 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { isBase64: true }).catch(() => {}); }); - return true; }); await this._page.exposeBinding('dispatch', false, (_, data: any) => this.emit('event', data)); diff --git a/tests/library/multiclient.spec.ts b/tests/library/multiclient.spec.ts index 74c2154505..604ad54b39 100644 --- a/tests/library/multiclient.spec.ts +++ b/tests/library/multiclient.spec.ts @@ -50,7 +50,7 @@ const test = playwrightTest.extend({ }); test.slow(true, 'All connect tests are slow'); -test.skip(({ mode }) => mode.startsWith('service')); +test.skip(({ mode }) => mode !== 'default'); async function disconnect(page: Page) { await page.context().browser().close(); @@ -161,6 +161,74 @@ test('last emulateMedia wins', async ({ twoPages }) => { expect(await pageA.evaluate(() => window.matchMedia('print').matches)).toBe(false); }); +test('should chain routes', async ({ twoPages, server }) => { + const { pageA, pageB } = twoPages; + + server.setRoute('/foo', (req, res) => res.writeHead(200, { 'Content-Type': 'text/html' }).end('
server-foo
')); + server.setRoute('/bar', (req, res) => res.writeHead(200, { 'Content-Type': 'text/html' }).end('
server-bar
')); + server.setRoute('/qux', (req, res) => res.writeHead(200, { 'Content-Type': 'text/html' }).end('
server-qux
')); + + let stall = false; + let stallCallback; + const stallPromise = new Promise(f => stallCallback = f); + await pageA.route('**/foo', async route => { + if (stall) + stallCallback(); + else + await route.fallback(); + }); + await pageA.route('**/bar', async route => { + await route.fulfill({ body: '
intercepted-bar
', contentType: 'text/html' }); + }); + + await pageB.route('**/foo', async route => { + await route.fulfill({ body: '
intercepted2-foo
', contentType: 'text/html' }); + }); + await pageB.route('**/bar', async route => { + await route.fulfill({ body: '
intercepted2-bar
', contentType: 'text/html' }); + }); + await pageB.route('**/qux', async route => { + await route.fulfill({ body: '
intercepted2-qux
', contentType: 'text/html' }); + }); + + await pageA.goto(server.PREFIX + '/foo'); + await expect(pageB.locator('div')).toHaveText('intercepted2-foo'); + + await pageA.goto(server.PREFIX + '/bar'); + await expect(pageB.locator('div')).toHaveText('intercepted-bar'); + + await pageA.goto(server.PREFIX + '/qux'); + await expect(pageB.locator('div')).toHaveText('intercepted2-qux'); + + stall = true; + const gotoPromise = pageB.goto(server.PREFIX + '/foo'); + await stallPromise; + await pageA.context().browser().close(); + + await gotoPromise; + await expect(pageB.locator('div')).toHaveText('intercepted2-foo'); + + await pageB.goto(server.PREFIX + '/bar'); + await expect(pageB.locator('div')).toHaveText('intercepted2-bar'); +}); + +test.fixme('should chain routes with changed url', async ({ twoPages, server }) => { + const { pageA, pageB } = twoPages; + + server.setRoute('/foo', (req, res) => res.writeHead(200, { 'Content-Type': 'text/html' }).end('
server-foo
')); + server.setRoute('/baz', (req, res) => res.writeHead(200, { 'Content-Type': 'text/html' }).end('
server-baz
')); + + await pageA.route('**/foo', async route => { + await route.fallback({ url: server.PREFIX + '/baz' }); + }); + await pageB.route('**/baz', async route => { + await route.fulfill({ body: '
intercepted2-baz
', contentType: 'text/html' }); + }); + + await pageA.goto(server.PREFIX + '/foo'); + await expect(pageB.locator('div')).toHaveText('intercepted2-baz'); +}); + test('should remove exposed bindings upon disconnect', async ({ twoPages }) => { const { pageA, pageB } = twoPages;