Skip to content

Commit

Permalink
feat: newContext.har (#14892)
Browse files Browse the repository at this point in the history
Replaced {Page,BrowserContext}.(un)routeFromHar with browser.newContext.har.
  • Loading branch information
yury-s committed Jun 15, 2022
1 parent 225ab68 commit c349c1d
Show file tree
Hide file tree
Showing 17 changed files with 360 additions and 463 deletions.
38 changes: 0 additions & 38 deletions docs/src/api/class-browsercontext.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]>>
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/src/api/class-electron.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-%%
Expand Down
38 changes: 0 additions & 38 deletions docs/src/api/class-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]>

Expand Down Expand Up @@ -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]>
Expand Down
35 changes: 35 additions & 0 deletions docs/src/api/params.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]>

Expand Down Expand Up @@ -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-%%
Expand Down
3 changes: 3 additions & 0 deletions packages/playwright-core/src/client/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<channels.BrowserChannel> implements api.Browser {
readonly _contexts = new Set<BrowserContext>();
Expand Down Expand Up @@ -60,12 +61,14 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap

async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
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;
}
Expand Down
10 changes: 0 additions & 10 deletions packages/playwright-core/src/client/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<channels.BrowserContextChannel> implements api.BrowserContext {
_pages = new Set<Page>();
Expand All @@ -52,7 +51,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
_ownerPage: Page | undefined;
private _closedPromise: Promise<void>;
_options: channels.BrowserNewContextParams = { };
private readonly _harRouter = new HarRouter(this);

readonly request: APIRequestContext;
readonly tracing: Tracing;
Expand Down Expand Up @@ -275,14 +273,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
await this._disableInterception();
}

async routeFromHar(harPath: string, options?: { strict?: boolean; url?: string|RegExp; }): Promise<void> {
await this._harRouter.routeFromHar(harPath, options);
}

async unrouteFromHar(harPath: string): Promise<void> {
await this._harRouter.unrouteFromHar(harPath);
}

async _unrouteAll() {
this._routes = [];
await this._disableInterception();
Expand Down
3 changes: 3 additions & 0 deletions packages/playwright-core/src/client/browserType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<api.BrowserServer>;
Expand Down Expand Up @@ -94,6 +95,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> 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,
Expand All @@ -108,6 +110,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
context._options = contextParams;
context._logger = logger;
context._setBrowserType(this);
harRouter?.addRoute(context);
await this._onDidCreateContext?.(context);
return context;
}
Expand Down
6 changes: 5 additions & 1 deletion packages/playwright-core/src/client/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<channels.ElectronLaunchOptions, 'env'|'extraHTTPHeaders'> & {
env?: Env,
extraHTTPHeaders?: Headers,
har?: BrowserContextOptions['har']
};

type ElectronAppType = typeof import('electron');
Expand All @@ -52,8 +54,10 @@ export class Electron extends ChannelOwner<channels.ElectronChannel> 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;
}
}
Expand Down
72 changes: 28 additions & 44 deletions packages/playwright-core/src/client/harRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BrowserContextOptions['har']>;

export class HarRouter {
private _harPathToHandlers: Map<string, HarHandler[]> = new Map();
private readonly owner: BrowserContext | Page;
private _pattern: string | RegExp;
private _handler: (route: Route) => Promise<void>;

constructor(owner: BrowserContext | Page) {
this.owner = owner;
static async create(options: HarOptions): Promise<HarRouter> {
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<void> {
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<void> {
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);
}
}

Expand Down
10 changes: 0 additions & 10 deletions packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -98,7 +97,6 @@ export class Page extends ChannelOwner<channels.PageChannel> 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;
Expand Down Expand Up @@ -475,14 +473,6 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
await this._disableInterception();
}

async routeFromHar(harPath: string, options?: { strict?: boolean; url?: string|RegExp; }): Promise<void> {
await this._harRouter.routeFromHar(harPath, options);
}

async unrouteFromHar(harPath: string): Promise<void> {
await this._harRouter.unrouteFromHar(harPath);
}

async _unrouteAll() {
this._routes = [];
await this._disableInterception();
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright-core/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'vie
videosPath?: string,
videoSize?: Size,
storageState?: string | SetStorageState,
har?: {
path: string;
fallback?: 'abort'|'continue';
urlFilter?: string|RegExp;
},
recordHar?: {
path: string,
omitContent?: boolean,
Expand Down

0 comments on commit c349c1d

Please sign in to comment.