some more

This commit is contained in:
Simon Knott 2025-01-23 15:15:49 +01:00
parent 10646b1c25
commit 74678855cf
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
4 changed files with 51 additions and 59 deletions

View file

@ -45,6 +45,7 @@ import { findValidator, ValidationError, type ValidatorContext } from '../protoc
import { createInstrumentation } from './clientInstrumentation'; import { createInstrumentation } from './clientInstrumentation';
import type { ClientInstrumentation } from './clientInstrumentation'; import type { ClientInstrumentation } from './clientInstrumentation';
import { formatCallLog, rewriteErrorMessage, zones } from '../utils'; import { formatCallLog, rewriteErrorMessage, zones } from '../utils';
import { MockingProxy } from './mockingProxy';
class Root extends ChannelOwner<channels.RootChannel> { class Root extends ChannelOwner<channels.RootChannel> {
constructor(connection: Connection) { constructor(connection: Connection) {
@ -279,6 +280,9 @@ export class Connection extends EventEmitter {
if (!this._localUtils) if (!this._localUtils)
this._localUtils = result as LocalUtils; this._localUtils = result as LocalUtils;
break; break;
case 'MockingProxy':
result = new MockingProxy(parent, type, guid, initializer);
break;
case 'Page': case 'Page':
result = new Page(parent, type, guid, initializer); result = new Page(parent, type, guid, initializer);
break; break;

View file

@ -17,7 +17,6 @@
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { ChannelOwner } from './channelOwner'; import { ChannelOwner } from './channelOwner';
import type { Size } from './types'; import type { Size } from './types';
import { APIRequestContext } from './fetch';
type DeviceDescriptor = { type DeviceDescriptor = {
userAgent: string, userAgent: string,
@ -31,7 +30,6 @@ type Devices = { [name: string]: DeviceDescriptor };
export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> { export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
readonly devices: Devices; readonly devices: Devices;
readonly requestContext: APIRequestContext;
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) { constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) {
super(parent, type, guid, initializer); super(parent, type, guid, initializer);
@ -39,6 +37,5 @@ export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
this.devices = {}; this.devices = {};
for (const { name, descriptor } of initializer.deviceDescriptors) for (const { name, descriptor } of initializer.deviceDescriptors)
this.devices[name] = descriptor; this.devices[name] = descriptor;
this.requestContext = APIRequestContext.from(initializer.requestContext);
} }
} }

View file

@ -18,7 +18,6 @@ import * as network from './network';
import { urlMatches, urlMatchesEqual, type URLMatch } from '../utils/isomorphic/urlMatch'; import { urlMatches, urlMatchesEqual, type URLMatch } from '../utils/isomorphic/urlMatch';
import type { LocalUtils } from './localUtils'; import type { LocalUtils } from './localUtils';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { EventEmitter } from './eventEmitter';
import type { WaitForEventOptions } from './types'; import type { WaitForEventOptions } from './types';
import { Waiter } from './waiter'; import { Waiter } from './waiter';
import { Events } from './events'; import { Events } from './events';
@ -26,31 +25,34 @@ import { isString } from '../utils/isomorphic/stringUtils';
import { isRegExp } from '../utils'; import { isRegExp } from '../utils';
import { trimUrl } from './page'; import { trimUrl } from './page';
import { TimeoutSettings } from '../common/timeoutSettings'; import { TimeoutSettings } from '../common/timeoutSettings';
import { ChannelOwner } from './channelOwner';
import { APIRequestContext } from './fetch';
export class MockingProxyFactory implements api.MockingProxyFactory { export class MockingProxyFactory implements api.MockingProxyFactory {
constructor(private _localUtils: LocalUtils) {} constructor(private _localUtils: LocalUtils) {}
async newProxy(port: number): Promise<api.MockingProxy> { async newProxy(port?: number): Promise<api.MockingProxy> {
return await MockingProxy.create(this._localUtils, port); const { mockingProxy } = await this._localUtils._channel.newMockingProxy({ port });
return (mockingProxy as any)._object;
} }
} }
export class MockingProxy extends EventEmitter implements api.MockingProxy { export class MockingProxy extends ChannelOwner<channels.MockingProxyChannel> implements api.MockingProxy {
_routes: network.RouteHandler[] = []; private _routes: network.RouteHandler[] = [];
private _localUtils: LocalUtils;
private _port: number; private _port: number;
private _timeoutSettings = new TimeoutSettings(); private _timeoutSettings = new TimeoutSettings();
private _requestContext: APIRequestContext;
private routeListener = ({ route }: channels.LocalUtilsRouteEvent) => { private routeListener = ({ route }: channels.MockingProxyRouteEvent) => {
this._onRoute(network.Route.from(route)); this._onRoute(network.Route.from(route));
}; };
private failedListener = (params: channels.LocalUtilsRequestFailedEvent) => { private failedListener = (params: channels.MockingProxyRequestFailedEvent) => {
const request = network.Request.from(params.request); const request = network.Request.from(params.request);
if (params.failureText) if (params.failureText)
request._failureText = params.failureText; request._failureText = params.failureText;
request._setResponseEndTiming(params.responseEndTiming); request._setResponseEndTiming(params.responseEndTiming);
this.emit('requestfailed', request); this.emit('requestfailed', request);
}; };
private finishedListener = (params: channels.LocalUtilsRequestFinishedEvent) => { private finishedListener = (params: channels.MockingProxyRequestFinishedEvent) => {
const { responseEndTiming } = params; const { responseEndTiming } = params;
const request = network.Request.from(params.request); const request = network.Request.from(params.request);
const response = network.Response.fromNullable(params.response); const response = network.Response.fromNullable(params.response);
@ -58,42 +60,32 @@ export class MockingProxy extends EventEmitter implements api.MockingProxy {
this.emit('requestfinished', request); this.emit('requestfinished', request);
response?._finishedPromise.resolve(null); response?._finishedPromise.resolve(null);
}; };
private responseListener = ({ response }: channels.LocalUtilsResponseEvent) => { private responseListener = ({ response }: channels.MockingProxyResponseEvent) => {
this.emit('response', network.Response.from(response)); this.emit('response', network.Response.from(response));
}; };
private requestListener = ({ request }: channels.LocalUtilsRequestEvent) => { private requestListener = ({ request }: channels.MockingProxyRequestEvent) => {
this.emit('request', network.Request.from(request)); this.emit('request', network.Request.from(request));
}; };
private constructor(localUtils: LocalUtils, port: number) { constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.MockingProxyInitializer) {
super(); super(parent, type, guid, initializer);
this._localUtils = localUtils; this._port = initializer.port;
this._port = port; this._requestContext = APIRequestContext.from(initializer.requestContext);
this._localUtils._channel.on('route', this.routeListener); this._channel.on('route', this.routeListener);
this._localUtils._channel.on('request', this.requestListener); this._channel.on('request', this.requestListener);
this._localUtils._channel.on('requestFailed', this.failedListener); this._channel.on('requestFailed', this.failedListener);
this._localUtils._channel.on('requestFinished', this.finishedListener); this._channel.on('requestFinished', this.finishedListener);
this._localUtils._channel.on('response', this.responseListener); this._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;
} }
dispose() { dispose() {
this._localUtils._channel.off('route', this.routeListener); this._channel.off('route', this.routeListener);
this._localUtils._channel.off('request', this.requestListener); this._channel.off('request', this.requestListener);
this._localUtils._channel.off('requestFailed', this.failedListener); this._channel.off('requestFailed', this.failedListener);
this._localUtils._channel.off('requestFinished', this.finishedListener); this._channel.off('requestFinished', this.finishedListener);
this._localUtils._channel.off('response', this.responseListener); this._channel.off('response', this.responseListener);
} }
async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise<void> { async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise<void> {
@ -127,7 +119,7 @@ export class MockingProxy extends EventEmitter implements api.MockingProxy {
} }
async _onRoute(route: network.Route) { async _onRoute(route: network.Route) {
route._request = this._localUtils.requestContext; route._request = this._requestContext;
const routeHandlers = this._routes.slice(); const routeHandlers = this._routes.slice();
for (const routeHandler of routeHandlers) { for (const routeHandler of routeHandlers) {
if (!routeHandler.matches(route.request().url())) if (!routeHandler.matches(route.request().url()))
@ -149,8 +141,7 @@ export class MockingProxy extends EventEmitter implements api.MockingProxy {
} }
private async _updateInterceptionPatterns() { private async _updateInterceptionPatterns() {
const patterns = network.RouteHandler.prepareInterceptionPatterns(this._routes); await this._channel.setInterceptionPatterns({ patterns: network.RouteHandler.prepareInterceptionPatterns(this._routes) });
await this._localUtils._channel.setServerNetworkInterceptionPatterns({ patterns, port: this._port });
} }
async waitForRequest(urlOrPredicate: string | RegExp | ((r: network.Request) => boolean | Promise<boolean>), options: { timeout?: number } = {}): Promise<network.Request> { async waitForRequest(urlOrPredicate: string | RegExp | ((r: network.Request) => boolean | Promise<boolean>), options: { timeout?: number } = {}): Promise<network.Request> {
@ -182,10 +173,10 @@ export class MockingProxy extends EventEmitter implements api.MockingProxy {
} }
private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise<any> { private async _waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions, logLine?: string): Promise<any> {
return await this._localUtils._wrapApiCall(async () => { return await this._wrapApiCall(async () => {
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate); const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate; const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = Waiter.createForEvent(this._localUtils, event); const waiter = Waiter.createForEvent(this, event);
if (logLine) if (logLine)
waiter.log(logLine); waiter.log(logLine);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`); waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);

View file

@ -18,13 +18,13 @@ import { playwrightTest as baseTest, expect } from '../config/browserTest';
import { pipeline } from 'stream/promises'; import { pipeline } from 'stream/promises';
import { suppressCertificateWarning } from 'tests/config/utils'; import { suppressCertificateWarning } from 'tests/config/utils';
const test = baseTest.extend<{ proxiedRequest: APIRequestContext }, { mockingProxy: MockingProxy }>({ const test = baseTest.extend<{ proxiedRequest: APIRequestContext }, { mockproxy: MockingProxy }>({
mockingProxy: [async ({ playwright }, use, testInfo) => { mockproxy: [async ({ playwright }, use, testInfo) => {
const port = 32181 + testInfo.parallelIndex; const port = 32181 + testInfo.parallelIndex;
const proxy = await playwright.mockingProxy.newProxy(port); const proxy = await playwright.mockingProxy.newProxy(port);
await use(proxy); await use(proxy);
}, { scope: 'worker' }], }, { scope: 'worker' }],
proxiedRequest: async ({ request, mockingProxy: mockproxy }, use) => { proxiedRequest: async ({ request, mockproxy }, use) => {
const originalFetch = request.fetch; const originalFetch = request.fetch;
request.fetch = function(urlOrRequest, options) { request.fetch = function(urlOrRequest, options) {
if (typeof urlOrRequest !== 'string') 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(); await mockproxy.unrouteAll();
}); });
test.describe('transparent', () => { test.describe('transparent', () => {
test('generates events', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { test('generates events', async ({ server, proxiedRequest, mockproxy }) => {
const events: string[] = []; const events: string[] = [];
mockproxy.on('request', () => { mockproxy.on('request', () => {
events.push('request'); events.push('request');
@ -58,7 +58,7 @@ test.describe('transparent', () => {
expect(events).toEqual(['request', 'response', 'requestfinished']); expect(events).toEqual(['request', 'response', 'requestfinished']);
}); });
test('event properties', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { test('event properties', async ({ server, proxiedRequest, mockproxy }) => {
const [ const [
requestFinished, requestFinished,
request, 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']; const oldValue = process.env['NODE_TLS_REJECT_UNAUTHORIZED'];
// https://stackoverflow.com/a/21961005/552185 // https://stackoverflow.com/a/21961005/552185
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'; 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)); server.setRoute('/echo', (req, res) => pipeline(req, res));
const [ const [
requestEvent, 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) => { server.setRoute('/failure', (req, res) => {
res.socket.destroy(); 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[] = []; const routes: Route[] = [];
await mockproxy.route('**/abort', route => routes.push(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'); await expect(() => proxiedRequest.get(server.PREFIX + '/abort', { timeout: 100 })).rejects.toThrowError('Request timed out after 100ms');
expect(routes.length).toBe(1); expect(routes.length).toBe(1);
}); });
test('route properties', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { test('route properties', async ({ server, proxiedRequest, mockproxy }) => {
const routes: Route[] = []; const routes: Route[] = [];
await mockproxy.route('**/*', (route, request) => { await mockproxy.route('**/*', (route, request) => {
expect(route.request()).toBe(request); expect(route.request()).toBe(request);
@ -214,12 +214,12 @@ test('route properties', async ({ server, proxiedRequest, mockingProxy: mockprox
expect(routes.length).toBe(1); 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 mockproxy.route('**/abort', route => route.abort());
await expect(() => proxiedRequest.get(server.PREFIX + '/abort', { timeout: 100 })).rejects.toThrowError('Request timed out after 100ms'); 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; let apiCalls = 0;
server.setRoute('/endpoint', (req, res) => { server.setRoute('/endpoint', (req, res) => {
apiCalls++; apiCalls++;
@ -233,7 +233,7 @@ test('fulfill', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => {
expect(apiCalls).toBe(0); expect(apiCalls).toBe(0);
}); });
test('continue', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { test('continue', async ({ server, proxiedRequest, mockproxy }) => {
server.setRoute('/echo', (req, res) => { server.setRoute('/echo', (req, res) => {
res.setHeader('request-method', req.method); res.setHeader('request-method', req.method);
res.writeHead(200, req.headers); res.writeHead(200, req.headers);
@ -255,7 +255,7 @@ test('continue', async ({ server, proxiedRequest, mockingProxy: mockproxy }) =>
expect(response.headers()['x-add']).toBe('baz'); 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) => { server.setRoute('/foo', (req, res) => {
res.end('ok'); res.end('ok');
}); });
@ -266,7 +266,7 @@ test('fallback', async ({ server, proxiedRequest, mockingProxy: mockproxy }) =>
expect(await response.text()).toBe('ok'); expect(await response.text()).toBe('ok');
}); });
test('fetch', async ({ server, proxiedRequest, mockingProxy: mockproxy }) => { test('fetch', async ({ server, proxiedRequest, mockproxy }) => {
server.setRoute('/foo', (req, res) => { server.setRoute('/foo', (req, res) => {
res.end('ok'); res.end('ok');
}); });