diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 690a1ed1e1..870fc8335d 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1021,6 +1021,11 @@ handler function to route the request. handler function to route the request. +### option: BrowserContext.route.times +- `times` <[int]> + +How often a route should be used. By default it will be used every time. + ## method: BrowserContext.serviceWorkers * langs: js, python - returns: <[Array]<[Worker]>> diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index aa66b9dd5d..e625f98ff0 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2561,6 +2561,11 @@ handler function to route the request. handler function to route the request. +### option: Page.route.times +- `times` <[int]> + +How often a route should be used. By default it will be used every time. + ## async method: Page.screenshot - returns: <[Buffer]> diff --git a/src/client/browserContext.ts b/src/client/browserContext.ts index a8b72abe56..a1da538548 100644 --- a/src/client/browserContext.ts +++ b/src/client/browserContext.ts @@ -21,7 +21,7 @@ import * as network from './network'; import * as channels from '../protocol/channels'; import fs from 'fs'; import { ChannelOwner } from './channelOwner'; -import { deprecate, evaluationScript, urlMatches } from './clientHelper'; +import { deprecate, evaluationScript } from './clientHelper'; import { Browser } from './browser'; import { Worker } from './worker'; import { Events } from './events'; @@ -38,7 +38,7 @@ import type { BrowserType } from './browserType'; export class BrowserContext extends ChannelOwner implements api.BrowserContext { _pages = new Set(); - private _routes: { url: URLMatch, handler: network.RouteHandler }[] = []; + private _routes: network.RouteHandler[] = []; readonly _browser: Browser | null = null; private _browserType: BrowserType | undefined; readonly _bindings = new Map any>(); @@ -132,9 +132,9 @@ export class BrowserContext extends ChannelOwner { + async route(url: URLMatch, handler: network.RouteHandlerCallback, options: { times?: number } = {}): Promise { return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { - this._routes.unshift({ url, handler }); + this._routes.unshift(new network.RouteHandler(this._options.baseURL, url, handler, options.times)); if (this._routes.length === 1) await channel.setNetworkInterceptionEnabled({ enabled: true }); }); } - async unroute(url: URLMatch, handler?: network.RouteHandler): Promise { + async unroute(url: URLMatch, handler?: network.RouteHandlerCallback): Promise { return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => { this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); if (this._routes.length === 0) diff --git a/src/client/network.ts b/src/client/network.ts index ef1a8c39da..3699627351 100644 --- a/src/client/network.ts +++ b/src/client/network.ts @@ -26,6 +26,8 @@ import { Events } from './events'; import { Page } from './page'; import { Waiter } from './waiter'; import * as api from '../../types/types'; +import { URLMatch } from '../common/types'; +import { urlMatches } from './clientHelper'; export type NetworkCookie = { name: string, @@ -352,7 +354,7 @@ export class Route extends ChannelOwner void; +export type RouteHandlerCallback = (route: Route, request: Request) => void; export type ResourceTiming = { startTime: number; @@ -516,3 +518,29 @@ export function validateHeaders(headers: Headers) { throw new Error(`Expected value of header "${key}" to be String, but "${typeof value}" is found.`); } } + +export class RouteHandler { + private handledCount = 0; + private readonly _baseURL: string | undefined; + private readonly _times: number | undefined; + readonly url: URLMatch; + readonly handler: RouteHandlerCallback; + + constructor(baseURL: string | undefined, url: URLMatch, handler: RouteHandlerCallback, times?: number) { + this._baseURL = baseURL; + this._times = times; + this.url = url; + this.handler = handler; + } + + public matches(requestURL: string): boolean { + if (this._times && this.handledCount >= this._times) + return false; + return urlMatches(this._baseURL, requestURL, this.url); + } + + public handle(route: Route, request: Request) { + this.handler(route, request); + this.handledCount++; + } +} diff --git a/src/client/page.ts b/src/client/page.ts index b9ae4833fe..87bf4c9f40 100644 --- a/src/client/page.ts +++ b/src/client/page.ts @@ -32,7 +32,7 @@ import { Worker } from './worker'; import { Frame, verifyLoadState, WaitForNavigationOptions } from './frame'; import { Keyboard, Mouse, Touchscreen } from './input'; import { assertMaxArguments, serializeArgument, parseResult, JSHandle } from './jsHandle'; -import { Request, Response, Route, RouteHandler, WebSocket, validateHeaders } from './network'; +import { Request, Response, Route, RouteHandlerCallback, WebSocket, validateHeaders, RouteHandler } from './network'; import { FileChooser } from './fileChooser'; import { Buffer } from 'buffer'; import { Coverage } from './coverage'; @@ -71,7 +71,7 @@ export class Page extends ChannelOwner; private _viewportSize: Size | null; - private _routes: { url: URLMatch, handler: RouteHandler }[] = []; + private _routes: RouteHandler[] = []; readonly accessibility: Accessibility; readonly coverage: Coverage; @@ -161,9 +161,9 @@ export class Page extends ChannelOwner { + async route(url: URLMatch, handler: RouteHandlerCallback, options: { times?: number } = {}): Promise { return this._wrapApiCall(async (channel: channels.PageChannel) => { - this._routes.unshift({ url, handler }); + this._routes.unshift(new RouteHandler(this._browserContext._options.baseURL, url, handler, options.times)); if (this._routes.length === 1) await channel.setNetworkInterceptionEnabled({ enabled: true }); }); } - async unroute(url: URLMatch, handler?: RouteHandler): Promise { + async unroute(url: URLMatch, handler?: RouteHandlerCallback): Promise { return this._wrapApiCall(async (channel: channels.PageChannel) => { this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); if (this._routes.length === 0) diff --git a/tests/browsercontext-route.spec.ts b/tests/browsercontext-route.spec.ts index 7582af30d8..3e5730637a 100644 --- a/tests/browsercontext-route.spec.ts +++ b/tests/browsercontext-route.spec.ts @@ -198,3 +198,15 @@ it('should work with ignoreHTTPSErrors', async ({browser, httpsServer}) => { expect(response.status()).toBe(200); await context.close(); }); + +it('should support the times parameter with route matching', async ({context, page, server}) => { + const intercepted = []; + await context.route('**/empty.html', route => { + intercepted.push(1); + route.continue(); + }, { times: 1}); + await page.goto(server.EMPTY_PAGE); + await page.goto(server.EMPTY_PAGE); + await page.goto(server.EMPTY_PAGE); + expect(intercepted).toHaveLength(1); +}); diff --git a/tests/page/page-route.spec.ts b/tests/page/page-route.spec.ts index fa248b14fb..9ae3a84a18 100644 --- a/tests/page/page-route.spec.ts +++ b/tests/page/page-route.spec.ts @@ -652,3 +652,15 @@ it('should support cors for different methods', async ({page, server}) => { expect(resp).toEqual(['DELETE', 'electric', 'gas']); } }); + +it('should support the times parameter with route matching', async ({page, server}) => { + const intercepted = []; + await page.route('**/empty.html', route => { + intercepted.push(1); + route.continue(); + }, { times: 1}); + await page.goto(server.EMPTY_PAGE); + await page.goto(server.EMPTY_PAGE); + await page.goto(server.EMPTY_PAGE); + expect(intercepted).toHaveLength(1); +}); diff --git a/types/test.d.ts b/types/test.d.ts index 32b9764ff6..4490d9938f 100644 --- a/types/test.d.ts +++ b/types/test.d.ts @@ -2508,7 +2508,7 @@ export interface PlaywrightTestOptions { viewport: ViewportSize | null | undefined; /** * When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto), - * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route), + * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route), * [page.waitForURL(url[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-url), * [page.waitForRequest(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-request), or * [page.waitForResponse(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-response) it diff --git a/types/types.d.ts b/types/types.d.ts index bcfb1ad4f4..d16cace8e5 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -507,8 +507,8 @@ export interface Page { /** * Emitted when a page issues a request. The [request] object is read-only. In order to intercept and mutate requests, see - * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route) or - * [browserContext.route(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browser-context-route). + * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route) or + * [browserContext.route(url, handler[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route). */ on(event: 'request', listener: (request: Request) => void): this; @@ -778,8 +778,8 @@ export interface Page { /** * Emitted when a page issues a request. The [request] object is read-only. In order to intercept and mutate requests, see - * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route) or - * [browserContext.route(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browser-context-route). + * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route) or + * [browserContext.route(url, handler[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route). */ addListener(event: 'request', listener: (request: Request) => void): this; @@ -2339,9 +2339,9 @@ export interface Page { * Once routing is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. * * > NOTE: The handler will only be called for the first url if the response is a redirect. - * > NOTE: [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route) will not intercept requests - * intercepted by Service Worker. See [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend - * disabling Service Workers when using request interception. Via `await context.addInitScript(() => delete + * > NOTE: [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route) will not intercept + * requests intercepted by Service Worker. See [this](https://github.com/microsoft/playwright/issues/1090) issue. We + * recommend disabling Service Workers when using request interception. Via `await context.addInitScript(() => delete * window.navigator.serviceWorker);` * * An example of a naive handler that aborts all image requests: @@ -2375,8 +2375,8 @@ export interface Page { * ``` * * Page routes take precedence over browser context routes (set up with - * [browserContext.route(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browser-context-route)) when - * request matches both handlers. + * [browserContext.route(url, handler[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route)) + * when request matches both handlers. * * To remove a route with its handler you can use * [page.unroute(url[, handler])](https://playwright.dev/docs/api/class-page#page-unroute). @@ -2385,8 +2385,14 @@ export interface Page { * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a `baseURL` via the context options was provided and the passed URL is a path, it gets merged via the * [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. * @param handler handler function to route the request. + * @param options */ - route(url: string|RegExp|((url: URL) => boolean), handler: ((route: Route, request: Request) => void)): Promise; + route(url: string|RegExp|((url: URL) => boolean), handler: ((route: Route, request: Request) => void), options?: { + /** + * How often a route should be used. By default it will be used every time. + */ + times?: number; + }): Promise; /** * Returns the buffer with the captured screenshot. @@ -2853,8 +2859,9 @@ export interface Page { }): Promise; /** - * Removes a route created with [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route). When - * `handler` is not specified, removes all routes for the `url`. + * Removes a route created with + * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route). When `handler` is not + * specified, removes all routes for the `url`. * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. * @param handler Optional handler function to route the request. */ @@ -3019,8 +3026,8 @@ export interface Page { /** * Emitted when a page issues a request. The [request] object is read-only. In order to intercept and mutate requests, see - * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route) or - * [browserContext.route(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browser-context-route). + * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route) or + * [browserContext.route(url, handler[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route). */ waitForEvent(event: 'request', optionsOrPredicate?: { predicate?: (request: Request) => boolean | Promise, timeout?: number } | ((request: Request) => boolean | Promise)): Promise; @@ -5029,8 +5036,8 @@ export interface BrowserContext { * [page.on('request')](https://playwright.dev/docs/api/class-page#page-event-request). * * In order to intercept and mutate requests, see - * [browserContext.route(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browser-context-route) or - * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route). + * [browserContext.route(url, handler[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route) + * or [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route). */ on(event: 'request', listener: (request: Request) => void): this; @@ -5156,8 +5163,8 @@ export interface BrowserContext { * [page.on('request')](https://playwright.dev/docs/api/class-page#page-event-request). * * In order to intercept and mutate requests, see - * [browserContext.route(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browser-context-route) or - * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route). + * [browserContext.route(url, handler[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route) + * or [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route). */ addListener(event: 'request', listener: (request: Request) => void): this; @@ -5500,9 +5507,9 @@ export interface BrowserContext { * Routing provides the capability to modify network requests that are made by any page in the browser context. Once route * is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. * - * > NOTE: [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route) will not intercept requests - * intercepted by Service Worker. See [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend - * disabling Service Workers when using request interception. Via `await context.addInitScript(() => delete + * > NOTE: [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route) will not intercept + * requests intercepted by Service Worker. See [this](https://github.com/microsoft/playwright/issues/1090) issue. We + * recommend disabling Service Workers when using request interception. Via `await context.addInitScript(() => delete * window.navigator.serviceWorker);` * * An example of a naive handler that aborts all image requests: @@ -5537,8 +5544,8 @@ export interface BrowserContext { * }); * ``` * - * Page routes (set up with [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route)) take - * precedence over browser context routes when request matches both handlers. + * Page routes (set up with [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route)) + * take precedence over browser context routes when request matches both handlers. * * To remove a route with its handler you can use * [browserContext.unroute(url[, handler])](https://playwright.dev/docs/api/class-browsercontext#browser-context-unroute). @@ -5547,8 +5554,14 @@ export interface BrowserContext { * @param url A glob pattern, regex pattern or predicate receiving [URL] to match while routing. When a `baseURL` via the context options was provided and the passed URL is a path, it gets merged via the * [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. * @param handler handler function to route the request. + * @param options */ - route(url: string|RegExp|((url: URL) => boolean), handler: ((route: Route, request: Request) => void)): Promise; + route(url: string|RegExp|((url: URL) => boolean), handler: ((route: Route, request: Request) => void), options?: { + /** + * How often a route should be used. By default it will be used every time. + */ + times?: number; + }): Promise; /** * > NOTE: Service workers are only supported on Chromium-based browsers. @@ -5693,10 +5706,10 @@ export interface BrowserContext { /** * Removes a route created with - * [browserContext.route(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browser-context-route). When - * `handler` is not specified, removes all routes for the `url`. - * @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with [browserContext.route(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browser-context-route). - * @param handler Optional handler function used to register a routing with [browserContext.route(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browser-context-route). + * [browserContext.route(url, handler[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route). + * When `handler` is not specified, removes all routes for the `url`. + * @param url A glob pattern, regex pattern or predicate receiving [URL] used to register a routing with [browserContext.route(url, handler[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route). + * @param handler Optional handler function used to register a routing with [browserContext.route(url, handler[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route). */ unroute(url: string|RegExp|((url: URL) => boolean), handler?: ((route: Route, request: Request) => void)): Promise; @@ -5749,8 +5762,8 @@ export interface BrowserContext { * [page.on('request')](https://playwright.dev/docs/api/class-page#page-event-request). * * In order to intercept and mutate requests, see - * [browserContext.route(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browser-context-route) or - * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route). + * [browserContext.route(url, handler[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route) + * or [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route). */ waitForEvent(event: 'request', optionsOrPredicate?: { predicate?: (request: Request) => boolean | Promise, timeout?: number } | ((request: Request) => boolean | Promise)): Promise; @@ -8250,7 +8263,7 @@ export interface BrowserType { /** * When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto), - * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route), + * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route), * [page.waitForURL(url[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-url), * [page.waitForRequest(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-request), or * [page.waitForResponse(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-response) it @@ -9430,7 +9443,7 @@ export interface AndroidDevice { /** * When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto), - * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route), + * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route), * [page.waitForURL(url[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-url), * [page.waitForRequest(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-request), or * [page.waitForResponse(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-response) it @@ -10202,7 +10215,7 @@ export interface Browser extends EventEmitter { /** * When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto), - * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route), + * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route), * [page.waitForURL(url[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-url), * [page.waitForRequest(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-request), or * [page.waitForResponse(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-response) it @@ -11838,9 +11851,9 @@ export interface Response { /** * Whenever a network route is set up with - * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route) or - * [browserContext.route(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browser-context-route), the - * `Route` object allows to handle the route. + * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route) or + * [browserContext.route(url, handler[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route), + * the `Route` object allows to handle the route. */ export interface Route { /** @@ -12357,7 +12370,7 @@ export interface BrowserContextOptions { /** * When using [page.goto(url[, options])](https://playwright.dev/docs/api/class-page#page-goto), - * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#page-route), + * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route), * [page.waitForURL(url[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-url), * [page.waitForRequest(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-request), or * [page.waitForResponse(urlOrPredicate[, options])](https://playwright.dev/docs/api/class-page#page-wait-for-response) it