feat(route): chain routes (#14771)
This commit is contained in:
parent
a98394033c
commit
7a568a2952
|
|
@ -143,30 +143,32 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
response._finishedPromise.resolve();
|
response._finishedPromise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRoute(route: network.Route, request: network.Request) {
|
async _onRoute(route: network.Route, request: network.Request) {
|
||||||
for (const routeHandler of this._routes) {
|
const routes = this._routes.filter(r => r.matches(request.url()));
|
||||||
if (!routeHandler.matches(request.url()))
|
|
||||||
continue;
|
const nextRoute = async () => {
|
||||||
// Immediately deactivate based on |times|.
|
const routeHandler = routes.shift();
|
||||||
|
if (!routeHandler) {
|
||||||
|
await route._finalContinue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (routeHandler.willExpire())
|
if (routeHandler.willExpire())
|
||||||
this._routes.splice(this._routes.indexOf(routeHandler), 1);
|
this._routes.splice(this._routes.indexOf(routeHandler), 1);
|
||||||
|
|
||||||
(async () => {
|
await new Promise<void>(f => {
|
||||||
try {
|
routeHandler.handle(route, request, async done => {
|
||||||
// Let async callback work prior to disabling interception.
|
if (!done)
|
||||||
await routeHandler.handle(route, request);
|
await nextRoute();
|
||||||
} finally {
|
f();
|
||||||
if (!this._routes.length)
|
});
|
||||||
await this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
|
});
|
||||||
}
|
};
|
||||||
})();
|
|
||||||
|
|
||||||
// There is no chaining, first handler wins.
|
await nextRoute();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// it can race with BrowserContext.close() which then throws since its closed
|
if (!this._routes.length)
|
||||||
route._internalContinue();
|
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onBinding(bindingCall: BindingCall) {
|
async _onBinding(bindingCall: BindingCall) {
|
||||||
|
|
|
||||||
|
|
@ -216,7 +216,17 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OverridesForContinue = {
|
||||||
|
url?: string;
|
||||||
|
method?: string;
|
||||||
|
headers?: Headers;
|
||||||
|
postData?: string | Buffer;
|
||||||
|
};
|
||||||
|
|
||||||
export class Route extends ChannelOwner<channels.RouteChannel> implements api.Route {
|
export class Route extends ChannelOwner<channels.RouteChannel> implements api.Route {
|
||||||
|
private _pendingContinueOverrides: OverridesForContinue | undefined;
|
||||||
|
private _routeChain: ((done: boolean) => Promise<void>) | null = null;
|
||||||
|
|
||||||
static from(route: channels.RouteChannel): Route {
|
static from(route: channels.RouteChannel): Route {
|
||||||
return (route as any)._object;
|
return (route as any)._object;
|
||||||
}
|
}
|
||||||
|
|
@ -240,11 +250,21 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_startHandling(routeChain: (done: boolean) => Promise<void>) {
|
||||||
|
this._routeChain = routeChain;
|
||||||
|
}
|
||||||
|
|
||||||
async abort(errorCode?: string) {
|
async abort(errorCode?: string) {
|
||||||
await this._raceWithPageClose(this._channel.abort({ errorCode }));
|
await this._raceWithPageClose(this._channel.abort({ errorCode }));
|
||||||
|
await this._followChain(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fulfill(options: { response?: api.APIResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string, har?: string } = {}) {
|
async fulfill(options: { response?: api.APIResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string, har?: string } = {}) {
|
||||||
|
await this._innerFulfill(options);
|
||||||
|
await this._followChain(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _innerFulfill(options: { response?: api.APIResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string, har?: string } = {}) {
|
||||||
let fetchResponseUid;
|
let fetchResponseUid;
|
||||||
let { status: statusOption, headers: headersOption, body } = options;
|
let { status: statusOption, headers: headersOption, body } = options;
|
||||||
|
|
||||||
|
|
@ -314,15 +334,21 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer } = {}) {
|
async continue(options: OverridesForContinue = {}) {
|
||||||
await this._continue(options);
|
if (!this._routeChain)
|
||||||
|
throw new Error('Route is already handled!');
|
||||||
|
this._pendingContinueOverrides = { ...this._pendingContinueOverrides, ...options };
|
||||||
|
await this._followChain(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _internalContinue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer } = {}) {
|
async _followChain(done: boolean) {
|
||||||
await this._continue(options, true).catch(() => {});
|
const chain = this._routeChain!;
|
||||||
|
this._routeChain = null;
|
||||||
|
await chain(done);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _continue(options: { url?: string, method?: string, headers?: Headers, postData?: string | Buffer }, isInternal?: boolean) {
|
async _finalContinue() {
|
||||||
|
const options = this._pendingContinueOverrides || {};
|
||||||
return await this._wrapApiCall(async () => {
|
return await this._wrapApiCall(async () => {
|
||||||
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
|
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
|
||||||
await this._raceWithPageClose(this._channel.continue({
|
await this._raceWithPageClose(this._channel.continue({
|
||||||
|
|
@ -331,11 +357,11 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
||||||
headers: options.headers ? headersObjectToArray(options.headers) : undefined,
|
headers: options.headers ? headersObjectToArray(options.headers) : undefined,
|
||||||
postData: postDataBuffer ? postDataBuffer.toString('base64') : undefined,
|
postData: postDataBuffer ? postDataBuffer.toString('base64') : undefined,
|
||||||
}));
|
}));
|
||||||
}, isInternal);
|
}, !this._pendingContinueOverrides);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RouteHandlerCallback = (route: Route, request: Request) => void | Promise<void>;
|
export type RouteHandlerCallback = (route: Route, request: Request) => void;
|
||||||
|
|
||||||
export type ResourceTiming = {
|
export type ResourceTiming = {
|
||||||
startTime: number;
|
startTime: number;
|
||||||
|
|
@ -548,9 +574,10 @@ export class RouteHandler {
|
||||||
return urlMatches(this._baseURL, requestURL, this.url);
|
return urlMatches(this._baseURL, requestURL, this.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public handle(route: Route, request: Request): Promise<void> | void {
|
public handle(route: Route, request: Request, routeChain: (done: boolean) => Promise<void>) {
|
||||||
++this.handledCount;
|
++this.handledCount;
|
||||||
return this.handler(route, request);
|
route._startHandling(routeChain);
|
||||||
|
this.handler(route, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public willExpire(): boolean {
|
public willExpire(): boolean {
|
||||||
|
|
|
||||||
|
|
@ -179,28 +179,31 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||||
this.emit(Events.Page.FrameDetached, frame);
|
this.emit(Events.Page.FrameDetached, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onRoute(route: Route, request: Request) {
|
private async _onRoute(route: Route, request: Request) {
|
||||||
for (const routeHandler of this._routes) {
|
const routes = this._routes.filter(r => r.matches(request.url()));
|
||||||
if (!routeHandler.matches(request.url()))
|
|
||||||
continue;
|
const nextRoute = async () => {
|
||||||
// Immediately deactivate based on |times|.
|
const routeHandler = routes.shift();
|
||||||
|
if (!routeHandler) {
|
||||||
|
await this._browserContext._onRoute(route, request);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (routeHandler.willExpire())
|
if (routeHandler.willExpire())
|
||||||
this._routes.splice(this._routes.indexOf(routeHandler), 1);
|
this._routes.splice(this._routes.indexOf(routeHandler), 1);
|
||||||
|
|
||||||
(async () => {
|
await new Promise<void>(f => {
|
||||||
try {
|
routeHandler.handle(route, request, async done => {
|
||||||
// Let async callback work prior to disabling interception.
|
if (!done)
|
||||||
await routeHandler.handle(route, request);
|
await nextRoute();
|
||||||
} finally {
|
f();
|
||||||
if (!this._routes.length)
|
});
|
||||||
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
|
});
|
||||||
}
|
};
|
||||||
})();
|
|
||||||
|
|
||||||
// There is no chaining, first handler wins.
|
await nextRoute();
|
||||||
return;
|
if (!this._routes.length)
|
||||||
}
|
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
|
||||||
this._browserContext._onRoute(route, request);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onBinding(bindingCall: BindingCall) {
|
async _onBinding(bindingCall: BindingCall) {
|
||||||
|
|
|
||||||
|
|
@ -63,12 +63,12 @@ it('should unroute', async ({ browser, server }) => {
|
||||||
};
|
};
|
||||||
await context.route('**/empty.html', handler4);
|
await context.route('**/empty.html', handler4);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
expect(intercepted).toEqual([4]);
|
expect(intercepted).toEqual([4, 3, 2, 1]);
|
||||||
|
|
||||||
intercepted = [];
|
intercepted = [];
|
||||||
await context.unroute('**/empty.html', handler4);
|
await context.unroute('**/empty.html', handler4);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
expect(intercepted).toEqual([3]);
|
expect(intercepted).toEqual([3, 2, 1]);
|
||||||
|
|
||||||
intercepted = [];
|
intercepted = [];
|
||||||
await context.unroute('**/empty.html');
|
await context.unroute('**/empty.html');
|
||||||
|
|
@ -249,3 +249,84 @@ it('should overwrite post body with empty string', async ({ context, server, pag
|
||||||
const body = (await req.postBody).toString();
|
const body = (await req.postBody).toString();
|
||||||
expect(body).toBe('');
|
expect(body).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should chain continue', async ({ context, page, server }) => {
|
||||||
|
const intercepted = [];
|
||||||
|
await context.route('**/empty.html', route => {
|
||||||
|
intercepted.push(1);
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
await context.route('**/empty.html', route => {
|
||||||
|
intercepted.push(2);
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
await context.route('**/empty.html', route => {
|
||||||
|
intercepted.push(3);
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
expect(intercepted).toEqual([3, 2, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not chain fulfill', async ({ context, page, server }) => {
|
||||||
|
let failed = false;
|
||||||
|
await context.route('**/empty.html', route => {
|
||||||
|
failed = true;
|
||||||
|
});
|
||||||
|
await context.route('**/empty.html', route => {
|
||||||
|
route.fulfill({ status: 200, body: 'fulfilled' });
|
||||||
|
});
|
||||||
|
await context.route('**/empty.html', route => {
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
const response = await page.goto(server.EMPTY_PAGE);
|
||||||
|
const body = await response.body();
|
||||||
|
expect(body.toString()).toEqual('fulfilled');
|
||||||
|
expect(failed).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not chain abort', async ({ context, page, server }) => {
|
||||||
|
let failed = false;
|
||||||
|
await context.route('**/empty.html', route => {
|
||||||
|
failed = true;
|
||||||
|
});
|
||||||
|
await context.route('**/empty.html', route => {
|
||||||
|
route.abort();
|
||||||
|
});
|
||||||
|
await context.route('**/empty.html', route => {
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
const e = await page.goto(server.EMPTY_PAGE).catch(e => e);
|
||||||
|
expect(e).toBeTruthy();
|
||||||
|
expect(failed).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should chain continue into page', async ({ context, page, server }) => {
|
||||||
|
const intercepted = [];
|
||||||
|
await context.route('**/empty.html', route => {
|
||||||
|
intercepted.push(1);
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
await context.route('**/empty.html', route => {
|
||||||
|
intercepted.push(2);
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
await context.route('**/empty.html', route => {
|
||||||
|
intercepted.push(3);
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
await page.route('**/empty.html', route => {
|
||||||
|
intercepted.push(4);
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
await page.route('**/empty.html', route => {
|
||||||
|
intercepted.push(5);
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
await page.route('**/empty.html', route => {
|
||||||
|
intercepted.push(6);
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
expect(intercepted).toEqual([6, 5, 4, 3, 2, 1]);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -59,12 +59,12 @@ it('should unroute', async ({ page, server }) => {
|
||||||
};
|
};
|
||||||
await page.route('**/empty.html', handler4);
|
await page.route('**/empty.html', handler4);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
expect(intercepted).toEqual([4]);
|
expect(intercepted).toEqual([4, 3, 2, 1]);
|
||||||
|
|
||||||
intercepted = [];
|
intercepted = [];
|
||||||
await page.unroute('**/empty.html', handler4);
|
await page.unroute('**/empty.html', handler4);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
expect(intercepted).toEqual([3]);
|
expect(intercepted).toEqual([3, 2, 1]);
|
||||||
|
|
||||||
intercepted = [];
|
intercepted = [];
|
||||||
await page.unroute('**/empty.html');
|
await page.unroute('**/empty.html');
|
||||||
|
|
@ -837,3 +837,80 @@ for (const method of ['fulfill', 'continue', 'abort'] as const) {
|
||||||
expect(e.message).toContain('Route is already handled!');
|
expect(e.message).toContain('Route is already handled!');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it('should chain continue', async ({ page, server }) => {
|
||||||
|
const intercepted = [];
|
||||||
|
await page.route('**/empty.html', route => {
|
||||||
|
intercepted.push(1);
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
await page.route('**/empty.html', route => {
|
||||||
|
intercepted.push(2);
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
await page.route('**/empty.html', route => {
|
||||||
|
intercepted.push(3);
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
expect(intercepted).toEqual([3, 2, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not chain fulfill', async ({ page, server }) => {
|
||||||
|
let failed = false;
|
||||||
|
await page.route('**/empty.html', route => {
|
||||||
|
failed = true;
|
||||||
|
});
|
||||||
|
await page.route('**/empty.html', route => {
|
||||||
|
route.fulfill({ status: 200, body: 'fulfilled' });
|
||||||
|
});
|
||||||
|
await page.route('**/empty.html', route => {
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
const response = await page.goto(server.EMPTY_PAGE);
|
||||||
|
const body = await response.body();
|
||||||
|
expect(body.toString()).toEqual('fulfilled');
|
||||||
|
expect(failed).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not chain abort', async ({ page, server }) => {
|
||||||
|
let failed = false;
|
||||||
|
await page.route('**/empty.html', route => {
|
||||||
|
failed = true;
|
||||||
|
});
|
||||||
|
await page.route('**/empty.html', route => {
|
||||||
|
route.abort();
|
||||||
|
});
|
||||||
|
await page.route('**/empty.html', route => {
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
const e = await page.goto(server.EMPTY_PAGE).catch(e => e);
|
||||||
|
expect(e).toBeTruthy();
|
||||||
|
expect(failed).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should continue after exception', async ({ page, server }) => {
|
||||||
|
await page.route('**/empty.html', route => {
|
||||||
|
route.continue();
|
||||||
|
});
|
||||||
|
await page.route('**/empty.html', async route => {
|
||||||
|
try {
|
||||||
|
await route.fulfill({ har: 'file', response: {} as any });
|
||||||
|
} catch (e) {
|
||||||
|
route.continue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should chain once', async ({ page, server }) => {
|
||||||
|
await page.route('**/empty.html', route => {
|
||||||
|
route.fulfill({ status: 200, body: 'fulfilled one' });
|
||||||
|
}, { times: 1 });
|
||||||
|
await page.route('**/empty.html', route => {
|
||||||
|
route.continue();
|
||||||
|
}, { times: 1 });
|
||||||
|
const response = await page.goto(server.EMPTY_PAGE);
|
||||||
|
const body = await response.body();
|
||||||
|
expect(body.toString()).toEqual('fulfilled one');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue