diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index 375a84265f..46feba1879 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -45,6 +45,7 @@ import { findValidator, ValidationError, type ValidatorContext } from '../protoc import { createInstrumentation } from './clientInstrumentation'; import type { ClientInstrumentation } from './clientInstrumentation'; import { formatCallLog, rewriteErrorMessage, zones } from '../utils'; +import { MockingProxy } from './mockingProxy'; class Root extends ChannelOwner { constructor(connection: Connection) { @@ -279,6 +280,9 @@ export class Connection extends EventEmitter { if (!this._localUtils) this._localUtils = result as LocalUtils; break; + case 'MockingProxy': + result = new MockingProxy(parent, type, guid, initializer); + break; case 'Page': result = new Page(parent, type, guid, initializer); break; diff --git a/packages/playwright-core/src/client/localUtils.ts b/packages/playwright-core/src/client/localUtils.ts index b50b4bc323..530547b227 100644 --- a/packages/playwright-core/src/client/localUtils.ts +++ b/packages/playwright-core/src/client/localUtils.ts @@ -17,7 +17,6 @@ import type * as channels from '@protocol/channels'; import { ChannelOwner } from './channelOwner'; import type { Size } from './types'; -import { APIRequestContext } from './fetch'; type DeviceDescriptor = { userAgent: string, @@ -31,7 +30,6 @@ type Devices = { [name: string]: DeviceDescriptor }; export class LocalUtils extends ChannelOwner { readonly devices: Devices; - readonly requestContext: APIRequestContext; constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) { super(parent, type, guid, initializer); @@ -39,6 +37,5 @@ export class LocalUtils extends ChannelOwner { this.devices = {}; for (const { name, descriptor } of initializer.deviceDescriptors) this.devices[name] = descriptor; - this.requestContext = APIRequestContext.from(initializer.requestContext); } } diff --git a/packages/playwright-core/src/client/mockingProxy.ts b/packages/playwright-core/src/client/mockingProxy.ts index 228a6d7fd4..fb41615cd9 100644 --- a/packages/playwright-core/src/client/mockingProxy.ts +++ b/packages/playwright-core/src/client/mockingProxy.ts @@ -18,7 +18,6 @@ import * as network from './network'; import { urlMatches, urlMatchesEqual, type URLMatch } from '../utils/isomorphic/urlMatch'; import type { LocalUtils } from './localUtils'; import type * as channels from '@protocol/channels'; -import { EventEmitter } from './eventEmitter'; import type { WaitForEventOptions } from './types'; import { Waiter } from './waiter'; import { Events } from './events'; @@ -26,31 +25,34 @@ import { isString } from '../utils/isomorphic/stringUtils'; import { isRegExp } from '../utils'; import { trimUrl } from './page'; import { TimeoutSettings } from '../common/timeoutSettings'; +import { ChannelOwner } from './channelOwner'; +import { APIRequestContext } from './fetch'; export class MockingProxyFactory implements api.MockingProxyFactory { constructor(private _localUtils: LocalUtils) {} - async newProxy(port: number): Promise { - return await MockingProxy.create(this._localUtils, port); + async newProxy(port?: number): Promise { + const { mockingProxy } = await this._localUtils._channel.newMockingProxy({ port }); + return (mockingProxy as any)._object; } } -export class MockingProxy extends EventEmitter implements api.MockingProxy { - _routes: network.RouteHandler[] = []; - private _localUtils: LocalUtils; +export class MockingProxy extends ChannelOwner implements api.MockingProxy { + private _routes: network.RouteHandler[] = []; private _port: number; private _timeoutSettings = new TimeoutSettings(); + private _requestContext: APIRequestContext; - private routeListener = ({ route }: channels.LocalUtilsRouteEvent) => { + private routeListener = ({ route }: channels.MockingProxyRouteEvent) => { this._onRoute(network.Route.from(route)); }; - private failedListener = (params: channels.LocalUtilsRequestFailedEvent) => { + private failedListener = (params: channels.MockingProxyRequestFailedEvent) => { const request = network.Request.from(params.request); if (params.failureText) request._failureText = params.failureText; request._setResponseEndTiming(params.responseEndTiming); this.emit('requestfailed', request); }; - private finishedListener = (params: channels.LocalUtilsRequestFinishedEvent) => { + private finishedListener = (params: channels.MockingProxyRequestFinishedEvent) => { const { responseEndTiming } = params; const request = network.Request.from(params.request); const response = network.Response.fromNullable(params.response); @@ -58,42 +60,32 @@ export class MockingProxy extends EventEmitter implements api.MockingProxy { this.emit('requestfinished', request); response?._finishedPromise.resolve(null); }; - private responseListener = ({ response }: channels.LocalUtilsResponseEvent) => { + private responseListener = ({ response }: channels.MockingProxyResponseEvent) => { this.emit('response', network.Response.from(response)); }; - private requestListener = ({ request }: channels.LocalUtilsRequestEvent) => { + private requestListener = ({ request }: channels.MockingProxyRequestEvent) => { this.emit('request', network.Request.from(request)); }; - private constructor(localUtils: LocalUtils, port: number) { - super(); + constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.MockingProxyInitializer) { + super(parent, type, guid, initializer); - this._localUtils = localUtils; - this._port = port; + this._port = initializer.port; + this._requestContext = APIRequestContext.from(initializer.requestContext); - this._localUtils._channel.on('route', this.routeListener); - this._localUtils._channel.on('request', this.requestListener); - this._localUtils._channel.on('requestFailed', this.failedListener); - this._localUtils._channel.on('requestFinished', this.finishedListener); - this._localUtils._channel.on('response', this.responseListener); - } - - private async _start() { - await this._localUtils._channel.setServerNetworkInterceptionPatterns({ patterns: [], port: this._port }); - } - - static async create(localUtils: LocalUtils, port: number) { - const instance = new MockingProxy(localUtils, port); - await instance._start(); - return instance; + this._channel.on('route', this.routeListener); + this._channel.on('request', this.requestListener); + this._channel.on('requestFailed', this.failedListener); + this._channel.on('requestFinished', this.finishedListener); + this._channel.on('response', this.responseListener); } dispose() { - this._localUtils._channel.off('route', this.routeListener); - this._localUtils._channel.off('request', this.requestListener); - this._localUtils._channel.off('requestFailed', this.failedListener); - this._localUtils._channel.off('requestFinished', this.finishedListener); - this._localUtils._channel.off('response', this.responseListener); + this._channel.off('route', this.routeListener); + this._channel.off('request', this.requestListener); + this._channel.off('requestFailed', this.failedListener); + this._channel.off('requestFinished', this.finishedListener); + this._channel.off('response', this.responseListener); } async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise { @@ -127,7 +119,7 @@ export class MockingProxy extends EventEmitter implements api.MockingProxy { } async _onRoute(route: network.Route) { - route._request = this._localUtils.requestContext; + route._request = this._requestContext; const routeHandlers = this._routes.slice(); for (const routeHandler of routeHandlers) { if (!routeHandler.matches(route.request().url())) @@ -149,8 +141,7 @@ export class MockingProxy extends EventEmitter implements api.MockingProxy { } private async _updateInterceptionPatterns() { - const patterns = network.RouteHandler.prepareInterceptionPatterns(this._routes); - await this._localUtils._channel.setServerNetworkInterceptionPatterns({ patterns, port: this._port }); + await this._channel.setInterceptionPatterns({ patterns: network.RouteHandler.prepareInterceptionPatterns(this._routes) }); } async waitForRequest(urlOrPredicate: string | RegExp | ((r: network.Request) => boolean | Promise), options: { timeout?: number } = {}): Promise { @@ -182,10 +173,10 @@ export class MockingProxy extends EventEmitter implements api.MockingProxy { } private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise { - return await this._localUtils._wrapApiCall(async () => { + return await this._wrapApiCall(async () => { const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; - const waiter = Waiter.createForEvent(this._localUtils, event); + const waiter = Waiter.createForEvent(this, event); if (logLine) waiter.log(logLine); waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`); diff --git a/tests/library/mockingproxy.spec.ts b/tests/library/mockingproxy.spec.ts index 6d341bc5de..f64b5d290b 100644 --- a/tests/library/mockingproxy.spec.ts +++ b/tests/library/mockingproxy.spec.ts @@ -18,13 +18,13 @@ import { playwrightTest as baseTest, expect } from '../config/browserTest'; import { pipeline } from 'stream/promises'; import { suppressCertificateWarning } from 'tests/config/utils'; -const test = baseTest.extend<{ proxiedRequest: APIRequestContext }, { mockingProxy: MockingProxy }>({ - mockingProxy: [async ({ playwright }, use, testInfo) => { +const test = baseTest.extend<{ proxiedRequest: APIRequestContext }, { mockproxy: MockingProxy }>({ + mockproxy: [async ({ playwright }, use, testInfo) => { const port = 32181 + testInfo.parallelIndex; const proxy = await playwright.mockingProxy.newProxy(port); await use(proxy); }, { scope: 'worker' }], - proxiedRequest: async ({ request, mockingProxy: mockproxy }, use) => { + proxiedRequest: async ({ request, mockproxy }, use) => { const originalFetch = request.fetch; request.fetch = function(urlOrRequest, options) { if (typeof urlOrRequest !== 'string') @@ -36,12 +36,12 @@ const test = baseTest.extend<{ proxiedRequest: APIRequestContext }, { mockingPro }, }); -test.beforeEach(async ({ mockingProxy: mockproxy }) => { +test.beforeEach(async ({ mockproxy }) => { await mockproxy.unrouteAll(); }); test.describe('transparent', () => { - test('generates events', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { + test('generates events', async ({ server, proxiedRequest, mockproxy }) => { const events: string[] = []; mockproxy.on('request', () => { events.push('request'); @@ -58,7 +58,7 @@ test.describe('transparent', () => { expect(events).toEqual(['request', 'response', 'requestfinished']); }); - test('event properties', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { + test('event properties', async ({ server, proxiedRequest, mockproxy }) => { const [ requestFinished, request, @@ -122,7 +122,7 @@ test.describe('transparent', () => { }); }); - test('securityDetails', async ({ httpsServer, proxiedRequest, mockingProxy: mockproxy }) => { + test('securityDetails', async ({ httpsServer, proxiedRequest, mockproxy }) => { const oldValue = process.env['NODE_TLS_REJECT_UNAUTHORIZED']; // https://stackoverflow.com/a/21961005/552185 process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'; @@ -149,7 +149,7 @@ test.describe('transparent', () => { } }); - test('request with body', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { + test('request with body', async ({ server, proxiedRequest, mockproxy }) => { server.setRoute('/echo', (req, res) => pipeline(req, res)); const [ requestEvent, @@ -173,7 +173,7 @@ test.describe('transparent', () => { }); }); - test('request failed', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { + test('request failed', async ({ server, proxiedRequest, mockproxy }) => { server.setRoute('/failure', (req, res) => { res.socket.destroy(); }); @@ -196,14 +196,14 @@ test.describe('transparent', () => { }); }); -test('stalling', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { +test('stalling', async ({ server, proxiedRequest, mockproxy }) => { const routes: Route[] = []; await mockproxy.route('**/abort', route => routes.push(route)); await expect(() => proxiedRequest.get(server.PREFIX + '/abort', { timeout: 100 })).rejects.toThrowError('Request timed out after 100ms'); expect(routes.length).toBe(1); }); -test('route properties', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { +test('route properties', async ({ server, proxiedRequest, mockproxy }) => { const routes: Route[] = []; await mockproxy.route('**/*', (route, request) => { expect(route.request()).toBe(request); @@ -214,12 +214,12 @@ test('route properties', async ({ server, proxiedRequest, mockingProxy: mockprox expect(routes.length).toBe(1); }); -test('aborting', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { +test('aborting', async ({ server, proxiedRequest, mockproxy }) => { await mockproxy.route('**/abort', route => route.abort()); await expect(() => proxiedRequest.get(server.PREFIX + '/abort', { timeout: 100 })).rejects.toThrowError('Request timed out after 100ms'); }); -test('fulfill', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { +test('fulfill', async ({ server, proxiedRequest, mockproxy }) => { let apiCalls = 0; server.setRoute('/endpoint', (req, res) => { apiCalls++; @@ -233,7 +233,7 @@ test('fulfill', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { expect(apiCalls).toBe(0); }); -test('continue', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { +test('continue', async ({ server, proxiedRequest, mockproxy }) => { server.setRoute('/echo', (req, res) => { res.setHeader('request-method', req.method); res.writeHead(200, req.headers); @@ -255,7 +255,7 @@ test('continue', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => expect(response.headers()['x-add']).toBe('baz'); }); -test('fallback', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { +test('fallback', async ({ server, proxiedRequest, mockproxy }) => { server.setRoute('/foo', (req, res) => { res.end('ok'); }); @@ -266,7 +266,7 @@ test('fallback', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => expect(await response.text()).toBe('ok'); }); -test('fetch', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { +test('fetch', async ({ server, proxiedRequest, mockproxy }) => { server.setRoute('/foo', (req, res) => { res.end('ok'); });