diff --git a/docs/src/mock.md b/docs/src/mock.md index bbb38564af..3e1c3e3c32 100644 --- a/docs/src/mock.md +++ b/docs/src/mock.md @@ -642,6 +642,8 @@ Known Limitations: 1. The mocking proxy is experimental and subject to change. 2. The injected `x-playwright-proxy` header affects CORS and might turn [simple requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests) into requests that require a preflight. 3. Requests on the server that were not made in response to a browser request, like those triggered by CRON job, won't be routed because they don't have access to the `x-playwright-proxy` header. +4. On Firefox, the first requests after page open might not be intercepted by the mocking proxy. +5. `defaultContextOptions` aren't applied when using `route.fetch`. ::: diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 06672cc64e..78074b9612 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -27,6 +27,7 @@ import { assert, headersObjectToArray, monotonicTime } from '../utils'; import type * as api from '../../types/types'; import { raceAgainstDeadline } from '../utils/timeoutRunner'; import type { Playwright } from './playwright'; +import type { Page } from './page'; export interface BrowserServerLauncher { launchServer(options?: LaunchServerOptions): Promise; @@ -241,6 +242,14 @@ export class BrowserType extends ChannelOwner imple context.setDefaultTimeout(this._defaultContextTimeout); if (this._defaultContextNavigationTimeout !== undefined) context.setDefaultNavigationTimeout(this._defaultContextNavigationTimeout); + + if (this._playwright._mockingProxy) { + context.on(Events.BrowserContext.Page, (page: Page) => { + // TODO: funnel through protocol, so these headers are known to the server browsercontext and can be applied earlier + page.setExtraHTTPHeaders(this._playwright._mockingProxy!.instrumentationHeaders(page)); + }); + } + await this._instrumentation.runAfterCreateBrowserContext(context); } diff --git a/packages/playwright-core/src/client/mockingProxy.ts b/packages/playwright-core/src/client/mockingProxy.ts index 898c620112..f49b09ba22 100644 --- a/packages/playwright-core/src/client/mockingProxy.ts +++ b/packages/playwright-core/src/client/mockingProxy.ts @@ -16,27 +16,27 @@ import * as network from './network'; import type * as channels from '@protocol/channels'; import { ChannelOwner } from './channelOwner'; -import { APIRequestContext } from './fetch'; +import type { APIRequestContext } from './fetch'; import { assert } from '../utils'; import type { Page } from './page'; -import { Events } from './events'; +import type { Playwright } from './playwright'; export class MockingProxy extends ChannelOwner { - private _pages = new Map(); + _requestContext!: APIRequestContext; + _playwright!: Playwright; constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.MockingProxyInitializer) { super(parent, type, guid, initializer); - const requestContext = APIRequestContext.from(initializer.requestContext); this._channel.on('route', async (params: channels.MockingProxyRouteEvent) => { const route = network.Route.from(params.route); - route._apiRequestContext = requestContext; + route._apiRequestContext = this._requestContext; const page = route.request()._pageForMockingProxy!; await page._onRoute(route); }); this._channel.on('request', async (params: channels.MockingProxyRequestEvent) => { - const page = this._pages.get(params.correlation); + const page = this.findPage(params.correlation); assert(page); const request = network.Request.from(params.request); request._pageForMockingProxy = page; @@ -68,16 +68,23 @@ export class MockingProxy extends ChannelOwner { return (channel as any)._object; } - async instrumentPage(page: Page) { - const correlation = page._guid.split('@')[1]; - this._pages.set(correlation, page); - page.on(Events.Page.Close, () => { - this._pages.delete(correlation); - }); - const proxyUrl = `http://localhost:${this._initializer.port}/pw_meta:${correlation}/`; - await page.setExtraHTTPHeaders({ - 'x-playwright-proxy': encodeURIComponent(proxyUrl) - }); + findPage(correlation: string): Page | undefined { + const guid = `Page@${correlation}`; + // TODO: move this as list onto Playwright directly + for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit]) { + for (const context of browserType._contexts) { + for (const page of context._pages) { + if (page._guid === guid) + return page; + } + } + } } + instrumentationHeaders(page: Page) { + const correlation = page._guid.substring('Page@'.length); + return { + 'x-playwright-proxy': `${this._initializer.baseURL}/pw_meta:${correlation}/`, + }; + } } diff --git a/packages/playwright-core/src/client/playwright.ts b/packages/playwright-core/src/client/playwright.ts index c3eb5bfa27..aa47db853d 100644 --- a/packages/playwright-core/src/client/playwright.ts +++ b/packages/playwright-core/src/client/playwright.ts @@ -36,6 +36,7 @@ export class Playwright extends ChannelOwner { selectors: Selectors; readonly request: APIRequest; readonly errors: { TimeoutError: typeof TimeoutError }; + _mockingProxy?: MockingProxy; constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PlaywrightInitializer) { super(parent, type, guid, initializer); @@ -77,7 +78,10 @@ export class Playwright extends ChannelOwner { async _startMockingProxy() { const requestContext = await this.request._newContext(undefined, this._connection.localUtils()._channel); - const { mockingProxy } = await this._connection.localUtils()._channel.newMockingProxy({ requestContext: requestContext._channel }); - return MockingProxy.from(mockingProxy); + const result = await this._connection.localUtils()._channel.newMockingProxy({ requestContext: requestContext._channel }); + this._mockingProxy = MockingProxy.from(result.mockingProxy); + this._mockingProxy._requestContext = requestContext; + this._mockingProxy._playwright = this; + return this._mockingProxy; } } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index bd332d7d89..a14c442c64 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -377,8 +377,7 @@ scheme.LocalUtilsNewMockingProxyResult = tObject({ mockingProxy: tChannel(['MockingProxy']), }); scheme.MockingProxyInitializer = tObject({ - port: tNumber, - requestContext: tChannel(['APIRequestContext']), + baseURL: tString, }); scheme.MockingProxyRouteEvent = tObject({ route: tChannel(['Route']), diff --git a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts index e83dc9ab77..ecfc43bd3c 100644 --- a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts @@ -289,7 +289,7 @@ export class LocalUtilsDispatcher extends Dispatcher implements channels.MockingProxyChannel { +export class MockingProxyDispatcher extends Dispatcher implements channels.MockingProxyChannel { _type_MockingProxy = true; _type_EventTarget = true; - static from(scope: RootDispatcher, mockingProxy: MockingProxy): MockingProxyDispatcher { - return existingDispatcher(mockingProxy) || new MockingProxyDispatcher(scope, mockingProxy); - } - - private constructor(scope: RootDispatcher, mockingProxy: MockingProxy) { + constructor(scope: LocalUtilsDispatcher, mockingProxy: MockingProxy) { super(scope, mockingProxy, 'MockingProxy', { - port: mockingProxy.port(), - requestContext: APIRequestContextDispatcher.from(scope, mockingProxy.fetchRequest), + baseURL: mockingProxy.baseURL(), }); - this.addObjectListener(MockingProxy.Events.Route, (route: Route) => { + mockingProxy.onRoute = async route => { const requestDispatcher = RequestDispatcher.from(this, route.request()); this._dispatchEvent('route', { route: RouteDispatcher.from(requestDispatcher, route) }); - }); + }; this.addObjectListener(MockingProxy.Events.Request, ({ request, correlation }: { request: Request, correlation: string }) => { this._dispatchEvent('request', { request: RequestDispatcher.from(this, request), correlation }); }); diff --git a/packages/playwright-core/src/server/mockingProxy.ts b/packages/playwright-core/src/server/mockingProxy.ts index fcd0c9cb2c..edd48e1b8a 100644 --- a/packages/playwright-core/src/server/mockingProxy.ts +++ b/packages/playwright-core/src/server/mockingProxy.ts @@ -32,13 +32,13 @@ export class MockingProxy extends SdkObject implements RequestContext { static Events = { Request: 'request', Response: 'response', - Route: 'route', RequestFailed: 'requestfailed', RequestFinished: 'requestfinished', }; fetchRequest: APIRequestContext; private _httpServer = new WorkerHttpServer(); + onRoute = (route: Route) => route.continue({ isFallback: true }); constructor(parent: SdkObject, requestContext: APIRequestContext) { super(parent, 'MockingProxy'); @@ -66,6 +66,10 @@ export class MockingProxy extends SdkObject implements RequestContext { return this._httpServer.port(); } + baseURL() { + return `http://localhost:${this.port()}/`; + } + private async _proxy(req: http.IncomingMessage, res: http.ServerResponse) { if (req.url?.startsWith('/')) req.url = req.url.substring(1); @@ -211,7 +215,7 @@ export class MockingProxy extends SdkObject implements RequestContext { }, }); - this.emit(MockingProxy.Events.Route, route); + await this.onRoute(route); } addRouteInFlight(route: Route): void { diff --git a/packages/playwright-core/src/server/network.ts b/packages/playwright-core/src/server/network.ts index a6738ee0e7..5ee6c48438 100644 --- a/packages/playwright-core/src/server/network.ts +++ b/packages/playwright-core/src/server/network.ts @@ -88,7 +88,7 @@ export function stripFragmentFromUrl(url: string): string { } export interface RequestContext extends SdkObject { - fetchRequest: APIRequestContext; + readonly fetchRequest: APIRequestContext; addRouteInFlight(route: Route): void; removeRouteInFlight(route: Route): void; diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index bc6cab3736..750709543d 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -647,8 +647,7 @@ export interface LocalUtilsEvents { // ----------- MockingProxy ----------- export type MockingProxyInitializer = { - port: number, - requestContext: APIRequestContextChannel, + baseURL: string, }; export interface MockingProxyEventTarget { on(event: 'route', callback: (params: MockingProxyRouteEvent) => void): this; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 6737d27ea5..2768d4cba5 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -737,8 +737,7 @@ MockingProxy: extends: EventTarget initializer: - port: number - requestContext: APIRequestContext + baseURL: string events: route: