diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 94225adb71fec..2d2ae9c623e98 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1415,6 +1415,14 @@ Returns storage state for this browser context, contains current cookies and loc * since: v1.12 - type: <[Tracing]> +## async method: BrowserContext.unrouteAll +* since: v1.41 + +Removes all routes created with [`method: BrowserContext.route`] and [`method: BrowserContext.routeFromHAR`]. + +### option: BrowserContext.unrouteAll.behavior = %%-unroute-all-options-behavior-%% +* since: v1.41 + ## async method: BrowserContext.unroute * since: v1.8 diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 7860c57e08bbc..f942966f3be9c 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -3870,6 +3870,14 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Page.uncheck.trial = %%-input-trial-%% * since: v1.11 +## async method: Page.unrouteAll +* since: v1.41 + +Removes all routes created with [`method: Page.route`] and [`method: Page.routeFromHAR`]. + +### option: Page.unrouteAll.behavior = %%-unroute-all-options-behavior-%% +* since: v1.41 + ## async method: Page.unroute * since: v1.8 diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 8de2a2a18f045..117fb343c76d9 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -734,6 +734,14 @@ Whether to allow sites to register Service workers. Defaults to `'allow'`. * `'allow'`: [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) can be registered. * `'block'`: Playwright will block all registration of Service Workers. +## unroute-all-options-behavior +* since: v1.41 +- `behavior` <[UnrouteAllBehavior]<"wait"|"ignoreErrors"|"default">> + +Specifies wether to wait for already running handlers and what to do if they throw errors: +* `'default'` - do not wait for current handler calls (if any) to finish, if unrouted handler throws, it may result in unhandled error +* `'wait'` - wait for current handler calls (if any) to finish +* `'ignoreErrors'` - do not wait for current handler calls (if any) to finish, all errors thrown by the handlers after unrouting are silently caught ## select-options-values * langs: java, js, csharp diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index c7eb89c5311df..976a0a27ebf8d 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -64,6 +64,7 @@ export class BrowserContext extends ChannelOwner private _harRecorders = new Map(); _closeWasCalled = false; private _closeReason: string | undefined; + private _harRouters: HarRouter[] = []; static from(context: channels.BrowserContextChannel): BrowserContext { return (context as any)._object; @@ -212,7 +213,9 @@ export class BrowserContext extends ChannelOwner if (handled) return; } - await route._innerContinue(true); + // If the page is closed or unrouteAll() was called without waiting and interception disabled, + // the method will throw an error - silence it. + await route._innerContinue(true).catch(() => {}); } async _onBinding(bindingCall: BindingCall) { @@ -331,7 +334,18 @@ export class BrowserContext extends ChannelOwner return; } const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url }); - harRouter.addContextRoute(this); + this._harRouters.push(harRouter); + await harRouter.addContextRoute(this); + } + + private _disposeHarRouters() { + this._harRouters.forEach(router => router.dispose()); + this._harRouters = []; + } + + async unrouteAll(options?: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise { + await this._unrouteInternal(this._routes, [], options); + this._disposeHarRouters(); } async unroute(url: URLMatch, handler?: network.RouteHandlerCallback, options?: { noWaitForActive?: boolean }): Promise { @@ -343,11 +357,17 @@ export class BrowserContext extends ChannelOwner else remaining.push(route); } + const behavior = options?.noWaitForActive ? 'ignoreErrors' : 'wait'; + await this._unrouteInternal(removed, remaining, { behavior }); + } + + private async _unrouteInternal(removed: network.RouteHandler[], remaining: network.RouteHandler[], options?: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise { this._routes = remaining; await this._updateInterceptionPatterns(); - const promises = removed.map(routeHandler => routeHandler.stopAndWaitForRunningHandlers(null, options?.noWaitForActive)); + if (!options?.behavior || options?.behavior === 'default') + return; + const promises = removed.map(routeHandler => routeHandler.stopAndWaitForRunningHandlers(null, options?.behavior === 'ignoreErrors')); await Promise.all(promises); - } private async _updateInterceptionPatterns() { @@ -402,6 +422,7 @@ export class BrowserContext extends ChannelOwner if (this._browser) this._browser._contexts.delete(this); this._browserType?._contexts?.delete(this); + this._disposeHarRouters(); this.emit(Events.BrowserContext.Close, this); } diff --git a/packages/playwright-core/src/client/harRouter.ts b/packages/playwright-core/src/client/harRouter.ts index 0ba71d8ba87aa..a05945d80c09f 100644 --- a/packages/playwright-core/src/client/harRouter.ts +++ b/packages/playwright-core/src/client/harRouter.ts @@ -16,7 +16,6 @@ import { debugLogger } from '../common/debugLogger'; import type { BrowserContext } from './browserContext'; -import { Events } from './events'; import type { LocalUtils } from './localUtils'; import type { Route } from './network'; import type { URLMatch } from './types'; @@ -85,12 +84,10 @@ export class HarRouter { async addContextRoute(context: BrowserContext) { await context.route(this._options.urlMatch || '**/*', route => this._handle(route)); - context.once(Events.BrowserContext.Close, () => this.dispose()); } async addPageRoute(page: Page) { await page.route(this._options.urlMatch || '**/*', route => this._handle(route)); - page.once(Events.Page.Close, () => this.dispose()); } async [Symbol.asyncDispose]() { diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 17b63636f3784..b03b7567c549b 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -95,6 +95,7 @@ export class Page extends ChannelOwner implements api.Page readonly _opener: Page | null; private _closeReason: string | undefined; _closeWasCalled: boolean = false; + private _harRouters: HarRouter[] = []; static from(page: channels.PageChannel): Page { return (page as any)._object; @@ -215,6 +216,7 @@ export class Page extends ChannelOwner implements api.Page this._closed = true; this._browserContext._pages.delete(this); this._browserContext._backgroundPages.delete(this); + this._disposeHarRouters(); this.emit(Events.Page.Close, this); } @@ -467,7 +469,18 @@ export class Page extends ChannelOwner implements api.Page return; } const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url }); - harRouter.addPageRoute(this); + this._harRouters.push(harRouter); + await harRouter.addPageRoute(this); + } + + private _disposeHarRouters() { + this._harRouters.forEach(router => router.dispose()); + this._harRouters = []; + } + + async unrouteAll(options?: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise { + await this._unrouteInternal(this._routes, [], options); + this._disposeHarRouters(); } async unroute(url: URLMatch, handler?: RouteHandlerCallback, options?: { noWaitForActive?: boolean }): Promise { @@ -479,9 +492,16 @@ export class Page extends ChannelOwner implements api.Page else remaining.push(route); } + const behavior = options?.noWaitForActive ? 'ignoreErrors' : 'wait'; + await this._unrouteInternal(removed, remaining, { behavior }); + } + + private async _unrouteInternal(removed: RouteHandler[], remaining: RouteHandler[], options?: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise { this._routes = remaining; await this._updateInterceptionPatterns(); - const promises = removed.map(routeHandler => routeHandler.stopAndWaitForRunningHandlers(this, options?.noWaitForActive)); + if (!options?.behavior || options?.behavior === 'default') + return; + const promises = removed.map(routeHandler => routeHandler.stopAndWaitForRunningHandlers(this, options?.behavior === 'ignoreErrors')); await Promise.all(promises); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index d37498c23b89e..570360399f2b6 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -4253,6 +4253,24 @@ export interface Page { noWaitForActive?: boolean; }): Promise; + /** + * Removes all routes created with + * [page.route(url, handler[, options])](https://playwright.dev/docs/api/class-page#page-route) and + * [page.routeFromHAR(har[, options])](https://playwright.dev/docs/api/class-page#page-route-from-har). + * @param options + */ + unrouteAll(options?: { + /** + * Specifies wether to wait for already running handlers and what to do if they throw errors: + * - `'default'` - do not wait for current handler calls (if any) to finish, if unrouted handler throws, it may + * result in unhandled error + * - `'wait'` - wait for current handler calls (if any) to finish + * - `'ignoreErrors'` - do not wait for current handler calls (if any) to finish, all errors thrown by the handlers + * after unrouting are silently caught + */ + behavior?: "wait"|"ignoreErrors"|"default"; + }): Promise; + url(): string; /** @@ -8636,6 +8654,25 @@ export interface BrowserContext { noWaitForActive?: boolean; }): Promise; + /** + * Removes all routes created with + * [browserContext.route(url, handler[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route) + * and + * [browserContext.routeFromHAR(har[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route-from-har). + * @param options + */ + unrouteAll(options?: { + /** + * Specifies wether to wait for already running handlers and what to do if they throw errors: + * - `'default'` - do not wait for current handler calls (if any) to finish, if unrouted handler throws, it may + * result in unhandled error + * - `'wait'` - wait for current handler calls (if any) to finish + * - `'ignoreErrors'` - do not wait for current handler calls (if any) to finish, all errors thrown by the handlers + * after unrouting are silently caught + */ + behavior?: "wait"|"ignoreErrors"|"default"; + }): Promise; + /** * **NOTE** Only works with Chromium browser's persistent context. * diff --git a/tests/library/browsercontext-har.spec.ts b/tests/library/browsercontext-har.spec.ts index 3f67309bbbfae..7913a079dfd7f 100644 --- a/tests/library/browsercontext-har.spec.ts +++ b/tests/library/browsercontext-har.spec.ts @@ -403,3 +403,29 @@ it('should update extracted har.zip for page', async ({ contextFactory, server } expect(await page2.content()).toContain('hello, world!'); await expect(page2.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)'); }); + +it('page.unrouteAll should stop page.routeFromHAR', async ({ contextFactory, server, asset }, testInfo) => { + const harPath = asset('har-fulfill.har'); + const context1 = await contextFactory(); + const page1 = await context1.newPage(); + // The har file contains requests for another domain, so the router + // is expected to abort all requests. + await page1.routeFromHAR(harPath, { notFound: 'abort' }); + await expect(page1.goto(server.EMPTY_PAGE)).rejects.toThrow(); + await page1.unrouteAll({ behavior: 'wait' }); + const response = await page1.goto(server.EMPTY_PAGE); + expect(response.ok()).toBeTruthy(); +}); + +it('context.unrouteAll should stop context.routeFromHAR', async ({ contextFactory, server, asset }, testInfo) => { + const harPath = asset('har-fulfill.har'); + const context1 = await contextFactory(); + const page1 = await context1.newPage(); + // The har file contains requests for another domain, so the router + // is expected to abort all requests. + await context1.routeFromHAR(harPath, { notFound: 'abort' }); + await expect(page1.goto(server.EMPTY_PAGE)).rejects.toThrow(); + await context1.unrouteAll({ behavior: 'wait' }); + const response = await page1.goto(server.EMPTY_PAGE); + expect(response.ok()).toBeTruthy(); +}); diff --git a/tests/library/browsercontext-route.spec.ts b/tests/library/browsercontext-route.spec.ts index 8cf13254bd215..8f2cd92e91756 100644 --- a/tests/library/browsercontext-route.spec.ts +++ b/tests/library/browsercontext-route.spec.ts @@ -139,6 +139,77 @@ it('unroute should not wait for pending handlers to complete if noWaitForActive expect(secondHandlerCalled).toBe(true); }); +it('unrouteAll removes all handlers', async ({ page, context, server }) => { + await context.route('**/*', route => { + void route.abort(); + }); + await context.route('**/empty.html', route => { + void route.abort(); + }); + await context.unrouteAll(); + await page.goto(server.EMPTY_PAGE); +}); + +it('unrouteAll should wait for pending handlers to complete', async ({ page, context, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23781' }); + let secondHandlerCalled = false; + await context.route(/.*/, async route => { + secondHandlerCalled = true; + await route.abort(); + }); + let routeCallback; + const routePromise = new Promise(f => routeCallback = f); + let continueRouteCallback; + const routeBarrier = new Promise(f => continueRouteCallback = f); + const handler = async route => { + routeCallback(); + await routeBarrier; + await route.fallback(); + }; + await context.route(/.*/, handler); + const navigationPromise = page.goto(server.EMPTY_PAGE); + await routePromise; + let didUnroute = false; + const unroutePromise = context.unrouteAll({ behavior: 'wait' }).then(() => didUnroute = true); + await new Promise(f => setTimeout(f, 500)); + expect(didUnroute).toBe(false); + continueRouteCallback(); + await unroutePromise; + expect(didUnroute).toBe(true); + await navigationPromise; + expect(secondHandlerCalled).toBe(false); +}); + +it('unrouteAll should not wait for pending handlers to complete if behavior is ignoreErrors', async ({ page, context, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23781' }); + let secondHandlerCalled = false; + await context.route(/.*/, async route => { + secondHandlerCalled = true; + await route.abort(); + }); + let routeCallback; + const routePromise = new Promise(f => routeCallback = f); + let continueRouteCallback; + const routeBarrier = new Promise(f => continueRouteCallback = f); + const handler = async route => { + routeCallback(); + await routeBarrier; + throw new Error('Handler error'); + }; + await context.route(/.*/, handler); + const navigationPromise = page.goto(server.EMPTY_PAGE); + await routePromise; + let didUnroute = false; + const unroutePromise = context.unrouteAll({ behavior: 'ignoreErrors' }).then(() => didUnroute = true); + await new Promise(f => setTimeout(f, 500)); + await unroutePromise; + expect(didUnroute).toBe(true); + continueRouteCallback(); + await navigationPromise.catch(e => void e); + // The error in the unrouted handler should be silently caught and remaining handler called. + expect(secondHandlerCalled).toBe(false); +}); + it('should yield to page.route', async ({ browser, server }) => { const context = await browser.newContext(); await context.route('**/empty.html', route => { diff --git a/tests/page/page-route.spec.ts b/tests/page/page-route.spec.ts index 578d763914173..df7ab719069e8 100644 --- a/tests/page/page-route.spec.ts +++ b/tests/page/page-route.spec.ts @@ -132,6 +132,78 @@ it('unroute should not wait for pending handlers to complete if noWaitForActive expect(secondHandlerCalled).toBe(true); }); +it('unrouteAll removes all routes', async ({ page, server }) => { + await page.route('**/*', route => { + void route.abort(); + }); + await page.route('**/empty.html', route => { + void route.abort(); + }); + await page.unrouteAll(); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok()).toBe(true); +}); + +it('unrouteAll should wait for pending handlers to complete', async ({ page, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23781' }); + let secondHandlerCalled = false; + await page.route(/.*/, async route => { + secondHandlerCalled = true; + await route.abort(); + }); + let routeCallback; + const routePromise = new Promise(f => routeCallback = f); + let continueRouteCallback; + const routeBarrier = new Promise(f => continueRouteCallback = f); + const handler = async route => { + routeCallback(); + await routeBarrier; + await route.fallback(); + }; + await page.route(/.*/, handler); + const navigationPromise = page.goto(server.EMPTY_PAGE); + await routePromise; + let didUnroute = false; + const unroutePromise = page.unrouteAll({ behavior: 'wait' }).then(() => didUnroute = true); + await new Promise(f => setTimeout(f, 500)); + expect(didUnroute).toBe(false); + continueRouteCallback(); + await unroutePromise; + expect(didUnroute).toBe(true); + await navigationPromise; + expect(secondHandlerCalled).toBe(false); +}); + +it('unrouteAll should not wait for pending handlers to complete if behavior is ignoreErrors', async ({ page, server }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23781' }); + let secondHandlerCalled = false; + await page.route(/.*/, async route => { + secondHandlerCalled = true; + await route.abort(); + }); + let routeCallback; + const routePromise = new Promise(f => routeCallback = f); + let continueRouteCallback; + const routeBarrier = new Promise(f => continueRouteCallback = f); + const handler = async route => { + routeCallback(); + await routeBarrier; + throw new Error('Handler error'); + }; + await page.route(/.*/, handler); + const navigationPromise = page.goto(server.EMPTY_PAGE); + await routePromise; + let didUnroute = false; + const unroutePromise = page.unrouteAll({ behavior: 'ignoreErrors' }).then(() => didUnroute = true); + await new Promise(f => setTimeout(f, 500)); + await unroutePromise; + expect(didUnroute).toBe(true); + continueRouteCallback(); + await navigationPromise.catch(e => void e); + // The error in the unrouted handler should be silently caught. + expect(secondHandlerCalled).toBe(false); +}); + it('should support ? in glob pattern', async ({ page, server }) => { server.setRoute('/index', (req, res) => res.end('index-no-hello')); server.setRoute('/index123hello', (req, res) => res.end('index123hello'));