This commit is contained in:
Simon Knott 2025-02-03 14:19:17 +01:00
parent 08afdc600c
commit d80f3297aa
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
14 changed files with 33 additions and 113 deletions

View file

@ -144,7 +144,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
}); });
this._channel.on('request', ({ request, page }) => this._onRequest(network.Request.from(request), Page.fromNullable(page))); this._channel.on('request', ({ request, page }) => this._onRequest(network.Request.from(request), Page.fromNullable(page)));
this._channel.on('requestFailed', ({ request, failureText, responseEndTiming, page }) => this._onRequestFailed(network.Request.from(request), responseEndTiming, failureText, Page.fromNullable(page))); this._channel.on('requestFailed', ({ request, failureText, responseEndTiming, page }) => this._onRequestFailed(network.Request.from(request), responseEndTiming, failureText, Page.fromNullable(page)));
this._channel.on('requestFinished', params => this._onRequestFinished(params)); this._channel.on('requestFinished', ({ request, response, page, responseEndTiming }) => this._onRequestFinished(network.Request.from(request), network.Response.fromNullable(response), Page.fromNullable(page), responseEndTiming));
this._channel.on('response', ({ response, page }) => this._onResponse(network.Response.from(response), Page.fromNullable(page))); this._channel.on('response', ({ response, page }) => this._onResponse(network.Response.from(response), Page.fromNullable(page)));
this._closedPromise = new Promise(f => this.once(Events.BrowserContext.Close, f)); this._closedPromise = new Promise(f => this.once(Events.BrowserContext.Close, f));
@ -165,27 +165,27 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
this.tracing._tracesDir = browserOptions.tracesDir; this.tracing._tracesDir = browserOptions.tracesDir;
} }
private async _onPage(page: Page): Promise<void>{ private _onPage(page: Page): void {
this._pages.add(page); this._pages.add(page);
this.emit(Events.BrowserContext.Page, page); this.emit(Events.BrowserContext.Page, page);
await this._mockingProxy?.instrumentPage(page);
if (page._opener && !page._opener.isClosed()) if (page._opener && !page._opener.isClosed())
page._opener.emit(Events.Page.Popup, page); page._opener.emit(Events.Page.Popup, page);
this._mockingProxy?.instrumentPage(page);
} }
private _onRequest(request: network.Request, page: Page | null) { _onRequest(request: network.Request, page: Page | null) {
this.emit(Events.BrowserContext.Request, request); this.emit(Events.BrowserContext.Request, request);
if (page) if (page)
page.emit(Events.Page.Request, request); page.emit(Events.Page.Request, request);
} }
private _onResponse(response: network.Response, page: Page | null) { _onResponse(response: network.Response, page: Page | null) {
this.emit(Events.BrowserContext.Response, response); this.emit(Events.BrowserContext.Response, response);
if (page) if (page)
page.emit(Events.Page.Response, response); page.emit(Events.Page.Response, response);
} }
private _onRequestFailed(request: network.Request, responseEndTiming: number, failureText: string | undefined, page: Page | null) { _onRequestFailed(request: network.Request, responseEndTiming: number, failureText: string | undefined, page: Page | null) {
request._failureText = failureText || null; request._failureText = failureText || null;
request._setResponseEndTiming(responseEndTiming); request._setResponseEndTiming(responseEndTiming);
this.emit(Events.BrowserContext.RequestFailed, request); this.emit(Events.BrowserContext.RequestFailed, request);
@ -193,11 +193,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
page.emit(Events.Page.RequestFailed, request); page.emit(Events.Page.RequestFailed, request);
} }
private _onRequestFinished(params: channels.BrowserContextRequestFinishedEvent) { _onRequestFinished(request: network.Request, response: network.Response | null, page: Page | null, responseEndTiming: number) {
const { responseEndTiming } = params;
const request = network.Request.from(params.request);
const response = network.Response.fromNullable(params.response);
const page = Page.fromNullable(params.page);
request._setResponseEndTiming(responseEndTiming); request._setResponseEndTiming(responseEndTiming);
this.emit(Events.BrowserContext.RequestFinished, request); this.emit(Events.BrowserContext.RequestFinished, request);
if (page) if (page)
@ -250,13 +246,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
if (this._mockingProxy) if (this._mockingProxy)
throw new Error('Multiple mocking proxies are not supported'); throw new Error('Multiple mocking proxies are not supported');
this._mockingProxy = mockingProxy; this._mockingProxy = mockingProxy;
this._registeredListeners.push(
eventsHelper.addEventListener(this._mockingProxy, Events.MockingProxy.Route, (route: network.Route) => {
const page = route.request()._safePage()!;
page._onRoute(route);
}),
// TODO: should we also emit `request`, `response`, `requestFinished`, `requestFailed` events?
);
} }
setDefaultNavigationTimeout(timeout: number | undefined) { setDefaultNavigationTimeout(timeout: number | undefined) {
@ -421,15 +410,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
private async _updateInterceptionPatterns() { private async _updateInterceptionPatterns() {
const patterns = network.RouteHandler.prepareInterceptionPatterns(this._routes); const patterns = network.RouteHandler.prepareInterceptionPatterns(this._routes);
await this._channel.setNetworkInterceptionPatterns({ patterns }); await this._channel.setNetworkInterceptionPatterns({ patterns });
await this._updateMockingProxyInterceptionPatterns();
}
async _updateMockingProxyInterceptionPatterns() {
if (!this._mockingProxy)
return;
const pageRoutes = this.pages().flatMap(page => page._routes);
const patterns = network.RouteHandler.prepareInterceptionPatterns(this._routes.concat(pageRoutes));
await this._mockingProxy.setInterceptionPatterns({ patterns });
} }
private async _updateWebSocketInterceptionPatterns() { private async _updateWebSocketInterceptionPatterns() {

View file

@ -94,8 +94,4 @@ export const Events = {
Console: 'console', Console: 'console',
Window: 'window', Window: 'window',
}, },
MockingProxy: {
Route: 'route',
},
}; };

View file

@ -17,7 +17,6 @@ import * as network from './network';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { ChannelOwner } from './channelOwner'; import { ChannelOwner } from './channelOwner';
import { APIRequestContext } from './fetch'; import { APIRequestContext } from './fetch';
import { Events } from './events';
import { assert } from '../utils'; import { assert } from '../utils';
import type { Page } from './page'; import type { Page } from './page';
@ -31,39 +30,39 @@ export class MockingProxy extends ChannelOwner<channels.MockingProxyChannel> {
this._channel.on('route', async (params: channels.MockingProxyRouteEvent) => { this._channel.on('route', async (params: channels.MockingProxyRouteEvent) => {
const route = network.Route.from(params.route); const route = network.Route.from(params.route);
route._context = requestContext; route._context = requestContext;
this.emit(Events.MockingProxy.Route, route); const page = route.request()._safePage()!;
await page._onRoute(route);
}); });
this._channel.on('request', async (params: channels.MockingProxyRequestEvent) => { this._channel.on('request', async (params: channels.MockingProxyRequestEvent) => {
const page = this._pages.get(params.correlation); const page = this._pages.get(params.correlation);
assert(page); assert(page);
const request = network.Request.from(params.request); const request = network.Request.from(params.request);
request._page = page; request._pageForMockingProxy = page;
page.context()._onRequest(request, page);
}); });
this._channel.on('requestFailed', async (params: channels.MockingProxyRequestFailedEvent) => { this._channel.on('requestFailed', async (params: channels.MockingProxyRequestFailedEvent) => {
const request = network.Request.from(params.request); const request = network.Request.from(params.request);
request._failureText = params.failureText ?? null; const page = request._safePage()!;
request._setResponseEndTiming(params.responseEndTiming); page.context()._onRequestFailed(request, params.responseEndTiming, params.failureText, page);
}); });
this._channel.on('requestFinished', async (params: channels.MockingProxyRequestFinishedEvent) => { this._channel.on('requestFinished', async (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);
request._setResponseEndTiming(responseEndTiming); const page = request._safePage()!;
response?._finishedPromise.resolve(null); page.context()._onRequestFinished(request, response, page, responseEndTiming);
}); });
this._channel.on('response', async (params: channels.MockingProxyResponseEvent) => { this._channel.on('response', async (params: channels.MockingProxyResponseEvent) => {
// no-op const response = network.Response.from(params.response);
const page = response.request()._safePage()!;
page.context()._onResponse(response, page);
}); });
} }
async setInterceptionPatterns(params: channels.MockingProxySetInterceptionPatternsParams) {
await this._channel.setInterceptionPatterns(params);
}
async instrumentPage(page: Page) { async instrumentPage(page: Page) {
const correlation = page._guid.split('@')[1]; const correlation = page._guid.split('@')[1];
this._pages.set(correlation, page); this._pages.set(correlation, page);

View file

@ -86,7 +86,7 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
private _actualHeadersPromise: Promise<RawHeaders> | undefined; private _actualHeadersPromise: Promise<RawHeaders> | undefined;
_timing: ResourceTiming; _timing: ResourceTiming;
private _fallbackOverrides: SerializedFallbackOverrides = {}; private _fallbackOverrides: SerializedFallbackOverrides = {};
_page: Page | null = null; _pageForMockingProxy: Page | null = null;
static from(request: channels.RequestChannel): Request { static from(request: channels.RequestChannel): Request {
return (request as any)._object; return (request as any)._object;
@ -217,7 +217,7 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
} }
_safePage(): Page | null { _safePage(): Page | null {
return this._page ?? Frame.fromNullable(this._initializer.frame)?._page ?? null; return this._pageForMockingProxy ?? Frame.fromNullable(this._initializer.frame)?._page ?? null;
} }
serviceWorker(): Worker | null { serviceWorker(): Worker | null {

View file

@ -568,7 +568,6 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
private async _updateInterceptionPatterns() { private async _updateInterceptionPatterns() {
const patterns = RouteHandler.prepareInterceptionPatterns(this._routes); const patterns = RouteHandler.prepareInterceptionPatterns(this._routes);
await this._channel.setNetworkInterceptionPatterns({ patterns }); await this._channel.setNetworkInterceptionPatterns({ patterns });
await this._browserContext._updateMockingProxyInterceptionPatterns();
} }
private async _updateWebSocketInterceptionPatterns() { private async _updateWebSocketInterceptionPatterns() {

View file

@ -73,4 +73,9 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
static from(channel: channels.PlaywrightChannel): Playwright { static from(channel: channels.PlaywrightChannel): Playwright {
return (channel as any)._object; return (channel as any)._object;
} }
async _startMockingProxy() {
const { mockingProxy } = await this._connection.localUtils()._channel.newMockingProxy({});
return (mockingProxy as any)._object;
}
} }

View file

@ -363,16 +363,7 @@ scheme.MockingProxyRequestFinishedEvent = tObject({
}); });
scheme.MockingProxyResponseEvent = tObject({ scheme.MockingProxyResponseEvent = tObject({
response: tChannel(['Response']), response: tChannel(['Response']),
page: tOptional(tChannel(['Page'])),
}); });
scheme.MockingProxySetInterceptionPatternsParams = tObject({
patterns: tArray(tObject({
glob: tOptional(tString),
regexSource: tOptional(tString),
regexFlags: tOptional(tString),
})),
});
scheme.MockingProxySetInterceptionPatternsResult = tOptional(tObject({}));
scheme.RootInitializer = tOptional(tObject({})); scheme.RootInitializer = tOptional(tObject({}));
scheme.RootInitializeParams = tObject({ scheme.RootInitializeParams = tObject({
sdkLanguage: tEnum(['javascript', 'python', 'java', 'csharp']), sdkLanguage: tEnum(['javascript', 'python', 'java', 'csharp']),

View file

@ -312,8 +312,7 @@ class HarBackend {
redirectURL?: string, redirectURL?: string,
status?: number, status?: number,
headers?: HeadersArray, headers?: HeadersArray,
body?: Buffer body?: Buffer }> {
}> {
let entry; let entry;
try { try {
entry = await this._harFindResponse(url, method, headers, postData); entry = await this._harFindResponse(url, method, headers, postData);

View file

@ -13,14 +13,12 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { CallMetadata } from '@protocol/callMetadata';
import { MockingProxy } from '../mockingProxy'; import { MockingProxy } from '../mockingProxy';
import type { RootDispatcher } from './dispatcher'; import type { RootDispatcher } from './dispatcher';
import { Dispatcher, existingDispatcher } from './dispatcher'; import { Dispatcher, existingDispatcher } from './dispatcher';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { APIRequestContextDispatcher, RequestDispatcher, ResponseDispatcher, RouteDispatcher } from './networkDispatchers'; import { APIRequestContextDispatcher, RequestDispatcher, ResponseDispatcher, RouteDispatcher } from './networkDispatchers';
import type { Request, Route } from '../network'; import type { Request, Route } from '../network';
import { urlMatches } from '../../utils/isomorphic/urlMatch';
export class MockingProxyDispatcher extends Dispatcher<MockingProxy, channels.MockingProxyChannel, RootDispatcher> implements channels.MockingProxyChannel { export class MockingProxyDispatcher extends Dispatcher<MockingProxy, channels.MockingProxyChannel, RootDispatcher> implements channels.MockingProxyChannel {
_type_MockingProxy = true; _type_MockingProxy = true;
@ -58,12 +56,4 @@ export class MockingProxyDispatcher extends Dispatcher<MockingProxy, channels.Mo
}); });
}); });
} }
async setInterceptionPatterns(params: channels.MockingProxySetInterceptionPatternsParams, metadata?: CallMetadata): Promise<channels.MockingProxySetInterceptionPatternsResult> {
if (params.patterns.length === 0)
return this._object.setInterceptionPatterns(undefined);
const urlMatchers = params.patterns.map(pattern => pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags!) : pattern.glob!);
this._object.setInterceptionPatterns(url => urlMatchers.some(urlMatch => urlMatches(undefined, url, urlMatch)));
}
} }

View file

@ -38,7 +38,6 @@ export class MockingProxy extends SdkObject implements RequestContext {
}; };
fetchRequest: APIRequestContext; fetchRequest: APIRequestContext;
private _matches?: (url: string) => boolean;
private _httpServer = new WorkerHttpServer(); private _httpServer = new WorkerHttpServer();
constructor(parent: SdkObject, requestContext: APIRequestContext) { constructor(parent: SdkObject, requestContext: APIRequestContext) {
@ -63,10 +62,6 @@ export class MockingProxy extends SdkObject implements RequestContext {
return this._httpServer.port(); return this._httpServer.port();
} }
setInterceptionPatterns(matches?: (url: string) => boolean) {
this._matches = matches;
}
private async _proxy(req: http.IncomingMessage, res: http.ServerResponse) { private async _proxy(req: http.IncomingMessage, res: http.ServerResponse) {
if (req.url?.startsWith('/')) if (req.url?.startsWith('/'))
req.url = req.url.substring(1); req.url = req.url.substring(1);
@ -212,14 +207,7 @@ export class MockingProxy extends SdkObject implements RequestContext {
}, },
}); });
if (!correlation) this.emit(MockingProxy.Events.Route, route);
return await route.continue({ isFallback: false });
if (this._matches?.(req.url!))
this.emit(MockingProxy.Events.Route, route);
else
await route.continue({ isFallback: false });
} }
addRouteInFlight(route: Route): void { addRouteInFlight(route: Route): void {
@ -258,7 +246,7 @@ async function collectBody(req: http.IncomingMessage) {
} }
export class WorkerHttpServer extends HttpServer { export class WorkerHttpServer extends HttpServer {
override _handleCORS(request: http.IncomingMessage, response: http.ServerResponse): boolean { override handleCORS(request: http.IncomingMessage, response: http.ServerResponse): boolean {
return false; return false;
} }
} }

View file

@ -213,7 +213,7 @@ export class HttpServer {
readable.pipe(response); readable.pipe(response);
} }
_handleCORS(request: http.IncomingMessage, response: http.ServerResponse): boolean { handleCORS(request: http.IncomingMessage, response: http.ServerResponse): boolean {
if (request.method === 'OPTIONS') { if (request.method === 'OPTIONS') {
response.writeHead(200); response.writeHead(200);
response.end(); response.end();
@ -224,7 +224,7 @@ export class HttpServer {
} }
private _onRequest(request: http.IncomingMessage, response: http.ServerResponse) { private _onRequest(request: http.IncomingMessage, response: http.ServerResponse) {
if (this._handleCORS(request, response)) if (this.handleCORS(request, response))
return; return;
request.on('error', () => response.end()); request.on('error', () => response.end());

View file

@ -27,7 +27,7 @@ import type { ApiCallData, ClientInstrumentation, ClientInstrumentationListener
import type { MockingProxy } from '../../playwright-core/src/client/mockingProxy'; import type { MockingProxy } from '../../playwright-core/src/client/mockingProxy';
import type { BrowserContext as BrowserContextImpl } from '../../playwright-core/src/client/browserContext'; import type { BrowserContext as BrowserContextImpl } from '../../playwright-core/src/client/browserContext';
import { currentTestInfo } from './common/globals'; import { currentTestInfo } from './common/globals';
import type { LocalUtils } from 'playwright-core/lib/client/localUtils'; import type { Playwright as PlaywrightImpl } from 'playwright-core/lib/client/playwright';
export { expect } from './matchers/expect'; export { expect } from './matchers/expect';
export const _baseTest: TestType<{}, {}> = rootTestType.test; export const _baseTest: TestType<{}, {}> = rootTestType.test;
@ -127,9 +127,8 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
_mockingProxy: [async ({ mockingProxy: mockingProxyOption, playwright }, use) => { _mockingProxy: [async ({ mockingProxy: mockingProxyOption, playwright }, use) => {
if (!mockingProxyOption) if (!mockingProxyOption)
return await use(undefined); return await use(undefined);
const localUtils: LocalUtils = (playwright as any)._connection.localUtils(); const mockingProxy = await (playwright as PlaywrightImpl)._startMockingProxy();
const { mockingProxy } = await localUtils._channel.newMockingProxy({}); await use(mockingProxy);
await use((mockingProxy as any)._object);
}, { scope: 'worker', box: true }], }, { scope: 'worker', box: true }],
acceptDownloads: [({ contextOptions }, use) => use(contextOptions.acceptDownloads ?? true), { option: true }], acceptDownloads: [({ contextOptions }, use) => use(contextOptions.acceptDownloads ?? true), { option: true }],

View file

@ -589,7 +589,6 @@ export interface MockingProxyEventTarget {
} }
export interface MockingProxyChannel extends MockingProxyEventTarget, EventTargetChannel { export interface MockingProxyChannel extends MockingProxyEventTarget, EventTargetChannel {
_type_MockingProxy: boolean; _type_MockingProxy: boolean;
setInterceptionPatterns(params: MockingProxySetInterceptionPatternsParams, metadata?: CallMetadata): Promise<MockingProxySetInterceptionPatternsResult>;
} }
export type MockingProxyRouteEvent = { export type MockingProxyRouteEvent = {
route: RouteChannel, route: RouteChannel,
@ -610,19 +609,7 @@ export type MockingProxyRequestFinishedEvent = {
}; };
export type MockingProxyResponseEvent = { export type MockingProxyResponseEvent = {
response: ResponseChannel, response: ResponseChannel,
page?: PageChannel,
}; };
export type MockingProxySetInterceptionPatternsParams = {
patterns: {
glob?: string,
regexSource?: string,
regexFlags?: string,
}[],
};
export type MockingProxySetInterceptionPatternsOptions = {
};
export type MockingProxySetInterceptionPatternsResult = void;
export interface MockingProxyEvents { export interface MockingProxyEvents {
'route': MockingProxyRouteEvent; 'route': MockingProxyRouteEvent;

View file

@ -682,18 +682,6 @@ MockingProxy:
port: number port: number
requestContext: APIRequestContext requestContext: APIRequestContext
commands:
setInterceptionPatterns:
parameters:
patterns:
type: array
items:
type: object
properties:
glob: string?
regexSource: string?
regexFlags: string?
events: events:
route: route:
parameters: parameters:
@ -719,7 +707,6 @@ MockingProxy:
response: response:
parameters: parameters:
response: Response response: Response
page: Page?
Root: Root:
type: interface type: interface