mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
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();
|
||||
}
|
||||
|
||||
_onRoute(route: network.Route, request: network.Request) {
|
||||
for (const routeHandler of this._routes) {
|
||||
if (!routeHandler.matches(request.url()))
|
||||
continue;
|
||||
// Immediately deactivate based on |times|.
|
||||
async _onRoute(route: network.Route, request: network.Request) {
|
||||
const routes = this._routes.filter(r => r.matches(request.url()));
|
||||
|
||||
const nextRoute = async () => {
|
||||
const routeHandler = routes.shift();
|
||||
if (!routeHandler) {
|
||||
await route._finalContinue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (routeHandler.willExpire())
|
||||
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)
|
||||
await this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
|
||||
}
|
||||
})();
|
||||
await new Promise<void>(f => {
|
||||
routeHandler.handle(route, request, async done => {
|
||||
if (!done)
|
||||
await nextRoute();
|
||||
f();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// There is no chaining, first handler wins.
|
||||
return;
|
||||
}
|
||||
await nextRoute();
|
||||
|
||||
// it can race with BrowserContext.close() which then throws since its closed
|
||||
route._internalContinue();
|
||||
if (!this._routes.length)
|
||||
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
|
||||
}
|
||||
|
||||
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 {
|
||||
private _pendingContinueOverrides: OverridesForContinue | undefined;
|
||||
private _routeChain: ((done: boolean) => Promise<void>) | null = null;
|
||||
|
||||
static from(route: channels.RouteChannel): Route {
|
||||
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) {
|
||||
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 } = {}) {
|
||||
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 { 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 } = {}) {
|
||||
await this._continue(options);
|
||||
async continue(options: OverridesForContinue = {}) {
|
||||
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 } = {}) {
|
||||
await this._continue(options, true).catch(() => {});
|
||||
async _followChain(done: boolean) {
|
||||
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 () => {
|
||||
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
|
||||
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,
|
||||
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 = {
|
||||
startTime: number;
|
||||
@ -548,9 +574,10 @@ export class RouteHandler {
|
||||
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;
|
||||
return this.handler(route, request);
|
||||
route._startHandling(routeChain);
|
||||
this.handler(route, request);
|
||||
}
|
||||
|
||||
public willExpire(): boolean {
|
||||
|
||||
@ -179,28 +179,31 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||
this.emit(Events.Page.FrameDetached, frame);
|
||||
}
|
||||
|
||||
private _onRoute(route: Route, request: Request) {
|
||||
for (const routeHandler of this._routes) {
|
||||
if (!routeHandler.matches(request.url()))
|
||||
continue;
|
||||
// Immediately deactivate based on |times|.
|
||||
private async _onRoute(route: Route, request: Request) {
|
||||
const routes = this._routes.filter(r => r.matches(request.url()));
|
||||
|
||||
const nextRoute = async () => {
|
||||
const routeHandler = routes.shift();
|
||||
if (!routeHandler) {
|
||||
await this._browserContext._onRoute(route, request);
|
||||
return;
|
||||
}
|
||||
|
||||
if (routeHandler.willExpire())
|
||||
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)
|
||||
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
|
||||
}
|
||||
})();
|
||||
await new Promise<void>(f => {
|
||||
routeHandler.handle(route, request, async done => {
|
||||
if (!done)
|
||||
await nextRoute();
|
||||
f();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// There is no chaining, first handler wins.
|
||||
return;
|
||||
}
|
||||
this._browserContext._onRoute(route, request);
|
||||
await nextRoute();
|
||||
if (!this._routes.length)
|
||||
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
|
||||
}
|
||||
|
||||
async _onBinding(bindingCall: BindingCall) {
|
||||
|
||||
@ -63,12 +63,12 @@ it('should unroute', async ({ browser, server }) => {
|
||||
};
|
||||
await context.route('**/empty.html', handler4);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(intercepted).toEqual([4]);
|
||||
expect(intercepted).toEqual([4, 3, 2, 1]);
|
||||
|
||||
intercepted = [];
|
||||
await context.unroute('**/empty.html', handler4);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(intercepted).toEqual([3]);
|
||||
expect(intercepted).toEqual([3, 2, 1]);
|
||||
|
||||
intercepted = [];
|
||||
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();
|
||||
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.goto(server.EMPTY_PAGE);
|
||||
expect(intercepted).toEqual([4]);
|
||||
expect(intercepted).toEqual([4, 3, 2, 1]);
|
||||
|
||||
intercepted = [];
|
||||
await page.unroute('**/empty.html', handler4);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(intercepted).toEqual([3]);
|
||||
expect(intercepted).toEqual([3, 2, 1]);
|
||||
|
||||
intercepted = [];
|
||||
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!');
|
||||
});
|
||||
}
|
||||
|
||||
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…
x
Reference in New Issue
Block a user