diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 50f2cd547f256..7a778d2f68aac 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1025,35 +1025,6 @@ handler function to route the request. How often a route should be used. By default it will be used every time. -## async method: BrowserContext.routeFromHar - -Provides the capability to serve network requests that are made in the context from prerecorded HAR file. - -:::note -[`method: BrowserContext.routeFromHar`] 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);` -::: - -### param: BrowserContext.routeFromHar.harPath -- `harPath` <[path]> - -Path to the HAR file with prerecorded network data. If HAR file contains an entry with the matching url and HTTP method, then the entry's headers, status and body will be used to fulfill. An entry resulting in a redirect will be followed automatically. If there is no matching entry in the file the execution continues to try other configured HAR files and [Route] handlers. -If `path` is a relative path, then it is resolved relative to the current working directory. - -### option: BrowserContext.routeFromHar.strict -- `strict` <[boolean]> - -If set to true any request not found in the HAR file will be aborted. If set to -false missing requests will continue normal flow and can be handled by other -[Route] handlers or served from other HAR files configured with [`method: BrowserContext.routeFromHar`]. -Defaults to true. - -### option: BrowserContext.routeFromHar.url -- `url` <[string]|[RegExp]> - -A glob pattern or regular expression to match request URL while routing. Only requests -with URL matching the pattern will be surved from the HAR file. If not specified, all -requests are served from the HAR file. - ## method: BrowserContext.serviceWorkers * langs: js, python - returns: <[Array]<[Worker]>> @@ -1220,15 +1191,6 @@ Optional handler function used to register a routing with [`method: BrowserConte Optional handler function used to register a routing with [`method: BrowserContext.route`]. -## async method: BrowserContext.unrouteFromHar - -Removes HAR handler previously added with [`method: BrowserContext.routeFromHar`]. - -### param: BrowserContext.unrouteFromHar.harPath -- `harPath` <[path]> - -Path to the HAR file which was passed to [`method: BrowserContext.routeFromHar`]. - ## async method: BrowserContext.waitForEvent * langs: js, python - alias-python: expect_event diff --git a/docs/src/api/class-electron.md b/docs/src/api/class-electron.md index c17c84957ef0f..2c9aa189d4b39 100644 --- a/docs/src/api/class-electron.md +++ b/docs/src/api/class-electron.md @@ -94,6 +94,7 @@ Maximum time in milliseconds to wait for the application to start. Defaults to ` ### option: Electron.launch.recordhar = %%-context-option-recordhar-%% ### option: Electron.launch.recordharpath = %%-context-option-recordhar-path-%% ### option: Electron.launch.recordHarOmitContent = %%-context-option-recordhar-omit-content-%% +### option: Electron.launch.har = %%-js-python-context-option-har-%% ### option: Electron.launch.recordvideo = %%-context-option-recordvideo-%% ### option: Electron.launch.recordvideodir = %%-context-option-recordvideo-dir-%% ### option: Electron.launch.recordvideosize = %%-context-option-recordvideo-size-%% diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 80b043aecfe10..77e35688061a1 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2732,35 +2732,6 @@ handler function to route the request. How often a route should be used. By default it will be used every time. -## async method: Page.routeFromHar - -Provides the capability to serve network requests that are made by a page from prerecorded HAR file. - -:::note -[`method: Page.routeFromHar`] 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);` -::: - -### param: Page.routeFromHar.harPath -- `harPath` <[path]> - -Path to the HAR file with prerecorded network data. If HAR file contains an entry with the matching url and HTTP method, then the entry's headers, status and body will be used to fulfill. An entry resulting in a redirect will be followed automatically. If there is no matching entry in the file the execution continues to try other configured HAR files and [Route] handlers. -If `path` is a relative path, then it is resolved relative to the current working directory. - -### option: Page.routeFromHar.strict -- `strict` <[boolean]> - -If set to true any request not found in the HAR file will be aborted. If set to -false missing requests will continue normal flow and can be handled by other -[Route] handlers or served from other HAR files configured with [`method: Page.routeFromHar`]. -Defaults to true. - -### option: Page.routeFromHar.url -- `url` <[string]|[RegExp]> - -A glob pattern or regular expression to match request URL while routing. Only requests -with URL matching the pattern will be surved from the HAR file. If not specified, all -requests are served from the HAR file. - ## async method: Page.screenshot - returns: <[Buffer]> @@ -3147,15 +3118,6 @@ Optional handler function to route the request. Optional handler function to route the request. -## async method: Page.unrouteFromHar - -Removes HAR handler previously added with [`method: Page.routeFromHar`]. - -### param: Page.unrouteFromHar.harPath -- `harPath` <[path]> - -Path to the HAR file which was passed to [`method: Page.routeFromHar`]. - ## method: Page.url - returns: <[string]> diff --git a/docs/src/api/params.md b/docs/src/api/params.md index a3d6a02ff602b..99db4b3f2e3d0 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -247,6 +247,37 @@ The file path to save the storage state to. If [`option: path`] is a relative pa current working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. +## js-python-context-option-har +* langs: js, python +- `har` <[Object]> + - `path` <[path]> Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If the HAR file contains an entry with the matching URL and HTTP method, then the entry's headers, status and body will be used to fulfill the network request. An entry resulting in a redirect will be followed automatically. If `path` is a relative path, then it is resolved relative to the current working directory. + - `fallback` ?<[HarFallback]<"abort"|"continue">> If set to 'abort' any request not found in the HAR file will be aborted. If set to'continue' missing requests will be sent to the network. Defaults to 'abort'. + - `urlFilter` ?<[string]|[RegExp]> A glob pattern or regular expression to match request URL while routing. Only requests with URL matching the pattern will be surved from the HAR file. If not specified, all requests are served from the HAR file. + +If specified the network requests that are made in the context will be served from the HAR file. + +:::note +Playwright will not serve requests intercepted by Service Worker from the HAR file. 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);` +::: + +## csharp-java-context-option-har-path +* langs: csharp, java +- `harPath` <[path]> + +Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If the HAR file contains an entry with the matching URL and HTTP method, then the entry's headers, status and body will be used to fulfill the network request. An entry resulting in a redirect will be followed automatically. If `path` is a relative path, then it is resolved relative to the current working directory. + +## csharp-java-context-option-har-fallback +* langs: csharp, java +- `fallback` ?<[HarFallback]<"abort"|"continue">> + +If set to 'abort' any request not found in the HAR file will be aborted. If set to'continue' missing requests will be sent to the network. Defaults to 'abort'. + +## csharp-java-context-option-har-urlfilter +* langs: csharp, java +- `urlFilter` ?<[string]|[RegExp]> + +A glob pattern or regular expression to match request URL while routing. Only requests with URL matching the pattern will be surved from the HAR file. If not specified, all requests are served from the HAR file. + ## context-option-acceptdownloads - `acceptDownloads` <[boolean]> @@ -802,6 +833,10 @@ An acceptable perceived color difference in the [YIQ color space](https://en.wik - %%-context-option-logger-%% - %%-context-option-videospath-%% - %%-context-option-videosize-%% +- %%-js-python-context-option-har-%% +- %%-csharp-java-context-option-har-path-%% +- %%-csharp-java-context-option-har-fallback-%% +- %%-csharp-java-context-option-har-urlfilter-%% - %%-context-option-recordhar-%% - %%-context-option-recordhar-path-%% - %%-context-option-recordhar-omit-content-%% diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index 077f8c1902d9e..db9825d0b8e3f 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -24,6 +24,7 @@ import { isSafeCloseError, kBrowserClosedError } from '../common/errors'; import type * as api from '../../types/types'; import { CDPSession } from './cdpSession'; import type { BrowserType } from './browserType'; +import { HarRouter } from './harRouter'; export class Browser extends ChannelOwner implements api.Browser { readonly _contexts = new Set(); @@ -60,12 +61,14 @@ export class Browser extends ChannelOwner implements ap async newContext(options: BrowserContextOptions = {}): Promise { options = { ...this._browserType._defaultContextOptions, ...options }; + const harRouter = options.har ? await HarRouter.create(options.har) : null; const contextOptions = await prepareBrowserContextParams(options); const context = BrowserContext.from((await this._channel.newContext(contextOptions)).context); context._options = contextOptions; this._contexts.add(context); context._logger = options.logger || this._logger; context._setBrowserType(this._browserType); + harRouter?.addRoute(context); await this._browserType._onDidCreateContext?.(context); return context; } diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index a87ee5be471cc..c27c66e140dbf 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -40,7 +40,6 @@ import { Artifact } from './artifact'; import { APIRequestContext } from './fetch'; import { createInstrumentation } from './clientInstrumentation'; import { rewriteErrorMessage } from '../utils/stackTrace'; -import { HarRouter } from './harRouter'; export class BrowserContext extends ChannelOwner implements api.BrowserContext { _pages = new Set(); @@ -52,7 +51,6 @@ export class BrowserContext extends ChannelOwner _ownerPage: Page | undefined; private _closedPromise: Promise; _options: channels.BrowserNewContextParams = { }; - private readonly _harRouter = new HarRouter(this); readonly request: APIRequestContext; readonly tracing: Tracing; @@ -275,14 +273,6 @@ export class BrowserContext extends ChannelOwner await this._disableInterception(); } - async routeFromHar(harPath: string, options?: { strict?: boolean; url?: string|RegExp; }): Promise { - await this._harRouter.routeFromHar(harPath, options); - } - - async unrouteFromHar(harPath: string): Promise { - await this._harRouter.unrouteFromHar(harPath); - } - async _unrouteAll() { this._routes = []; await this._disableInterception(); diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index c4b782f5c3189..acc745734e5e7 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -28,6 +28,7 @@ import type * as api from '../../types/types'; import { kBrowserClosedError } from '../common/errors'; import { raceAgainstTimeout } from '../utils/timeoutRunner'; import type { Playwright } from './playwright'; +import { HarRouter } from './harRouter'; export interface BrowserServerLauncher { launchServer(options?: LaunchServerOptions): Promise; @@ -94,6 +95,7 @@ export class BrowserType extends ChannelOwner imple const logger = options.logger || this._defaultLaunchOptions?.logger; assert(!(options as any).port, 'Cannot specify a port without launching as a server.'); options = { ...this._defaultLaunchOptions, ...this._defaultContextOptions, ...options }; + const harRouter = options.har ? await HarRouter.create(options.har) : null; const contextParams = await prepareBrowserContextParams(options); const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = { ...contextParams, @@ -108,6 +110,7 @@ export class BrowserType extends ChannelOwner imple context._options = contextParams; context._logger = logger; context._setBrowserType(this); + harRouter?.addRoute(context); await this._onDidCreateContext?.(context); return context; } diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index a4b2d2ad89bcf..98fe7964f640b 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -27,12 +27,14 @@ import { envObjectToArray } from './clientHelper'; import { Events } from './events'; import { JSHandle, parseResult, serializeArgument } from './jsHandle'; import type { Page } from './page'; -import type { Env, WaitForEventOptions, Headers } from './types'; +import type { Env, WaitForEventOptions, Headers, BrowserContextOptions } from './types'; import { Waiter } from './waiter'; +import { HarRouter } from './harRouter'; type ElectronOptions = Omit & { env?: Env, extraHTTPHeaders?: Headers, + har?: BrowserContextOptions['har'] }; type ElectronAppType = typeof import('electron'); @@ -52,8 +54,10 @@ export class Electron extends ChannelOwner implements extraHTTPHeaders: options.extraHTTPHeaders && headersObjectToArray(options.extraHTTPHeaders), env: envObjectToArray(options.env ? options.env : process.env), }; + const harRouter = options.har ? await HarRouter.create(options.har) : null; const app = ElectronApplication.from((await this._channel.launch(params)).electronApplication); app._context._options = params; + harRouter?.addRoute(app._context); return app; } } diff --git a/packages/playwright-core/src/client/harRouter.ts b/packages/playwright-core/src/client/harRouter.ts index 116997868b014..3ed4bbe062667 100644 --- a/packages/playwright-core/src/client/harRouter.ts +++ b/packages/playwright-core/src/client/harRouter.ts @@ -18,60 +18,44 @@ import fs from 'fs'; import type { HAREntry, HARFile, HARResponse } from '../../types/types'; import type { BrowserContext } from './browserContext'; import type { Route } from './network'; -import type { Page } from './page'; +import type { BrowserContextOptions } from './types'; -type HarHandler = { - pattern: string | RegExp; - handler: (route: Route) => any; -}; +type HarOptions = NonNullable; export class HarRouter { - private _harPathToHandlers: Map = new Map(); - private readonly owner: BrowserContext | Page; + private _pattern: string | RegExp; + private _handler: (route: Route) => Promise; - constructor(owner: BrowserContext | Page) { - this.owner = owner; + static async create(options: HarOptions): Promise { + const harFile = JSON.parse(await fs.promises.readFile(options.path, 'utf-8')) as HARFile; + return new HarRouter(harFile, options); } - async routeFromHar(path: string, options?: { strict?: boolean; url?: string|RegExp; }): Promise { - const harFile = JSON.parse(await fs.promises.readFile(path, 'utf-8')) as HARFile; - const harHandler = { - pattern: options?.url ?? /.*/, - handler: async (route: Route) => { - let response; - try { - response = harFindResponse(harFile, { - url: route.request().url(), - method: route.request().method() - }); - } catch (e) { - // TODO: throw or at least error log? - // rewriteErrorMessage(e, e.message + `\n\nFailed to find matching entry for ${route.request().method()} ${route.request().url()} in ${path}`); - // throw e; - } - if (response) - await route.fulfill({ response }); - else if (options?.strict === false) - await route.fallback(); - else - await route.abort(); + constructor(harFile: HARFile, options?: HarOptions) { + this._pattern = options?.urlFilter ?? /.*/; + this._handler = async (route: Route) => { + let response; + try { + response = harFindResponse(harFile, { + url: route.request().url(), + method: route.request().method() + }); + } catch (e) { + // TODO: throw or at least error log? + // rewriteErrorMessage(e, e.message + `\n\nFailed to find matching entry for ${route.request().method()} ${route.request().url()} in ${path}`); + // throw e; } + if (response) + await route.fulfill({ response }); + else if (options?.fallback === 'continue') + await route.fallback(); + else + await route.abort(); }; - let handlers = this._harPathToHandlers.get(path); - if (!handlers) { - handlers = []; - this._harPathToHandlers.set(path, handlers); - } - handlers.push(harHandler); - await this.owner.route(harHandler.pattern, harHandler.handler); } - async unrouteFromHar(path: string): Promise { - const handlers = this._harPathToHandlers.get(path); - if (!handlers) - return; - this._harPathToHandlers.delete(path); - await Promise.all(handlers.map(h => this.owner.unroute(h.pattern, h.handler))); + async addRoute(context: BrowserContext) { + await context.route(this._pattern, this._handler); } } diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index fdea84d8cdab7..4b82e708ea1e9 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -43,7 +43,6 @@ import type { APIRequestContext } from './fetch'; import { FileChooser } from './fileChooser'; import type { WaitForNavigationOptions } from './frame'; import { Frame, verifyLoadState } from './frame'; -import { HarRouter } from './harRouter'; import { Keyboard, Mouse, Touchscreen } from './input'; import { assertMaxArguments, JSHandle, parseResult, serializeArgument } from './jsHandle'; import type { FrameLocator, Locator, LocatorOptions } from './locator'; @@ -98,7 +97,6 @@ export class Page extends ChannelOwner implements api.Page readonly _timeoutSettings: TimeoutSettings; private _video: Video | null = null; readonly _opener: Page | null; - private readonly _harRouter = new HarRouter(this); static from(page: channels.PageChannel): Page { return (page as any)._object; @@ -475,14 +473,6 @@ export class Page extends ChannelOwner implements api.Page await this._disableInterception(); } - async routeFromHar(harPath: string, options?: { strict?: boolean; url?: string|RegExp; }): Promise { - await this._harRouter.routeFromHar(harPath, options); - } - - async unrouteFromHar(harPath: string): Promise { - await this._harRouter.unrouteFromHar(harPath); - } - async _unrouteAll() { this._routes = []; await this._disableInterception(); diff --git a/packages/playwright-core/src/client/types.ts b/packages/playwright-core/src/client/types.ts index 00e8d3203f69a..d6ffe950357bb 100644 --- a/packages/playwright-core/src/client/types.ts +++ b/packages/playwright-core/src/client/types.ts @@ -54,6 +54,11 @@ export type BrowserContextOptions = Omit; - /** - * Provides the capability to serve network requests that are made by a page from prerecorded HAR file. - * - * > NOTE: [page.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-page#page-route-from-har) 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);` - * @param harPath Path to the HAR file with prerecorded network data. If HAR file contains an entry with the matching url and HTTP method, then the entry's headers, status and body will be used to fulfill. An entry resulting in a redirect will be followed - * automatically. If there is no matching entry in the file the execution continues to try other configured HAR files and - * [Route] handlers. If `path` is a relative path, then it is resolved relative to the current working directory. - * @param options - */ - routeFromHar(harPath: string, options?: { - /** - * If set to true any request not found in the HAR file will be aborted. If set to false missing requests will continue - * normal flow and can be handled by other [Route] handlers or served from other HAR files configured with - * [page.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-page#page-route-from-har). Defaults to - * true. - */ - strict?: boolean; - - /** - * A glob pattern or regular expression to match request URL while routing. Only requests with URL matching the pattern - * will be surved from the HAR file. If not specified, all requests are served from the HAR file. - */ - url?: string|RegExp; - }): Promise; - /** * Returns the buffer with the captured screenshot. * @param options @@ -3742,13 +3714,6 @@ export interface Page { */ unroute(url: string|RegExp|((url: URL) => boolean), handler?: ((route: Route, request: Request) => void)): Promise; - /** - * Removes HAR handler previously added with - * [page.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-page#page-route-from-har). - * @param harPath Path to the HAR file which was passed to [page.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-page#page-route-from-har). - */ - unrouteFromHar(harPath: string): Promise; - /** * Shortcut for main frame's [frame.url()](https://playwright.dev/docs/api/class-frame#frame-url). */ @@ -7129,35 +7094,6 @@ export interface BrowserContext { times?: number; }): Promise; - /** - * Provides the capability to serve network requests that are made in the context from prerecorded HAR file. - * - * > NOTE: - * [browserContext.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route-from-har) - * 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);` - * @param harPath Path to the HAR file with prerecorded network data. If HAR file contains an entry with the matching url and HTTP method, then the entry's headers, status and body will be used to fulfill. An entry resulting in a redirect will be followed - * automatically. If there is no matching entry in the file the execution continues to try other configured HAR files and - * [Route] handlers. If `path` is a relative path, then it is resolved relative to the current working directory. - * @param options - */ - routeFromHar(harPath: string, options?: { - /** - * If set to true any request not found in the HAR file will be aborted. If set to false missing requests will continue - * normal flow and can be handled by other [Route] handlers or served from other HAR files configured with - * [browserContext.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route-from-har). - * Defaults to true. - */ - strict?: boolean; - - /** - * A glob pattern or regular expression to match request URL while routing. Only requests with URL matching the pattern - * will be surved from the HAR file. If not specified, all requests are served from the HAR file. - */ - url?: string|RegExp; - }): Promise; - /** * > NOTE: Service workers are only supported on Chromium-based browsers. * @@ -7308,13 +7244,6 @@ export interface BrowserContext { */ unroute(url: string|RegExp|((url: URL) => boolean), handler?: ((route: Route, request: Request) => void)): Promise; - /** - * Removes HAR handler previously added with - * [browserContext.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route-from-har). - * @param harPath Path to the HAR file which was passed to [browserContext.routeFromHar(harPath[, options])](https://playwright.dev/docs/api/class-browsercontext#browser-context-route-from-har). - */ - unrouteFromHar(harPath: string): Promise; - /** * > NOTE: Only works with Chromium browser's persistent context. * @@ -10568,6 +10497,35 @@ export interface BrowserType { */ handleSIGTERM?: boolean; + /** + * If specified the network requests that are made in the context will be served from the HAR file. + * + * > NOTE: Playwright will not serve requests intercepted by Service Worker from the HAR file. 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);` + */ + har?: { + /** + * Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If the HAR file + * contains an entry with the matching URL and HTTP method, then the entry's headers, status and body will be used to + * fulfill the network request. An entry resulting in a redirect will be followed automatically. If `path` is a relative + * path, then it is resolved relative to the current working directory. + */ + path: string; + + /** + * If set to 'abort' any request not found in the HAR file will be aborted. If set to'continue' missing requests will be + * sent to the network. Defaults to 'abort'. + */ + fallback?: "abort"|"continue"; + + /** + * A glob pattern or regular expression to match request URL while routing. Only requests with URL matching the pattern + * will be surved from the HAR file. If not specified, all requests are served from the HAR file. + */ + urlFilter?: string|RegExp; + }; + /** * Specifies if viewport supports touch events. Defaults to false. */ @@ -11787,6 +11745,35 @@ export interface AndroidDevice { accuracy?: number; }; + /** + * If specified the network requests that are made in the context will be served from the HAR file. + * + * > NOTE: Playwright will not serve requests intercepted by Service Worker from the HAR file. 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);` + */ + har?: { + /** + * Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If the HAR file + * contains an entry with the matching URL and HTTP method, then the entry's headers, status and body will be used to + * fulfill the network request. An entry resulting in a redirect will be followed automatically. If `path` is a relative + * path, then it is resolved relative to the current working directory. + */ + path: string; + + /** + * If set to 'abort' any request not found in the HAR file will be aborted. If set to'continue' missing requests will be + * sent to the network. Defaults to 'abort'. + */ + fallback?: "abort"|"continue"; + + /** + * A glob pattern or regular expression to match request URL while routing. Only requests with URL matching the pattern + * will be surved from the HAR file. If not specified, all requests are served from the HAR file. + */ + urlFilter?: string|RegExp; + }; + /** * Specifies if viewport supports touch events. Defaults to false. */ @@ -13319,6 +13306,35 @@ export interface Browser extends EventEmitter { accuracy?: number; }; + /** + * If specified the network requests that are made in the context will be served from the HAR file. + * + * > NOTE: Playwright will not serve requests intercepted by Service Worker from the HAR file. 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);` + */ + har?: { + /** + * Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If the HAR file + * contains an entry with the matching URL and HTTP method, then the entry's headers, status and body will be used to + * fulfill the network request. An entry resulting in a redirect will be followed automatically. If `path` is a relative + * path, then it is resolved relative to the current working directory. + */ + path: string; + + /** + * If set to 'abort' any request not found in the HAR file will be aborted. If set to'continue' missing requests will be + * sent to the network. Defaults to 'abort'. + */ + fallback?: "abort"|"continue"; + + /** + * A glob pattern or regular expression to match request URL while routing. Only requests with URL matching the pattern + * will be surved from the HAR file. If not specified, all requests are served from the HAR file. + */ + urlFilter?: string|RegExp; + }; + /** * Specifies if viewport supports touch events. Defaults to false. */ @@ -14155,6 +14171,35 @@ export interface Electron { accuracy?: number; }; + /** + * If specified the network requests that are made in the context will be served from the HAR file. + * + * > NOTE: Playwright will not serve requests intercepted by Service Worker from the HAR file. 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);` + */ + har?: { + /** + * Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If the HAR file + * contains an entry with the matching URL and HTTP method, then the entry's headers, status and body will be used to + * fulfill the network request. An entry resulting in a redirect will be followed automatically. If `path` is a relative + * path, then it is resolved relative to the current working directory. + */ + path: string; + + /** + * If set to 'abort' any request not found in the HAR file will be aborted. If set to'continue' missing requests will be + * sent to the network. Defaults to 'abort'. + */ + fallback?: "abort"|"continue"; + + /** + * A glob pattern or regular expression to match request URL while routing. Only requests with URL matching the pattern + * will be surved from the HAR file. If not specified, all requests are served from the HAR file. + */ + urlFilter?: string|RegExp; + }; + /** * Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). */ @@ -15912,6 +15957,35 @@ export interface BrowserContextOptions { geolocation?: Geolocation; + /** + * If specified the network requests that are made in the context will be served from the HAR file. + * + * > NOTE: Playwright will not serve requests intercepted by Service Worker from the HAR file. 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);` + */ + har?: { + /** + * Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If the HAR file + * contains an entry with the matching URL and HTTP method, then the entry's headers, status and body will be used to + * fulfill the network request. An entry resulting in a redirect will be followed automatically. If `path` is a relative + * path, then it is resolved relative to the current working directory. + */ + path: string; + + /** + * If set to 'abort' any request not found in the HAR file will be aborted. If set to'continue' missing requests will be + * sent to the network. Defaults to 'abort'. + */ + fallback?: "abort"|"continue"; + + /** + * A glob pattern or regular expression to match request URL while routing. Only requests with URL matching the pattern + * will be surved from the HAR file. If not specified, all requests are served from the HAR file. + */ + urlFilter?: string|RegExp; + }; + /** * Specifies if viewport supports touch events. Defaults to false. */ diff --git a/tests/electron/electron-app.spec.ts b/tests/electron/electron-app.spec.ts index 5f6ed031f6ba9..5b021373bd182 100644 --- a/tests/electron/electron-app.spec.ts +++ b/tests/electron/electron-app.spec.ts @@ -191,3 +191,19 @@ test('should detach debugger on app-initiated exit', async ({ playwright }) => { }); await closePromise; }); + +test('should serve from HAR', async ({ playwright, asset }) => { + const harPath = asset('har-fulfill.har'); + const app = await playwright._electron.launch({ + args: [path.join(__dirname, 'electron-window-app.js')], + har: { path: harPath }, + }); + const page = await app.firstWindow(); + // await page.goto('https://playwright.dev/'); + await page.goto('http://no.playwright/'); + // HAR contains a redirect for the script that should be followed automatically. + expect(await page.evaluate('window.value')).toBe('foo'); + // HAR contains a POST for the css file that should not be used. + await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)'); + await app.close(); +}); diff --git a/tests/library/browsercontext-har.spec.ts b/tests/library/browsercontext-har.spec.ts new file mode 100644 index 0000000000000..8dce6eaa74294 --- /dev/null +++ b/tests/library/browsercontext-har.spec.ts @@ -0,0 +1,106 @@ +/** + * Copyright Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { browserTest as it, expect } from '../config/browserTest'; +import fs from 'fs'; + +it('should fulfill from har, matching the method and following redirects', async ({ contextFactory, isAndroid, asset }) => { + it.fixme(isAndroid); + + const path = asset('har-fulfill.har'); + const context = await contextFactory({ har: { path } }); + const page = await context.newPage(); + await page.goto('http://no.playwright/'); + // HAR contains a redirect for the script that should be followed automatically. + expect(await page.evaluate('window.value')).toBe('foo'); + // HAR contains a POST for the css file that should not be used. + await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)'); +}); + +it('fallback:continue should continue when not found in har', async ({ contextFactory, server, isAndroid, asset }) => { + it.fixme(isAndroid); + + const path = asset('har-fulfill.har'); + const context = await contextFactory({ har: { path, fallback: 'continue' } }); + const page = await context.newPage(); + await page.goto(server.PREFIX + '/one-style.html'); + await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)'); +}); + +it('by default should abort requests not found in har', async ({ contextFactory, server, isAndroid, asset }) => { + it.fixme(isAndroid); + + const path = asset('har-fulfill.har'); + const context = await contextFactory({ har: { path } }); + const page = await context.newPage(); + const error = await page.goto(server.EMPTY_PAGE).catch(e => e); + expect(error instanceof Error).toBe(true); +}); + +it('fallback:continue should continue requests on bad har', async ({ contextFactory, server, isAndroid }, testInfo) => { + it.fixme(isAndroid); + + const path = testInfo.outputPath('test.har'); + fs.writeFileSync(path, JSON.stringify({ log: {} }), 'utf-8'); + const context = await contextFactory({ har: { path, fallback: 'continue' } }); + const page = await context.newPage(); + await page.goto(server.PREFIX + '/one-style.html'); + await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)'); +}); + +it('should only handle requests matching url filter', async ({ contextFactory, isAndroid, asset }) => { + it.fixme(isAndroid); + + const path = asset('har-fulfill.har'); + const context = await contextFactory({ har: { path, urlFilter: '**/*.js' } }); + const page = await context.newPage(); + await context.route('http://no.playwright/', async route => { + expect(route.request().url()).toBe('http://no.playwright/'); + await route.fulfill({ + status: 200, + contentType: 'text/html', + body: '
hello
', + }); + }); + await page.goto('http://no.playwright/'); + // HAR contains a redirect for the script that should be followed automatically. + expect(await page.evaluate('window.value')).toBe('foo'); + await expect(page.locator('body')).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)'); +}); + +it('should support regex filter', async ({ contextFactory, isAndroid, asset }) => { + it.fixme(isAndroid); + + const path = asset('har-fulfill.har'); + const context = await contextFactory({ har: { path, urlFilter: /.*(\.js|.*\.css|no.playwright\/)$/ } }); + const page = await context.newPage(); + await page.goto('http://no.playwright/'); + expect(await page.evaluate('window.value')).toBe('foo'); + await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)'); +}); + +it('newPage should fulfill from har, matching the method and following redirects', async ({ browser, isAndroid, asset }) => { + it.fixme(isAndroid); + + const path = asset('har-fulfill.har'); + const page = await browser.newPage({ har: { path } }); + await page.goto('http://no.playwright/'); + // HAR contains a redirect for the script that should be followed automatically. + expect(await page.evaluate('window.value')).toBe('foo'); + // HAR contains a POST for the css file that should not be used. + await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)'); + await page.close(); +}); diff --git a/tests/library/browsercontext-route-from-har.spec.ts b/tests/library/browsercontext-route-from-har.spec.ts deleted file mode 100644 index f32addfe00af8..0000000000000 --- a/tests/library/browsercontext-route-from-har.spec.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Copyright Microsoft Corporation. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { browserTest as it, expect } from '../config/browserTest'; -import fs from 'fs'; - -it('routeFromHar should fulfill from har, matching the method and following redirects', async ({ context, page, isAndroid, asset }) => { - it.fixme(isAndroid); - - const harPath = asset('har-fulfill.har'); - await context.routeFromHar(harPath); - await page.goto('http://no.playwright/'); - // HAR contains a redirect for the script that should be followed automatically. - expect(await page.evaluate('window.value')).toBe('foo'); - // HAR contains a POST for the css file that should not be used. - await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)'); -}); - -it('routeFromHar strict:false should fallback when not found in har', async ({ context, page, server, isAndroid, asset }) => { - it.fixme(isAndroid); - - const harPath = asset('har-fulfill.har'); - let requestCount = 0; - await context.route('**/*', route => { - ++requestCount; - route.continue(); - }); - await context.routeFromHar(harPath, { strict: false }); - await page.goto(server.PREFIX + '/one-style.html'); - await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)'); - expect(requestCount).toBe(2); -}); - -it('routeFromHar by default should abort requests not found in har', async ({ context, page, server, isAndroid, asset }) => { - it.fixme(isAndroid); - - const harPath = asset('har-fulfill.har'); - let requestCount = 0; - await context.route('**/*', route => { - ++requestCount; - route.continue(); - }); - await context.routeFromHar(harPath); - const error = await page.goto(server.EMPTY_PAGE).catch(e => e); - expect(error instanceof Error).toBe(true); - expect(requestCount).toBe(0); -}); - -it('routeFromHar strict:false should continue requests on bad har', async ({ context, page, server, isAndroid }, testInfo) => { - it.fixme(isAndroid); - - const harPath = testInfo.outputPath('test.har'); - fs.writeFileSync(harPath, JSON.stringify({ log: {} }), 'utf-8'); - let requestCount = 0; - await context.route('**/*', route => { - ++requestCount; - route.continue(); - }); - await context.routeFromHar(harPath, { strict: false }); - await page.goto(server.PREFIX + '/one-style.html'); - expect(requestCount).toBe(2); -}); - -it('routeFromHar should only handle requests matching url filter', async ({ context, page, isAndroid, asset }) => { - it.fixme(isAndroid); - - const harPath = asset('har-fulfill.har'); - let fulfillCount = 0; - let passthroughCount = 0; - await context.route('**/*', async route => { - ++fulfillCount; - expect(route.request().url()).toBe('http://no.playwright/'); - await route.fulfill({ - status: 200, - contentType: 'text/html', - body: '
hello
', - }); - }); - await context.routeFromHar(harPath, { url: '**/*.js' }); - await context.route('**/*', route => { - ++passthroughCount; - route.fallback(); - }); - await page.goto('http://no.playwright/'); - // HAR contains a redirect for the script that should be followed automatically. - expect(await page.evaluate('window.value')).toBe('foo'); - expect(fulfillCount).toBe(1); - expect(passthroughCount).toBe(2); -}); - -it('routeFromHar should support mutliple calls with same path', async ({ context, page, isAndroid, asset }) => { - it.fixme(isAndroid); - - const harPath = asset('har-fulfill.har'); - let abortCount = 0; - await context.route('**/*', async route => { - ++abortCount; - await route.abort(); - }); - await context.routeFromHar(harPath, { url: '**/*.js' }); - await context.routeFromHar(harPath, { url: '**/*.css' }); - await context.routeFromHar(harPath, { url: /.*no.playwright\/$/ }); - await page.goto('http://no.playwright/'); - expect(await page.evaluate('window.value')).toBe('foo'); - expect(abortCount).toBe(0); -}); - -it('unrouteFromHar should remove har handler added with routeFromHar', async ({ context, page, server, isAndroid, asset }) => { - it.fixme(isAndroid); - - const harPath = asset('har-fulfill.har'); - let requestCount = 0; - await context.route('**/*', route => { - ++requestCount; - route.continue(); - }); - await context.routeFromHar(harPath, { strict: true }); - await context.unrouteFromHar(harPath); - await page.goto(server.EMPTY_PAGE); - expect(requestCount).toBe(1); -}); diff --git a/tests/library/defaultbrowsercontext-2.spec.ts b/tests/library/defaultbrowsercontext-2.spec.ts index 15b1f17507e92..db50890768b04 100644 --- a/tests/library/defaultbrowsercontext-2.spec.ts +++ b/tests/library/defaultbrowsercontext-2.spec.ts @@ -223,3 +223,16 @@ it('should connect to a browser with the default page', async ({ browserType,cre expect(context.pages().length).toBe(1); await context.close(); }); + +it('should support har option', async ({ isAndroid, launchPersistent, asset }) => { + it.fixme(isAndroid); + + const path = asset('har-fulfill.har'); + const { page } = await launchPersistent({ har: { path } }); + await page.goto('http://no.playwright/'); + // HAR contains a redirect for the script that should be followed automatically. + expect(await page.evaluate('window.value')).toBe('foo'); + // HAR contains a POST for the css file that should not be used. + await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)'); +}); + diff --git a/tests/page/page-request-fulfill.spec.ts b/tests/page/page-request-fulfill.spec.ts index 6fe278646b819..31efbc1ef5815 100644 --- a/tests/page/page-request-fulfill.spec.ts +++ b/tests/page/page-request-fulfill.spec.ts @@ -365,120 +365,3 @@ function findResponse(har: HARFile, url: string) { expect(entry, originalUrl).toBeTruthy(); return entry?.response; } - -it('routeFromHar should fulfill from har, matching the method and following redirects', async ({ page, isAndroid, asset }) => { - it.fixme(isAndroid); - - const harPath = asset('har-fulfill.har'); - await page.routeFromHar(harPath); - await page.goto('http://no.playwright/'); - // HAR contains a redirect for the script that should be followed automatically. - expect(await page.evaluate('window.value')).toBe('foo'); - // HAR contains a POST for the css file that should not be used. - await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)'); -}); - -it('routeFromHar strict:false should fallback when not found in har', async ({ page, server, isAndroid, asset }) => { - it.fixme(isAndroid); - - const harPath = asset('har-fulfill.har'); - let requestCount = 0; - await page.route('**/*', route => { - ++requestCount; - route.continue(); - }); - await page.routeFromHar(harPath, { strict: false }); - await page.goto(server.PREFIX + '/one-style.html'); - await expect(page.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)'); - expect(requestCount).toBe(2); -}); - -it('routeFromHar by default should abort requests not found in har', async ({ page, server, isAndroid, asset }) => { - it.fixme(isAndroid); - - const harPath = asset('har-fulfill.har'); - let requestCount = 0; - await page.route('**/*', route => { - ++requestCount; - route.continue(); - }); - await page.routeFromHar(harPath); - const error = await page.goto(server.EMPTY_PAGE).catch(e => e); - expect(error instanceof Error).toBe(true); - expect(requestCount).toBe(0); -}); - -it('routeFromHar strict:false should continue requests on bad har', async ({ page, server, isAndroid }, testInfo) => { - it.fixme(isAndroid); - - const harPath = testInfo.outputPath('test.har'); - fs.writeFileSync(harPath, JSON.stringify({ log: {} }), 'utf-8'); - let requestCount = 0; - await page.route('**/*', route => { - ++requestCount; - route.continue(); - }); - await page.routeFromHar(harPath, { strict: false }); - await page.goto(server.PREFIX + '/one-style.html'); - expect(requestCount).toBe(2); -}); - -it('routeFromHar should only handle requests matching url filter', async ({ page, server, isAndroid, asset }) => { - it.fixme(isAndroid); - - const harPath = asset('har-fulfill.har'); - let fulfillCount = 0; - let passthroughCount = 0; - await page.route('**/*', async route => { - ++fulfillCount; - expect(route.request().url()).toBe('http://no.playwright/'); - await route.fulfill({ - status: 200, - contentType: 'text/html', - body: '
hello
', - }); - }); - await page.routeFromHar(harPath, { url: '**/*.js' }); - await page.route('**/*', route => { - ++passthroughCount; - route.fallback(); - }); - await page.goto('http://no.playwright/'); - // HAR contains a redirect for the script that should be followed automatically. - expect(await page.evaluate('window.value')).toBe('foo'); - expect(fulfillCount).toBe(1); - expect(passthroughCount).toBe(2); -}); - -it('routeFromHar should support mutliple calls with same path', async ({ page, server, isAndroid, asset }) => { - it.fixme(isAndroid); - - const harPath = asset('har-fulfill.har'); - let abortCount = 0; - await page.route('**/*', async route => { - ++abortCount; - await route.abort(); - }); - await page.routeFromHar(harPath, { url: '**/*.js' }); - await page.routeFromHar(harPath, { url: '**/*.css' }); - await page.routeFromHar(harPath, { url: /.*no.playwright\/$/ }); - await page.goto('http://no.playwright/'); - expect(await page.evaluate('window.value')).toBe('foo'); - expect(abortCount).toBe(0); -}); - -it('unrouteFromHar should remove har handler added with routeFromHar', async ({ page, server, isAndroid, asset }) => { - it.fixme(isAndroid); - - const harPath = asset('har-fulfill.har'); - let requestCount = 0; - await page.route('**/*', route => { - ++requestCount; - route.continue(); - }); - await page.routeFromHar(harPath, { strict: true }); - await page.unrouteFromHar(harPath); - await page.goto(server.EMPTY_PAGE); - expect(requestCount).toBe(1); -}); -