fix(route): support route w/ async handler & times (#14317)

This commit is contained in:
Pavel Feldman 2022-05-21 21:55:46 -07:00 committed by GitHub
parent abed166dd4
commit a1324bd935
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 64 additions and 23 deletions

View File

@ -145,19 +145,26 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
_onRoute(route: network.Route, request: network.Request) { _onRoute(route: network.Route, request: network.Request) {
for (const routeHandler of this._routes) { for (const routeHandler of this._routes) {
if (routeHandler.matches(request.url())) { if (!routeHandler.matches(request.url()))
try { continue;
routeHandler.handle(route, request); // Immediately deactivate based on |times|.
} finally { if (routeHandler.willExpire())
if (!routeHandler.isActive()) {
this._routes.splice(this._routes.indexOf(routeHandler), 1); this._routes.splice(this._routes.indexOf(routeHandler), 1);
(async () => {
try {
// Let async callback work prior to disabling interception.
await routeHandler.handle(route, request);
} finally {
if (!this._routes.length) if (!this._routes.length)
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {}); await this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
}
} }
})();
// There is no chaining, first handler wins.
return; return;
} }
}
// it can race with BrowserContext.close() which then throws since its closed // it can race with BrowserContext.close() which then throws since its closed
route._internalContinue(); route._internalContinue();
} }

View File

@ -309,7 +309,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
} }
} }
export type RouteHandlerCallback = (route: Route, request: Request) => void; export type RouteHandlerCallback = (route: Route, request: Request) => void | Promise<void>;
export type ResourceTiming = { export type ResourceTiming = {
startTime: number; startTime: number;
@ -518,13 +518,13 @@ export class RouteHandler {
return urlMatches(this._baseURL, requestURL, this.url); return urlMatches(this._baseURL, requestURL, this.url);
} }
public handle(route: Route, request: Request): void { public handle(route: Route, request: Request): Promise<void> | void {
++this.handledCount; ++this.handledCount;
this.handler(route, request); return this.handler(route, request);
} }
public isActive(): boolean { public willExpire(): boolean {
return this.handledCount < this._times; return this.handledCount + 1 >= this._times;
} }
} }

View File

@ -180,19 +180,25 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
private _onRoute(route: Route, request: Request) { private _onRoute(route: Route, request: Request) {
for (const routeHandler of this._routes) { for (const routeHandler of this._routes) {
if (routeHandler.matches(request.url())) { if (!routeHandler.matches(request.url()))
try { continue;
routeHandler.handle(route, request); // Immediately deactivate based on |times|.
} finally { if (routeHandler.willExpire())
if (!routeHandler.isActive()) {
this._routes.splice(this._routes.indexOf(routeHandler), 1); this._routes.splice(this._routes.indexOf(routeHandler), 1);
(async () => {
try {
// Let async callback work prior to disabling interception.
await routeHandler.handle(route, request);
} finally {
if (!this._routes.length) if (!this._routes.length)
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {}); this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
} }
} })();
// There is no chaining, first handler wins.
return; return;
} }
}
this._browserContext._onRoute(route, request); this._browserContext._onRoute(route, request);
} }

View File

@ -211,6 +211,20 @@ it('should support the times parameter with route matching', async ({ context, p
expect(intercepted).toHaveLength(1); expect(intercepted).toHaveLength(1);
}); });
it('should support async handler w/ times', async ({ context, page, server }) => {
await context.route('**/empty.html', async route => {
await new Promise(f => setTimeout(f, 100));
route.fulfill({
body: '<html>intercepted</html>',
contentType: 'text/html'
});
}, { times: 1 });
await page.goto(server.EMPTY_PAGE);
await expect(page.locator('body')).toHaveText('intercepted');
await page.goto(server.EMPTY_PAGE);
await expect(page.locator('body')).not.toHaveText('intercepted');
});
it('should overwrite post body with empty string', async ({ context, server, page, browserName }) => { it('should overwrite post body with empty string', async ({ context, server, page, browserName }) => {
await context.route('**/empty.html', route => { await context.route('**/empty.html', route => {
route.continue({ route.continue({

View File

@ -772,6 +772,20 @@ it('should support the times parameter with route matching', async ({ page, serv
expect(intercepted).toHaveLength(1); expect(intercepted).toHaveLength(1);
}); });
it('should support async handler w/ times', async ({ page, server }) => {
await page.route('**/empty.html', async route => {
await new Promise(f => setTimeout(f, 100));
route.fulfill({
body: '<html>intercepted</html>',
contentType: 'text/html'
});
}, { times: 1 });
await page.goto(server.EMPTY_PAGE);
await expect(page.locator('body')).toHaveText('intercepted');
await page.goto(server.EMPTY_PAGE);
await expect(page.locator('body')).not.toHaveText('intercepted');
});
it('should contain raw request header', async ({ page, server }) => { it('should contain raw request header', async ({ page, server }) => {
let headers: any; let headers: any;
await page.route('**/*', async route => { await page.route('**/*', async route => {