more
This commit is contained in:
parent
aa2aef146d
commit
7a08cd6fa7
|
|
@ -642,6 +642,8 @@ Known Limitations:
|
||||||
1. The mocking proxy is experimental and subject to change.
|
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.
|
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.
|
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`.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import { assert, headersObjectToArray, monotonicTime } from '../utils';
|
||||||
import type * as api from '../../types/types';
|
import type * as api from '../../types/types';
|
||||||
import { raceAgainstDeadline } from '../utils/timeoutRunner';
|
import { raceAgainstDeadline } from '../utils/timeoutRunner';
|
||||||
import type { Playwright } from './playwright';
|
import type { Playwright } from './playwright';
|
||||||
|
import type { Page } from './page';
|
||||||
|
|
||||||
export interface BrowserServerLauncher {
|
export interface BrowserServerLauncher {
|
||||||
launchServer(options?: LaunchServerOptions): Promise<api.BrowserServer>;
|
launchServer(options?: LaunchServerOptions): Promise<api.BrowserServer>;
|
||||||
|
|
@ -241,6 +242,14 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
context.setDefaultTimeout(this._defaultContextTimeout);
|
context.setDefaultTimeout(this._defaultContextTimeout);
|
||||||
if (this._defaultContextNavigationTimeout !== undefined)
|
if (this._defaultContextNavigationTimeout !== undefined)
|
||||||
context.setDefaultNavigationTimeout(this._defaultContextNavigationTimeout);
|
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);
|
await this._instrumentation.runAfterCreateBrowserContext(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,27 +16,27 @@
|
||||||
import * as network from './network';
|
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 type { APIRequestContext } from './fetch';
|
||||||
import { assert } from '../utils';
|
import { assert } from '../utils';
|
||||||
import type { Page } from './page';
|
import type { Page } from './page';
|
||||||
import { Events } from './events';
|
import type { Playwright } from './playwright';
|
||||||
|
|
||||||
export class MockingProxy extends ChannelOwner<channels.MockingProxyChannel> {
|
export class MockingProxy extends ChannelOwner<channels.MockingProxyChannel> {
|
||||||
private _pages = new Map<string, Page>();
|
_requestContext!: APIRequestContext;
|
||||||
|
_playwright!: Playwright;
|
||||||
|
|
||||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.MockingProxyInitializer) {
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.MockingProxyInitializer) {
|
||||||
super(parent, type, guid, initializer);
|
super(parent, type, guid, initializer);
|
||||||
|
|
||||||
const requestContext = APIRequestContext.from(initializer.requestContext);
|
|
||||||
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._apiRequestContext = requestContext;
|
route._apiRequestContext = this._requestContext;
|
||||||
const page = route.request()._pageForMockingProxy!;
|
const page = route.request()._pageForMockingProxy!;
|
||||||
await page._onRoute(route);
|
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.findPage(params.correlation);
|
||||||
assert(page);
|
assert(page);
|
||||||
const request = network.Request.from(params.request);
|
const request = network.Request.from(params.request);
|
||||||
request._pageForMockingProxy = page;
|
request._pageForMockingProxy = page;
|
||||||
|
|
@ -68,16 +68,23 @@ export class MockingProxy extends ChannelOwner<channels.MockingProxyChannel> {
|
||||||
return (channel as any)._object;
|
return (channel as any)._object;
|
||||||
}
|
}
|
||||||
|
|
||||||
async instrumentPage(page: Page) {
|
findPage(correlation: string): Page | undefined {
|
||||||
const correlation = page._guid.split('@')[1];
|
const guid = `Page@${correlation}`;
|
||||||
this._pages.set(correlation, page);
|
// TODO: move this as list onto Playwright directly
|
||||||
page.on(Events.Page.Close, () => {
|
for (const browserType of [this._playwright.chromium, this._playwright.firefox, this._playwright.webkit]) {
|
||||||
this._pages.delete(correlation);
|
for (const context of browserType._contexts) {
|
||||||
});
|
for (const page of context._pages) {
|
||||||
const proxyUrl = `http://localhost:${this._initializer.port}/pw_meta:${correlation}/`;
|
if (page._guid === guid)
|
||||||
await page.setExtraHTTPHeaders({
|
return page;
|
||||||
'x-playwright-proxy': encodeURIComponent(proxyUrl)
|
}
|
||||||
});
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
instrumentationHeaders(page: Page) {
|
||||||
|
const correlation = page._guid.substring('Page@'.length);
|
||||||
|
return {
|
||||||
|
'x-playwright-proxy': `${this._initializer.baseURL}/pw_meta:${correlation}/`,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||||
selectors: Selectors;
|
selectors: Selectors;
|
||||||
readonly request: APIRequest;
|
readonly request: APIRequest;
|
||||||
readonly errors: { TimeoutError: typeof TimeoutError };
|
readonly errors: { TimeoutError: typeof TimeoutError };
|
||||||
|
_mockingProxy?: MockingProxy;
|
||||||
|
|
||||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PlaywrightInitializer) {
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.PlaywrightInitializer) {
|
||||||
super(parent, type, guid, initializer);
|
super(parent, type, guid, initializer);
|
||||||
|
|
@ -77,7 +78,10 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
||||||
|
|
||||||
async _startMockingProxy() {
|
async _startMockingProxy() {
|
||||||
const requestContext = await this.request._newContext(undefined, this._connection.localUtils()._channel);
|
const requestContext = await this.request._newContext(undefined, this._connection.localUtils()._channel);
|
||||||
const { mockingProxy } = await this._connection.localUtils()._channel.newMockingProxy({ requestContext: requestContext._channel });
|
const result = await this._connection.localUtils()._channel.newMockingProxy({ requestContext: requestContext._channel });
|
||||||
return MockingProxy.from(mockingProxy);
|
this._mockingProxy = MockingProxy.from(result.mockingProxy);
|
||||||
|
this._mockingProxy._requestContext = requestContext;
|
||||||
|
this._mockingProxy._playwright = this;
|
||||||
|
return this._mockingProxy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -377,8 +377,7 @@ scheme.LocalUtilsNewMockingProxyResult = tObject({
|
||||||
mockingProxy: tChannel(['MockingProxy']),
|
mockingProxy: tChannel(['MockingProxy']),
|
||||||
});
|
});
|
||||||
scheme.MockingProxyInitializer = tObject({
|
scheme.MockingProxyInitializer = tObject({
|
||||||
port: tNumber,
|
baseURL: tString,
|
||||||
requestContext: tChannel(['APIRequestContext']),
|
|
||||||
});
|
});
|
||||||
scheme.MockingProxyRouteEvent = tObject({
|
scheme.MockingProxyRouteEvent = tObject({
|
||||||
route: tChannel(['Route']),
|
route: tChannel(['Route']),
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,7 @@ export class LocalUtilsDispatcher extends Dispatcher<SdkObject, channels.LocalUt
|
||||||
const requestContext = (params.requestContext as APIRequestContextDispatcher)._object;
|
const requestContext = (params.requestContext as APIRequestContextDispatcher)._object;
|
||||||
const mockingProxy = new MockingProxy(this._object, requestContext);
|
const mockingProxy = new MockingProxy(this._object, requestContext);
|
||||||
await mockingProxy.start();
|
await mockingProxy.start();
|
||||||
return { mockingProxy: MockingProxyDispatcher.from(this.parentScope(), mockingProxy) };
|
return { mockingProxy: new MockingProxyDispatcher(this, mockingProxy) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,30 +14,25 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { MockingProxy } from '../mockingProxy';
|
import { MockingProxy } from '../mockingProxy';
|
||||||
import type { RootDispatcher } from './dispatcher';
|
import { Dispatcher } 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 { RequestDispatcher, ResponseDispatcher, RouteDispatcher } from './networkDispatchers';
|
||||||
import type { Request, Route } from '../network';
|
import type { Request } from '../network';
|
||||||
|
import type { LocalUtilsDispatcher } from './localUtilsDispatcher';
|
||||||
|
|
||||||
export class MockingProxyDispatcher extends Dispatcher<MockingProxy, channels.MockingProxyChannel, RootDispatcher> implements channels.MockingProxyChannel {
|
export class MockingProxyDispatcher extends Dispatcher<MockingProxy, channels.MockingProxyChannel, LocalUtilsDispatcher> implements channels.MockingProxyChannel {
|
||||||
_type_MockingProxy = true;
|
_type_MockingProxy = true;
|
||||||
_type_EventTarget = true;
|
_type_EventTarget = true;
|
||||||
|
|
||||||
static from(scope: RootDispatcher, mockingProxy: MockingProxy): MockingProxyDispatcher {
|
constructor(scope: LocalUtilsDispatcher, mockingProxy: MockingProxy) {
|
||||||
return existingDispatcher<MockingProxyDispatcher>(mockingProxy) || new MockingProxyDispatcher(scope, mockingProxy);
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor(scope: RootDispatcher, mockingProxy: MockingProxy) {
|
|
||||||
super(scope, mockingProxy, 'MockingProxy', {
|
super(scope, mockingProxy, 'MockingProxy', {
|
||||||
port: mockingProxy.port(),
|
baseURL: mockingProxy.baseURL(),
|
||||||
requestContext: APIRequestContextDispatcher.from(scope, mockingProxy.fetchRequest),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addObjectListener(MockingProxy.Events.Route, (route: Route) => {
|
mockingProxy.onRoute = async route => {
|
||||||
const requestDispatcher = RequestDispatcher.from(this, route.request());
|
const requestDispatcher = RequestDispatcher.from(this, route.request());
|
||||||
this._dispatchEvent('route', { route: RouteDispatcher.from(requestDispatcher, route) });
|
this._dispatchEvent('route', { route: RouteDispatcher.from(requestDispatcher, route) });
|
||||||
});
|
};
|
||||||
this.addObjectListener(MockingProxy.Events.Request, ({ request, correlation }: { request: Request, correlation: string }) => {
|
this.addObjectListener(MockingProxy.Events.Request, ({ request, correlation }: { request: Request, correlation: string }) => {
|
||||||
this._dispatchEvent('request', { request: RequestDispatcher.from(this, request), correlation });
|
this._dispatchEvent('request', { request: RequestDispatcher.from(this, request), correlation });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,13 @@ export class MockingProxy extends SdkObject implements RequestContext {
|
||||||
static Events = {
|
static Events = {
|
||||||
Request: 'request',
|
Request: 'request',
|
||||||
Response: 'response',
|
Response: 'response',
|
||||||
Route: 'route',
|
|
||||||
RequestFailed: 'requestfailed',
|
RequestFailed: 'requestfailed',
|
||||||
RequestFinished: 'requestfinished',
|
RequestFinished: 'requestfinished',
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchRequest: APIRequestContext;
|
fetchRequest: APIRequestContext;
|
||||||
private _httpServer = new WorkerHttpServer();
|
private _httpServer = new WorkerHttpServer();
|
||||||
|
onRoute = (route: Route) => route.continue({ isFallback: true });
|
||||||
|
|
||||||
constructor(parent: SdkObject, requestContext: APIRequestContext) {
|
constructor(parent: SdkObject, requestContext: APIRequestContext) {
|
||||||
super(parent, 'MockingProxy');
|
super(parent, 'MockingProxy');
|
||||||
|
|
@ -66,6 +66,10 @@ export class MockingProxy extends SdkObject implements RequestContext {
|
||||||
return this._httpServer.port();
|
return this._httpServer.port();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
baseURL() {
|
||||||
|
return `http://localhost:${this.port()}/`;
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -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 {
|
addRouteInFlight(route: Route): void {
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ export function stripFragmentFromUrl(url: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestContext extends SdkObject {
|
export interface RequestContext extends SdkObject {
|
||||||
fetchRequest: APIRequestContext;
|
readonly fetchRequest: APIRequestContext;
|
||||||
|
|
||||||
addRouteInFlight(route: Route): void;
|
addRouteInFlight(route: Route): void;
|
||||||
removeRouteInFlight(route: Route): void;
|
removeRouteInFlight(route: Route): void;
|
||||||
|
|
|
||||||
3
packages/protocol/src/channels.d.ts
vendored
3
packages/protocol/src/channels.d.ts
vendored
|
|
@ -647,8 +647,7 @@ export interface LocalUtilsEvents {
|
||||||
|
|
||||||
// ----------- MockingProxy -----------
|
// ----------- MockingProxy -----------
|
||||||
export type MockingProxyInitializer = {
|
export type MockingProxyInitializer = {
|
||||||
port: number,
|
baseURL: string,
|
||||||
requestContext: APIRequestContextChannel,
|
|
||||||
};
|
};
|
||||||
export interface MockingProxyEventTarget {
|
export interface MockingProxyEventTarget {
|
||||||
on(event: 'route', callback: (params: MockingProxyRouteEvent) => void): this;
|
on(event: 'route', callback: (params: MockingProxyRouteEvent) => void): this;
|
||||||
|
|
|
||||||
|
|
@ -737,8 +737,7 @@ MockingProxy:
|
||||||
extends: EventTarget
|
extends: EventTarget
|
||||||
|
|
||||||
initializer:
|
initializer:
|
||||||
port: number
|
baseURL: string
|
||||||
requestContext: APIRequestContext
|
|
||||||
|
|
||||||
events:
|
events:
|
||||||
route:
|
route:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue