mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: chain routes from multiple connections (#36105)
This commit is contained in:
parent
7cbc04e5ac
commit
70d64a2978
@ -70,7 +70,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||
readonly _pageBindings = new Map<string, PageBinding>();
|
||||
readonly _activeProgressControllers = new Set<ProgressController>();
|
||||
readonly _options: types.BrowserContextOptions;
|
||||
_requestInterceptor?: network.RouteHandler;
|
||||
readonly requestInterceptors: network.RouteHandler[] = [];
|
||||
private _isPersistentContext: boolean;
|
||||
private _closedStatus: 'open' | 'closing' | 'closed' = 'open';
|
||||
readonly _closePromise: Promise<Error>;
|
||||
@ -200,8 +200,6 @@ export abstract class BrowserContext extends SdkObject {
|
||||
this.selectors().setTestIdAttributeName(params.testIdAttributeName);
|
||||
}
|
||||
|
||||
await this._cancelAllRoutesInFlight();
|
||||
|
||||
// Close extra pages early.
|
||||
let page: Page | undefined = this.pages()[0];
|
||||
const [, ...otherPages] = this.pages();
|
||||
@ -439,8 +437,17 @@ export abstract class BrowserContext extends SdkObject {
|
||||
await this.doRemoveInitScripts(initScripts);
|
||||
}
|
||||
|
||||
async setRequestInterceptor(handler: network.RouteHandler | undefined): Promise<void> {
|
||||
this._requestInterceptor = handler;
|
||||
async addRequestInterceptor(handler: network.RouteHandler): Promise<void> {
|
||||
this.requestInterceptors.push(handler);
|
||||
await this.doUpdateRequestInterception();
|
||||
}
|
||||
|
||||
async removeRequestInterceptor(handler: network.RouteHandler): Promise<void> {
|
||||
const index = this.requestInterceptors.indexOf(handler);
|
||||
if (index === -1)
|
||||
return;
|
||||
this.requestInterceptors.splice(index, 1);
|
||||
await this.notifyRoutesInFlightAboutRemovedHandler(handler);
|
||||
await this.doUpdateRequestInterception();
|
||||
}
|
||||
|
||||
@ -547,10 +554,9 @@ export abstract class BrowserContext extends SdkObject {
|
||||
if (originsToSave.size) {
|
||||
const internalMetadata = serverSideCallMetadata();
|
||||
const page = await this.newPage(internalMetadata);
|
||||
await page.setServerRequestInterceptor(handler => {
|
||||
handler.fulfill({ body: '<html></html>' }).catch(() => {});
|
||||
return true;
|
||||
});
|
||||
page.addRequestInterceptor(route => {
|
||||
route.fulfill({ body: '<html></html>' }).catch(() => {});
|
||||
}, 'prepend');
|
||||
for (const origin of originsToSave) {
|
||||
const frame = page.mainFrame();
|
||||
await frame.goto(internalMetadata, origin, { timeout: 0 });
|
||||
@ -577,10 +583,10 @@ export abstract class BrowserContext extends SdkObject {
|
||||
// as a user-visible page.
|
||||
isServerSide: false,
|
||||
});
|
||||
await page.setServerRequestInterceptor(handler => {
|
||||
handler.fulfill({ body: '<html></html>' }).catch(() => {});
|
||||
return true;
|
||||
});
|
||||
const interceptor = (route: network.Route) => {
|
||||
route.fulfill({ body: '<html></html>' }).catch(() => {});
|
||||
};
|
||||
await page.addRequestInterceptor(interceptor, 'prepend');
|
||||
|
||||
for (const origin of new Set([...oldOrigins, ...newOrigins.keys()])) {
|
||||
const frame = page.mainFrame();
|
||||
@ -588,7 +594,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||
await frame.resetStorageForCurrentOriginBestEffort(newOrigins.get(origin));
|
||||
}
|
||||
|
||||
await page.setServerRequestInterceptor(undefined);
|
||||
await page.removeRequestInterceptor(interceptor);
|
||||
|
||||
this._origins = new Set([...newOrigins.keys()]);
|
||||
// It is safe to not restore the URL to about:blank since we are doing it in Page::resetForReuse.
|
||||
@ -612,10 +618,9 @@ export abstract class BrowserContext extends SdkObject {
|
||||
if (state.origins && state.origins.length) {
|
||||
const internalMetadata = serverSideCallMetadata();
|
||||
const page = await this.newPage(internalMetadata);
|
||||
await page.setServerRequestInterceptor(handler => {
|
||||
handler.fulfill({ body: '<html></html>' }).catch(() => {});
|
||||
return true;
|
||||
});
|
||||
await page.addRequestInterceptor(route => {
|
||||
route.fulfill({ body: '<html></html>' }).catch(() => {});
|
||||
}, 'prepend');
|
||||
for (const originState of state.origins) {
|
||||
const frame = page.mainFrame();
|
||||
await frame.goto(metadata, originState.origin, { timeout: 0 });
|
||||
@ -667,9 +672,8 @@ export abstract class BrowserContext extends SdkObject {
|
||||
this._routesInFlight.delete(route);
|
||||
}
|
||||
|
||||
async _cancelAllRoutesInFlight() {
|
||||
await Promise.all([...this._routesInFlight].map(r => r.abort())).catch(() => {});
|
||||
this._routesInFlight.clear();
|
||||
async notifyRoutesInFlightAboutRemovedHandler(handler: network.RouteHandler): Promise<void> {
|
||||
await Promise.all([...this._routesInFlight].map(route => route.removeHandler(handler)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,7 +84,7 @@ export class CRServiceWorker extends Worker {
|
||||
}
|
||||
|
||||
needsRequestInterception(): boolean {
|
||||
return this._isNetworkInspectionEnabled() && !!this.browserContext._requestInterceptor;
|
||||
return this._isNetworkInspectionEnabled() && this.browserContext.requestInterceptors.length > 0;
|
||||
}
|
||||
|
||||
reportRequestFinished(request: network.Request, response: network.Response | null) {
|
||||
@ -101,12 +101,8 @@ export class CRServiceWorker extends Worker {
|
||||
|
||||
requestStarted(request: network.Request, route?: network.RouteDelegate) {
|
||||
this.browserContext.emit(BrowserContext.Events.Request, request);
|
||||
if (route) {
|
||||
const r = new network.Route(request, route);
|
||||
if (this.browserContext._requestInterceptor?.(r, request))
|
||||
return;
|
||||
r.continue({ isFallback: true }).catch(() => {});
|
||||
}
|
||||
if (route)
|
||||
new network.Route(request, route).handle(this.browserContext.requestInterceptors);
|
||||
}
|
||||
|
||||
private _isNetworkInspectionEnabled(): boolean {
|
||||
|
@ -39,7 +39,7 @@ import type { Artifact } from '../artifact';
|
||||
import type { ConsoleMessage } from '../console';
|
||||
import type { Dialog } from '../dialog';
|
||||
import type { CallMetadata } from '../instrumentation';
|
||||
import type { Request, Response } from '../network';
|
||||
import type { Request, Response, RouteHandler } from '../network';
|
||||
import type { InitScript, Page, PageBinding } from '../page';
|
||||
import type { DispatcherScope } from './dispatcher';
|
||||
import type { FrameDispatcher } from './frameDispatcher';
|
||||
@ -55,6 +55,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||
private _initScritps: InitScript[] = [];
|
||||
private _dialogHandler: (dialog: Dialog) => boolean;
|
||||
private _clockPaused = false;
|
||||
private _requestInterceptor: RouteHandler;
|
||||
private _interceptionUrlMatchers: (string | RegExp)[] = [];
|
||||
|
||||
static from(parentScope: DispatcherScope, context: BrowserContext): BrowserContextDispatcher {
|
||||
const result = parentScope.connection.existingDispatcher<BrowserContextDispatcher>(context);
|
||||
@ -76,9 +78,21 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||
this.adopt(requestContext);
|
||||
this.adopt(tracing);
|
||||
|
||||
this._requestInterceptor = (route, request) => {
|
||||
const matchesSome = this._interceptionUrlMatchers.some(urlMatch => urlMatches(this._context._options.baseURL, request.url(), urlMatch));
|
||||
// If there is already a dispatcher, that means we've already routed this request through page.
|
||||
// Client expects a single `route` event, either on the page or on the context, so we can just fallback here.
|
||||
const routeDispatcher = this.connection.existingDispatcher<RouteDispatcher>(route);
|
||||
if (!matchesSome || routeDispatcher) {
|
||||
route.continue({ isFallback: true }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
this._dispatchEvent('route', { route: new RouteDispatcher(RequestDispatcher.from(this, request), route) });
|
||||
};
|
||||
|
||||
this._context = context;
|
||||
// Note: when launching persistent context, dispatcher is created very late,
|
||||
// so we can already have pages, videos and everything else.
|
||||
// Note: when launching persistent context, or connecting to an existing browser,
|
||||
// dispatcher is created very late, so we can already have pages, videos and everything else.
|
||||
|
||||
const onVideo = (artifact: Artifact) => {
|
||||
// Note: Video must outlive Page and BrowserContext, so that client can saveAs it
|
||||
@ -276,18 +290,18 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||
}
|
||||
|
||||
async setNetworkInterceptionPatterns(params: channels.BrowserContextSetNetworkInterceptionPatternsParams): Promise<void> {
|
||||
const hadMatchers = this._interceptionUrlMatchers.length > 0;
|
||||
if (!params.patterns.length) {
|
||||
await this._context.setRequestInterceptor(undefined);
|
||||
return;
|
||||
// Note: it is important to remove the interceptor when there are no patterns,
|
||||
// because that disables the slow-path interception in the browser itself.
|
||||
if (hadMatchers)
|
||||
await this._context.removeRequestInterceptor(this._requestInterceptor);
|
||||
this._interceptionUrlMatchers = [];
|
||||
} else {
|
||||
this._interceptionUrlMatchers = params.patterns.map(pattern => pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags!) : pattern.glob!);
|
||||
if (!hadMatchers)
|
||||
await this._context.addRequestInterceptor(this._requestInterceptor);
|
||||
}
|
||||
const urlMatchers = params.patterns.map(pattern => pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags!) : pattern.glob!);
|
||||
await this._context.setRequestInterceptor((route, request) => {
|
||||
const matchesSome = urlMatchers.some(urlMatch => urlMatches(this._context._options.baseURL, request.url(), urlMatch));
|
||||
if (!matchesSome)
|
||||
return false;
|
||||
this._dispatchEvent('route', { route: RouteDispatcher.from(RequestDispatcher.from(this, request), route) });
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async setWebSocketInterceptionPatterns(params: channels.PageSetWebSocketInterceptionPatternsParams, metadata: CallMetadata): Promise<void> {
|
||||
@ -383,8 +397,11 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||
// Avoid protocol calls for the closed context.
|
||||
if (this._context.isClosingOrClosed())
|
||||
return;
|
||||
|
||||
// Cleanup properly and leave the page in a good state. Other clients may still connect and use it.
|
||||
this._context.dialogManager.removeDialogHandler(this._dialogHandler);
|
||||
this._context.setRequestInterceptor(undefined).catch(() => {});
|
||||
this._interceptionUrlMatchers = [];
|
||||
this._context.removeRequestInterceptor(this._requestInterceptor).catch(() => {});
|
||||
this._context.removeExposedBindings(this._bindings).catch(() => {});
|
||||
this._bindings = [];
|
||||
this._context.removeInitScripts(this._initScritps).catch(() => {});
|
||||
|
@ -123,19 +123,23 @@ export class ResponseDispatcher extends Dispatcher<Response, channels.ResponseCh
|
||||
export class RouteDispatcher extends Dispatcher<Route, channels.RouteChannel, RequestDispatcher> implements channels.RouteChannel {
|
||||
_type_Route = true;
|
||||
|
||||
static from(scope: RequestDispatcher, route: Route): RouteDispatcher {
|
||||
const result = scope.connection.existingDispatcher<RouteDispatcher>(route);
|
||||
return result || new RouteDispatcher(scope, route);
|
||||
}
|
||||
private _handled = false;
|
||||
|
||||
private constructor(scope: RequestDispatcher, route: Route) {
|
||||
constructor(scope: RequestDispatcher, route: Route) {
|
||||
super(scope, route, 'Route', {
|
||||
// Context route can point to a non-reported request, so we send the request in the initializer.
|
||||
request: scope
|
||||
});
|
||||
}
|
||||
|
||||
private _checkNotHandled() {
|
||||
if (this._handled)
|
||||
throw new Error('Route is already handled!');
|
||||
this._handled = true;
|
||||
}
|
||||
|
||||
async continue(params: channels.RouteContinueParams, metadata: CallMetadata): Promise<channels.RouteContinueResult> {
|
||||
this._checkNotHandled();
|
||||
await this._object.continue({
|
||||
url: params.url,
|
||||
method: params.method,
|
||||
@ -146,14 +150,17 @@ export class RouteDispatcher extends Dispatcher<Route, channels.RouteChannel, Re
|
||||
}
|
||||
|
||||
async fulfill(params: channels.RouteFulfillParams, metadata: CallMetadata): Promise<void> {
|
||||
this._checkNotHandled();
|
||||
await this._object.fulfill(params);
|
||||
}
|
||||
|
||||
async abort(params: channels.RouteAbortParams, metadata: CallMetadata): Promise<void> {
|
||||
this._checkNotHandled();
|
||||
await this._object.abort(params.errorCode || 'failed');
|
||||
}
|
||||
|
||||
async redirectNavigationRequest(params: channels.RouteRedirectNavigationRequestParams): Promise<void> {
|
||||
this._checkNotHandled();
|
||||
await this._object.redirectNavigationRequest(params.url);
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ import type { CallMetadata } from '../instrumentation';
|
||||
import type { JSHandle } from '../javascript';
|
||||
import type { BrowserContextDispatcher } from './browserContextDispatcher';
|
||||
import type { Frame } from '../frames';
|
||||
import type { RouteHandler } from '../network';
|
||||
import type { InitScript, PageBinding } from '../page';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
@ -48,6 +49,8 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
|
||||
_webSocketInterceptionPatterns: channels.PageSetWebSocketInterceptionPatternsParams['patterns'] = [];
|
||||
private _bindings: PageBinding[] = [];
|
||||
private _initScripts: InitScript[] = [];
|
||||
private _requestInterceptor: RouteHandler;
|
||||
private _interceptionUrlMatchers: (string | RegExp)[] = [];
|
||||
private _locatorHandlers = new Set<number>();
|
||||
private _jsCoverageActive = false;
|
||||
private _cssCoverageActive = false;
|
||||
@ -80,6 +83,15 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
|
||||
this.adopt(mainFrame);
|
||||
|
||||
this._page = page;
|
||||
this._requestInterceptor = (route, request) => {
|
||||
const matchesSome = this._interceptionUrlMatchers.some(urlMatch => urlMatches(this._page.browserContext._options.baseURL, request.url(), urlMatch));
|
||||
if (!matchesSome) {
|
||||
route.continue({ isFallback: true }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
this._dispatchEvent('route', { route: new RouteDispatcher(RequestDispatcher.from(this.parentScope(), request), route) });
|
||||
};
|
||||
|
||||
this.addObjectListener(Page.Events.Close, () => {
|
||||
this._dispatchEvent('close');
|
||||
this._dispose();
|
||||
@ -179,18 +191,18 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
|
||||
}
|
||||
|
||||
async setNetworkInterceptionPatterns(params: channels.PageSetNetworkInterceptionPatternsParams, metadata: CallMetadata): Promise<void> {
|
||||
const hadMatchers = this._interceptionUrlMatchers.length > 0;
|
||||
if (!params.patterns.length) {
|
||||
await this._page.setClientRequestInterceptor(undefined);
|
||||
return;
|
||||
// Note: it is important to remove the interceptor when there are no patterns,
|
||||
// because that disables the slow-path interception in the browser itself.
|
||||
if (hadMatchers)
|
||||
await this._page.removeRequestInterceptor(this._requestInterceptor);
|
||||
this._interceptionUrlMatchers = [];
|
||||
} else {
|
||||
this._interceptionUrlMatchers = params.patterns.map(pattern => pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags!) : pattern.glob!);
|
||||
if (!hadMatchers)
|
||||
await this._page.addRequestInterceptor(this._requestInterceptor);
|
||||
}
|
||||
const urlMatchers = params.patterns.map(pattern => pattern.regexSource ? new RegExp(pattern.regexSource, pattern.regexFlags!) : pattern.glob!);
|
||||
await this._page.setClientRequestInterceptor((route, request) => {
|
||||
const matchesSome = urlMatchers.some(urlMatch => urlMatches(this._page.browserContext._options.baseURL, request.url(), urlMatch));
|
||||
if (!matchesSome)
|
||||
return false;
|
||||
this._dispatchEvent('route', { route: RouteDispatcher.from(RequestDispatcher.from(this.parentScope(), request), route) });
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async setWebSocketInterceptionPatterns(params: channels.PageSetWebSocketInterceptionPatternsParams, metadata: CallMetadata): Promise<void> {
|
||||
@ -343,7 +355,10 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
|
||||
// Avoid protocol calls for the closed page.
|
||||
if (this._page.isClosedOrClosingOrCrashed())
|
||||
return;
|
||||
this._page.setClientRequestInterceptor(undefined).catch(() => {});
|
||||
|
||||
// Cleanup properly and leave the page in a good state. Other clients may still connect and use it.
|
||||
this._interceptionUrlMatchers = [];
|
||||
this._page.removeRequestInterceptor(this._requestInterceptor).catch(() => {});
|
||||
this._page.removeExposedBindings(this._bindings).catch(() => {});
|
||||
this._bindings = [];
|
||||
this._page.removeInitScripts(this._initScripts).catch(() => {});
|
||||
|
@ -387,8 +387,8 @@ export class FFBrowserContext extends BrowserContext {
|
||||
|
||||
async doUpdateRequestInterception(): Promise<void> {
|
||||
await Promise.all([
|
||||
this._browser.session.send('Browser.setRequestInterception', { browserContextId: this._browserContextId, enabled: !!this._requestInterceptor }),
|
||||
this._browser.session.send('Browser.setCacheDisabled', { browserContextId: this._browserContextId, cacheDisabled: !!this._requestInterceptor }),
|
||||
this._browser.session.send('Browser.setRequestInterception', { browserContextId: this._browserContextId, enabled: this.requestInterceptors.length > 0 }),
|
||||
this._browser.session.send('Browser.setCacheDisabled', { browserContextId: this._browserContextId, cacheDisabled: this.requestInterceptors.length > 0 }),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -301,16 +301,8 @@ export class FrameManager {
|
||||
return;
|
||||
}
|
||||
this._page.emitOnContext(BrowserContext.Events.Request, request);
|
||||
if (route) {
|
||||
const r = new network.Route(request, route);
|
||||
if (this._page.serverRequestInterceptor?.(r, request))
|
||||
return;
|
||||
if (this._page.clientRequestInterceptor?.(r, request))
|
||||
return;
|
||||
if (this._page.browserContext._requestInterceptor?.(r, request))
|
||||
return;
|
||||
r.continue({ isFallback: true }).catch(() => {});
|
||||
}
|
||||
if (route)
|
||||
new network.Route(request, route).handle([...this._page.requestInterceptors, ...this._page.browserContext.requestInterceptors]);
|
||||
}
|
||||
|
||||
requestReceivedResponse(response: network.Response) {
|
||||
|
@ -292,7 +292,7 @@ export class HarTracer {
|
||||
}
|
||||
|
||||
private _recordRequestOverrides(harEntry: har.Entry, request: network.Request) {
|
||||
if (!request._hasOverrides() || !this._options.recordRequestOverrides)
|
||||
if (!request.overrides() || !this._options.recordRequestOverrides)
|
||||
return;
|
||||
harEntry.request.method = request.method();
|
||||
harEntry.request.url = request.url();
|
||||
|
@ -137,9 +137,10 @@ export class Request extends SdkObject {
|
||||
this._waitForResponsePromise.resolve(null);
|
||||
}
|
||||
|
||||
_setOverrides(overrides: types.NormalizedContinueOverrides) {
|
||||
this._overrides = overrides;
|
||||
_applyOverrides(overrides: types.NormalizedContinueOverrides) {
|
||||
this._overrides = { ...this._overrides, ...overrides };
|
||||
this._updateHeadersMap();
|
||||
return this._overrides;
|
||||
}
|
||||
|
||||
private _updateHeadersMap() {
|
||||
@ -147,8 +148,8 @@ export class Request extends SdkObject {
|
||||
this._headersMap.set(name.toLowerCase(), value);
|
||||
}
|
||||
|
||||
_hasOverrides() {
|
||||
return !!this._overrides;
|
||||
overrides() {
|
||||
return this._overrides;
|
||||
}
|
||||
|
||||
url(): string {
|
||||
@ -251,6 +252,8 @@ export class Route extends SdkObject {
|
||||
private readonly _request: Request;
|
||||
private readonly _delegate: RouteDelegate;
|
||||
private _handled = false;
|
||||
private _currentHandler: RouteHandler | undefined;
|
||||
private _futureHandlers: RouteHandler[] = [];
|
||||
|
||||
constructor(request: Request, delegate: RouteDelegate) {
|
||||
super(request._frame || request._context, 'route');
|
||||
@ -259,6 +262,19 @@ export class Route extends SdkObject {
|
||||
this._request._context.addRouteInFlight(this);
|
||||
}
|
||||
|
||||
handle(handlers: RouteHandler[]) {
|
||||
this._futureHandlers = [...handlers];
|
||||
this.continue({ isFallback: true }).catch(() => {});
|
||||
}
|
||||
|
||||
async removeHandler(handler: RouteHandler) {
|
||||
this._futureHandlers = this._futureHandlers.filter(h => h !== handler);
|
||||
if (handler === this._currentHandler) {
|
||||
await this.continue({ isFallback: true }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
request(): Request {
|
||||
return this._request;
|
||||
}
|
||||
@ -274,6 +290,7 @@ export class Route extends SdkObject {
|
||||
this._startHandling();
|
||||
assert(this._request.isNavigationRequest());
|
||||
this._request.frame()!.redirectNavigation(url, this._request._documentId!, this._request.headerValue('referer'));
|
||||
this._endHandling();
|
||||
}
|
||||
|
||||
async fulfill(overrides: channels.RouteFulfillParams) {
|
||||
@ -322,7 +339,6 @@ export class Route extends SdkObject {
|
||||
}
|
||||
|
||||
async continue(overrides: types.NormalizedContinueOverrides) {
|
||||
this._startHandling();
|
||||
if (overrides.url) {
|
||||
const newUrl = new URL(overrides.url);
|
||||
const oldUrl = new URL(this._request.url());
|
||||
@ -331,9 +347,18 @@ export class Route extends SdkObject {
|
||||
}
|
||||
if (overrides.headers)
|
||||
overrides.headers = overrides.headers?.filter(header => header.name.toLowerCase() !== 'cookie');
|
||||
this._request._setOverrides(overrides);
|
||||
overrides = this._request._applyOverrides(overrides);
|
||||
|
||||
const nextHandler = this._futureHandlers.shift();
|
||||
if (nextHandler) {
|
||||
this._currentHandler = nextHandler;
|
||||
nextHandler(this, this._request);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!overrides.isFallback)
|
||||
this._request._context.emit(BrowserContext.Events.RequestContinued, this._request);
|
||||
this._startHandling();
|
||||
await this._delegate.continue(overrides);
|
||||
this._endHandling();
|
||||
}
|
||||
@ -341,14 +366,17 @@ export class Route extends SdkObject {
|
||||
private _startHandling() {
|
||||
assert(!this._handled, 'Route is already handled!');
|
||||
this._handled = true;
|
||||
this._currentHandler = undefined;
|
||||
}
|
||||
|
||||
private _endHandling() {
|
||||
this._futureHandlers = [];
|
||||
this._currentHandler = undefined;
|
||||
this._request._context.removeRouteInFlight(this);
|
||||
}
|
||||
}
|
||||
|
||||
export type RouteHandler = (route: Route, request: Request) => boolean;
|
||||
export type RouteHandler = (route: Route, request: Request) => void;
|
||||
|
||||
type GetResponseBodyCallback = () => Promise<Buffer>;
|
||||
|
||||
|
@ -160,8 +160,7 @@ export class Page extends SdkObject {
|
||||
private _workers = new Map<string, Worker>();
|
||||
readonly pdf: ((options: channels.PagePdfParams) => Promise<Buffer>) | undefined;
|
||||
readonly coverage: any;
|
||||
clientRequestInterceptor: network.RouteHandler | undefined;
|
||||
serverRequestInterceptor: network.RouteHandler | undefined;
|
||||
readonly requestInterceptors: network.RouteHandler[] = [];
|
||||
video: Artifact | null = null;
|
||||
private _opener: Page | undefined;
|
||||
private _isServerSideOnly = false;
|
||||
@ -258,8 +257,6 @@ export class Page extends SdkObject {
|
||||
async resetForReuse(metadata: CallMetadata) {
|
||||
this._locatorHandlers.clear();
|
||||
|
||||
await this.setClientRequestInterceptor(undefined);
|
||||
await this.setServerRequestInterceptor(undefined);
|
||||
// Re-navigate once init scripts are gone.
|
||||
// TODO: we should have a timeout for `resetForReuse`.
|
||||
await this.mainFrame().goto(metadata, 'about:blank', { timeout: 0 });
|
||||
@ -571,16 +568,23 @@ export class Page extends SdkObject {
|
||||
}
|
||||
|
||||
needsRequestInterception(): boolean {
|
||||
return !!this.clientRequestInterceptor || !!this.serverRequestInterceptor || !!this.browserContext._requestInterceptor;
|
||||
return this.requestInterceptors.length > 0 || this.browserContext.requestInterceptors.length > 0;
|
||||
}
|
||||
|
||||
async setClientRequestInterceptor(handler: network.RouteHandler | undefined): Promise<void> {
|
||||
this.clientRequestInterceptor = handler;
|
||||
async addRequestInterceptor(handler: network.RouteHandler, prepend?: 'prepend'): Promise<void> {
|
||||
if (prepend)
|
||||
this.requestInterceptors.unshift(handler);
|
||||
else
|
||||
this.requestInterceptors.push(handler);
|
||||
await this.delegate.updateRequestInterception();
|
||||
}
|
||||
|
||||
async setServerRequestInterceptor(handler: network.RouteHandler | undefined): Promise<void> {
|
||||
this.serverRequestInterceptor = handler;
|
||||
async removeRequestInterceptor(handler: network.RouteHandler): Promise<void> {
|
||||
const index = this.requestInterceptors.indexOf(handler);
|
||||
if (index === -1)
|
||||
return;
|
||||
this.requestInterceptors.splice(index, 1);
|
||||
await this.browserContext.notifyRoutesInFlightAboutRemovedHandler(handler);
|
||||
await this.delegate.updateRequestInterception();
|
||||
}
|
||||
|
||||
|
@ -63,9 +63,11 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
|
||||
private async _init() {
|
||||
await syncLocalStorageWithSettings(this._page, 'recorder');
|
||||
|
||||
await this._page.setServerRequestInterceptor(route => {
|
||||
if (!route.request().url().startsWith('https://playwright/'))
|
||||
return false;
|
||||
await this._page.addRequestInterceptor(route => {
|
||||
if (!route.request().url().startsWith('https://playwright/')) {
|
||||
route.continue({ isFallback: true }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
const uri = route.request().url().substring('https://playwright/'.length);
|
||||
const file = require.resolve('../../vite/recorder/' + uri);
|
||||
@ -79,7 +81,6 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
|
||||
isBase64: true
|
||||
}).catch(() => {});
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
await this._page.exposeBinding('dispatch', false, (_, data: any) => this.emit('event', data));
|
||||
|
@ -50,7 +50,7 @@ const test = playwrightTest.extend<ExtraFixtures>({
|
||||
});
|
||||
|
||||
test.slow(true, 'All connect tests are slow');
|
||||
test.skip(({ mode }) => mode.startsWith('service'));
|
||||
test.skip(({ mode }) => mode !== 'default');
|
||||
|
||||
async function disconnect(page: Page) {
|
||||
await page.context().browser().close();
|
||||
@ -161,6 +161,74 @@ test('last emulateMedia wins', async ({ twoPages }) => {
|
||||
expect(await pageA.evaluate(() => window.matchMedia('print').matches)).toBe(false);
|
||||
});
|
||||
|
||||
test('should chain routes', async ({ twoPages, server }) => {
|
||||
const { pageA, pageB } = twoPages;
|
||||
|
||||
server.setRoute('/foo', (req, res) => res.writeHead(200, { 'Content-Type': 'text/html' }).end('<div>server-foo</div>'));
|
||||
server.setRoute('/bar', (req, res) => res.writeHead(200, { 'Content-Type': 'text/html' }).end('<div>server-bar</div>'));
|
||||
server.setRoute('/qux', (req, res) => res.writeHead(200, { 'Content-Type': 'text/html' }).end('<div>server-qux</div>'));
|
||||
|
||||
let stall = false;
|
||||
let stallCallback;
|
||||
const stallPromise = new Promise(f => stallCallback = f);
|
||||
await pageA.route('**/foo', async route => {
|
||||
if (stall)
|
||||
stallCallback();
|
||||
else
|
||||
await route.fallback();
|
||||
});
|
||||
await pageA.route('**/bar', async route => {
|
||||
await route.fulfill({ body: '<div>intercepted-bar</div>', contentType: 'text/html' });
|
||||
});
|
||||
|
||||
await pageB.route('**/foo', async route => {
|
||||
await route.fulfill({ body: '<div>intercepted2-foo</div>', contentType: 'text/html' });
|
||||
});
|
||||
await pageB.route('**/bar', async route => {
|
||||
await route.fulfill({ body: '<div>intercepted2-bar</div>', contentType: 'text/html' });
|
||||
});
|
||||
await pageB.route('**/qux', async route => {
|
||||
await route.fulfill({ body: '<div>intercepted2-qux</div>', contentType: 'text/html' });
|
||||
});
|
||||
|
||||
await pageA.goto(server.PREFIX + '/foo');
|
||||
await expect(pageB.locator('div')).toHaveText('intercepted2-foo');
|
||||
|
||||
await pageA.goto(server.PREFIX + '/bar');
|
||||
await expect(pageB.locator('div')).toHaveText('intercepted-bar');
|
||||
|
||||
await pageA.goto(server.PREFIX + '/qux');
|
||||
await expect(pageB.locator('div')).toHaveText('intercepted2-qux');
|
||||
|
||||
stall = true;
|
||||
const gotoPromise = pageB.goto(server.PREFIX + '/foo');
|
||||
await stallPromise;
|
||||
await pageA.context().browser().close();
|
||||
|
||||
await gotoPromise;
|
||||
await expect(pageB.locator('div')).toHaveText('intercepted2-foo');
|
||||
|
||||
await pageB.goto(server.PREFIX + '/bar');
|
||||
await expect(pageB.locator('div')).toHaveText('intercepted2-bar');
|
||||
});
|
||||
|
||||
test.fixme('should chain routes with changed url', async ({ twoPages, server }) => {
|
||||
const { pageA, pageB } = twoPages;
|
||||
|
||||
server.setRoute('/foo', (req, res) => res.writeHead(200, { 'Content-Type': 'text/html' }).end('<div>server-foo</div>'));
|
||||
server.setRoute('/baz', (req, res) => res.writeHead(200, { 'Content-Type': 'text/html' }).end('<div>server-baz</div>'));
|
||||
|
||||
await pageA.route('**/foo', async route => {
|
||||
await route.fallback({ url: server.PREFIX + '/baz' });
|
||||
});
|
||||
await pageB.route('**/baz', async route => {
|
||||
await route.fulfill({ body: '<div>intercepted2-baz</div>', contentType: 'text/html' });
|
||||
});
|
||||
|
||||
await pageA.goto(server.PREFIX + '/foo');
|
||||
await expect(pageB.locator('div')).toHaveText('intercepted2-baz');
|
||||
});
|
||||
|
||||
test('should remove exposed bindings upon disconnect', async ({ twoPages }) => {
|
||||
const { pageA, pageB } = twoPages;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user