diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index be47ddeb51..fd811dfa18 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -81,7 +81,7 @@ export class Browser extends ChannelOwner implements ap async _innerNewContext(options: BrowserContextOptions = {}, forReuse: boolean): Promise { options = { ...this._browserType._defaultContextOptions, ...options }; - const contextOptions = await prepareBrowserContextParams(options); + const contextOptions = await prepareBrowserContextParams(options, this._browserType); const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions); const context = BrowserContext.from(response.context); await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger); diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 71a5aca8ec..ee94a313bf 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -29,8 +29,7 @@ import { Events } from './events'; import { TimeoutSettings } from '../common/timeoutSettings'; import { Waiter } from './waiter'; import type { Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types'; -import type { RegisteredListener } from '../utils'; -import { type URLMatch, headersObjectToArray, isRegExp, isString, urlMatchesEqual, mkdirIfNeeded, eventsHelper } from '../utils'; +import { type URLMatch, headersObjectToArray, isRegExp, isString, urlMatchesEqual, mkdirIfNeeded } from '../utils'; import type * as api from '../../types/types'; import type * as structs from '../../types/structs'; import { CDPSession } from './cdpSession'; @@ -45,7 +44,6 @@ import { Dialog } from './dialog'; import { WebError } from './webError'; import { TargetClosedError, parseError } from './errors'; import { Clock } from './clock'; -import type { MockingProxy } from './mockingProxy'; export class BrowserContext extends ChannelOwner implements api.BrowserContext { _pages = new Set(); @@ -70,7 +68,6 @@ export class BrowserContext extends ChannelOwner _closeWasCalled = false; private _closeReason: string | undefined; private _harRouters: HarRouter[] = []; - _mockingProxy?: MockingProxy; static from(context: channels.BrowserContextChannel): BrowserContext { return (context as any)._object; @@ -169,7 +166,6 @@ export class BrowserContext extends ChannelOwner this.emit(Events.BrowserContext.Page, page); if (page._opener && !page._opener.isClosed()) page._opener.emit(Events.Page.Popup, page); - this._mockingProxy?.instrumentPage(page); } _onRequest(request: network.Request, page: Page | null) { @@ -241,12 +237,6 @@ export class BrowserContext extends ChannelOwner await bindingCall.call(func); } - async _subscribeToMockingProxy(mockingProxy: MockingProxy) { - if (this._mockingProxy) - throw new Error('Multiple mocking proxies are not supported'); - this._mockingProxy = mockingProxy; - } - setDefaultNavigationTimeout(timeout: number | undefined) { this._timeoutSettings.setDefaultNavigationTimeout(timeout); this._wrapApiCall(async () => { @@ -530,7 +520,7 @@ function prepareRecordHarOptions(options: BrowserContextOptions['recordHar']): c }; } -export async function prepareBrowserContextParams(options: BrowserContextOptions): Promise { +export async function prepareBrowserContextParams(options: BrowserContextOptions, type?: BrowserType): Promise { if (options.videoSize && !options.videosPath) throw new Error(`"videoSize" option requires "videosPath" to be specified`); if (options.extraHTTPHeaders) @@ -548,6 +538,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors, acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads), clientCertificates: await toClientCertificatesProtocol(options.clientCertificates), + mockingProxyBaseURL: type?._playwright._mockingProxy?.baseURL(), }; if (!contextParams.recordVideo && options.videosPath) { contextParams.recordVideo = { diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 78074b9612..5417dd529d 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -27,7 +27,6 @@ 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; @@ -96,7 +95,7 @@ export class BrowserType extends ChannelOwner imple const logger = options.logger || this._defaultLaunchOptions?.logger; assert(!(options as any).port, 'Cannot specify a port without launching as a server.'); options = { ...this._defaultLaunchOptions, ...this._defaultContextOptions, ...options }; - const contextParams = await prepareBrowserContextParams(options); + const contextParams = await prepareBrowserContextParams(options, this); const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = { ...contextParams, ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined, @@ -243,13 +242,6 @@ export class BrowserType extends ChannelOwner imple 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 f49b09ba22..7886243ea1 100644 --- a/packages/playwright-core/src/client/mockingProxy.ts +++ b/packages/playwright-core/src/client/mockingProxy.ts @@ -69,7 +69,7 @@ export class MockingProxy extends ChannelOwner { } findPage(correlation: string): Page | undefined { - const guid = `Page@${correlation}`; + 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) { @@ -81,10 +81,7 @@ export class MockingProxy extends ChannelOwner { } } - instrumentationHeaders(page: Page) { - const correlation = page._guid.substring('Page@'.length); - return { - 'x-playwright-proxy': `${this._initializer.baseURL}/pw_meta:${correlation}/`, - }; + baseURL() { + return this._initializer.baseURL; } } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index a14c442c64..2508e34171 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -775,6 +775,7 @@ scheme.BrowserNewContextParams = tObject({ cookies: tOptional(tArray(tType('SetNetworkCookie'))), origins: tOptional(tArray(tType('OriginStorage'))), })), + mockingProxyBaseURL: tOptional(tString), }); scheme.BrowserNewContextResult = tObject({ context: tChannel(['BrowserContext']), diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 68559d7c7e..4e891a34bf 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -330,7 +330,7 @@ export class FFPage implements PageDelegate { } async updateExtraHTTPHeaders(): Promise { - await this._session.send('Network.setExtraHTTPHeaders', { headers: this._page.extraHTTPHeaders() || [] }); + await this._session.send('Network.setExtraHTTPHeaders', { headers: this._page.extraHTTPHeaders() }); } async updateEmulatedViewportSize(): Promise { diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 1570df5770..463bd898db 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -654,7 +654,7 @@ export class Frame extends SdkObject { private async _gotoAction(progress: Progress, url: string, options: types.GotoOptions): Promise { const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); progress.log(`navigating to "${url}", waiting until "${waitUntil}"`); - const headers = this._page.extraHTTPHeaders() || []; + const headers = this._page.extraHTTPHeaders(); const refererHeader = headers.find(h => h.name.toLowerCase() === 'referer'); let referer = refererHeader ? refererHeader.value : undefined; if (options.referer !== undefined) { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 9b85837b65..b4e8fbb504 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -365,8 +365,20 @@ export class Page extends SdkObject { return this._delegate.updateExtraHTTPHeaders(); } - extraHTTPHeaders(): types.HeadersArray | undefined { - return this._extraHTTPHeaders; + extraHTTPHeaders(): types.HeadersArray { + return this.instrumentationHeaders().concat(this._extraHTTPHeaders ?? []); + } + + instrumentationHeaders() { + const headers: channels.NameValue[] = []; + + const mockingProxyBaseURL = this.context()._options.mockingProxyBaseURL; + if (mockingProxyBaseURL) { + const correlation = this.guid.substring('Page@'.length); + headers.push({ name: 'x-playwright-proxy', value: encodeURIComponent(mockingProxyBaseURL + `pw_meta:${correlation}/`) }); + } + + return headers; } async _onBindingCalled(payload: string, context: dom.FrameExecutionContext) { diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index cd3d249ad0..a261cec928 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -57,7 +57,7 @@ type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & { _optionContextReuseMode: ContextReuseMode, _optionConnectOptions: PlaywrightWorkerOptions['connectOptions'], _reuseContext: boolean, - _mockingProxy?: MockingProxy, + _mockingProxy?: void, }; const playwrightFixtures: Fixtures = ({ @@ -124,12 +124,11 @@ const playwrightFixtures: Fixtures = ({ }, true); }, { scope: 'worker', timeout: 0 }], - _mockingProxy: [async ({ mockingProxy: mockingProxyOption, playwright }, use) => { - if (mockingProxyOption !== 'inject-via-header') - return await use(undefined); - const mockingProxy = await (playwright as PlaywrightImpl)._startMockingProxy(); - await use(mockingProxy); - }, { scope: 'worker', box: true }], + _mockingProxy: [async ({ mockingProxy, playwright }, use) => { + if (mockingProxy === 'inject-via-header') + await (playwright as PlaywrightImpl)._startMockingProxy(); + await use(); + }, { scope: 'worker', box: true, auto: true }], acceptDownloads: [({ contextOptions }, use) => use(contextOptions.acceptDownloads ?? true), { option: true }], bypassCSP: [({ contextOptions }, use) => use(contextOptions.bypassCSP ?? false), { option: true }], @@ -259,7 +258,7 @@ const playwrightFixtures: Fixtures = ({ } }, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any], - _setupArtifacts: [async ({ playwright, screenshot, _mockingProxy }, use, testInfo) => { + _setupArtifacts: [async ({ playwright, screenshot }, use, testInfo) => { // This fixture has a separate zero-timeout slot to ensure that artifact collection // happens even after some fixtures or hooks time out. // Now that default test timeout is known, we can replace zero with an actual value. @@ -313,8 +312,6 @@ const playwrightFixtures: Fixtures = ({ currentTestInfo()?._setDebugMode(); }, runAfterCreateBrowserContext: async (context: BrowserContextImpl) => { - if (_mockingProxy) - await context._subscribeToMockingProxy(_mockingProxy); await artifactsRecorder?.didCreateBrowserContext(context); const testInfo = currentTestInfo(); if (testInfo) diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 750709543d..f197aefe12 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -1369,6 +1369,7 @@ export type BrowserNewContextParams = { cookies?: SetNetworkCookie[], origins?: OriginStorage[], }, + mockingProxyBaseURL?: string, }; export type BrowserNewContextOptions = { noDefaultViewport?: boolean, @@ -1435,6 +1436,7 @@ export type BrowserNewContextOptions = { cookies?: SetNetworkCookie[], origins?: OriginStorage[], }, + mockingProxyBaseURL?: string, }; export type BrowserNewContextResult = { context: BrowserContextChannel, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 2768d4cba5..c50d69492a 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1038,6 +1038,7 @@ Browser: origins: type: array? items: OriginStorage + mockingProxyBaseURL: string? returns: context: BrowserContext