cherry-pick(#15024): feat(har): re-add routeFromHAR (#15047)

Co-authored-by: Pavel Feldman <pavel.feldman@gmail.com>
This commit is contained in:
Dmitry Gozman 2022-06-22 12:33:19 -07:00 committed by GitHub
parent 82769c23de
commit c840a28946
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 234 additions and 291 deletions

View file

@ -1025,6 +1025,30 @@ 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
If specified the network requests that are made in the context will be served from the HAR file. Read more about [Replaying from HAR](../network.md#replaying-from-har).
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 by setting [`option: Browser.newContext.serviceWorkers`] to `'block'`.
### param: BrowserContext.routeFromHAR.har
- `har` <[path]>
Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If `path` is a relative path, then it is resolved relative to the current working directory.
### option: BrowserContext.routeFromHAR.fallback
- `notFound` ?<[HarNotFound]<"abort"|"fallback">>
* If set to 'abort' any request not found in the HAR file will be aborted.
* If set to 'fallback' falls through to the next route handler in the handler chain.
Defaults to abort.
### option: BrowserContext.routeFromHAR.url
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
A glob pattern, regular expression or predicate to match the request URL. 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]>>

View file

@ -94,7 +94,6 @@ 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-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-%%

View file

@ -2732,6 +2732,30 @@ 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
If specified the network requests that are made in the page will be served from the HAR file. Read more about [Replaying from HAR](../network.md#replaying-from-har).
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 by setting [`option: Browser.newContext.serviceWorkers`] to `'block'`.
### param: Page.routeFromHAR.har
- `har` <[path]>
Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If `path` is a relative path, then it is resolved relative to the current working directory.
### option: Page.routeFromHAR.notFound
- `notFound` ?<[HarNotFound]<"abort"|"fallback">>
* If set to 'abort' any request not found in the HAR file will be aborted.
* If set to 'fallback' missing requests will be sent to the network.
Defaults to abort.
### option: Page.routeFromHAR.url
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
A glob pattern, regular expression or predicate to match the request URL. 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]>

View file

@ -247,37 +247,6 @@ 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-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 `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. Read more about [Replaying from HAR](../network.md#replaying-from-har).
:::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 by setting [`option: Browser.newContext.serviceWorkers`] to `'block'`.
:::
## csharp-java-python-context-option-har-path
* langs: csharp, java, python
- `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-python-context-option-har-fallback
* langs: csharp, java, python
- `harFallback` ?<[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-python-context-option-har-urlfilter
* langs: csharp, java, python
- `harUrlFilter` ?<[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]>
@ -834,10 +803,6 @@ An acceptable perceived color difference in the [YIQ color space](https://en.wik
- %%-context-option-logger-%%
- %%-context-option-videospath-%%
- %%-context-option-videosize-%%
- %%-js-context-option-har-%%
- %%-csharp-java-python-context-option-har-path-%%
- %%-csharp-java-python-context-option-har-fallback-%%
- %%-csharp-java-python-context-option-har-urlfilter-%%
- %%-context-option-recordhar-%%
- %%-context-option-recordhar-path-%%
- %%-context-option-recordhar-omit-content-%%

View file

@ -780,61 +780,43 @@ await context.CloseAsync();
### Replaying from HAR
Pass [`option: har`] option to the [`method: Browser.newContext`] method to use matching responses from the [HAR](http://www.softwareishard.com/blog/har-12-spec/) file.
Use [`method: Page.routeFromHAR`] or [`method: BrowserContext.routeFromHAR`] to serve matching responses from the [HAR](http://www.softwareishard.com/blog/har-12-spec/) file.
```js
// Replay API requests from HAR.
// Either use a matching response from the HAR,
// or abort the request if nothing matches.
const context = await browser.newContext({ har: { path: 'example.har' } });
await page.routeFromHAR('example.har');
```
```java
// Either use a matching response from the HAR,
// or abort the request if nothing matches.
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setHarPath(Paths.get("example.har")));
Page page = context.newPage();
page.navigate("https://example.com");
page.routeFromHAR(Paths.get("example.har"));
```
```python async
# Either use a matching response from the HAR,
# or abort the request if nothing matches.
context = await browser.new_context(
har_path = "example.har"
)
page = await context.new_page()
await page.goto("https://example.com")
await page.routeFromHAR("example.har")
```
```python sync
# Either use a matching response from the HAR,
# or abort the request if nothing matches.
context = browser.new_context(
har_path="example.har"
)
page = context.new_page()
page.goto("https://example.com")
page.routeFromHAR("example.har")
```
```csharp
// Either use a matching response from the HAR,
// or abort the request if nothing matches.
var context = await Browser.NewContextAsync(new () {
HarPath = "example.har"
});
var page = await context.NewPageAsync();
await page.GotoAsync("https://example.com");
await context.RouteFromHARAsync("example.har");
```
HAR replay matches URL and HTTP method strictly. For POST requests, it also matches POST payloads strictly. If multiple recordings match a request, the one with the most matching headers is picked. An entry resulting in a redirect will be followed automatically.
Similar to when recording, if given HAR file name ends with `.zip`, it is considered an archive containing the HAR file along with network payloads stored as separate entries. You can also extract this archive, edit payloads or HAR log manually and point to the extracted har file. All the payloads will be resolved relative to the extracted har file on the file system.
### API reference
- [`method: Browser.newContext`]
- [`method: Route.fulfill`]
<br/>
## WebSockets

View file

@ -129,8 +129,6 @@ Options used to create the context, as passed to [`method: Browser.newContext`].
## property: TestOptions.geolocation = %%-context-option-geolocation-%%
## property: TestOptions.har = %%-js-context-option-har-%%
## property: TestOptions.hasTouch = %%-context-option-hastouch-%%
## property: TestOptions.headless = %%-browser-option-headless-%%

View file

@ -24,7 +24,6 @@ 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>();
@ -61,14 +60,12 @@ 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(this._connection.localUtils(), 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;
}

View file

@ -40,6 +40,7 @@ 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>();
@ -267,6 +268,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
await this._channel.setNetworkInterceptionEnabled({ enabled: true });
}
async routeFromHAR(har: string, options: { url?: URLMatch, notFound?: 'abort' | 'fallback' } = {}): Promise<void> {
const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url });
harRouter.addContextRoute(this);
}
async unroute(url: URLMatch, handler?: network.RouteHandlerCallback): Promise<void> {
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
if (!this._routes.length)

View file

@ -28,7 +28,6 @@ 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>;
@ -95,7 +94,6 @@ 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(this._connection.localUtils(), options.har) : null;
const contextParams = await prepareBrowserContextParams(options);
const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = {
...contextParams,
@ -110,7 +108,6 @@ 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;
}

View file

@ -28,12 +28,10 @@ import { JSHandle, parseResult, serializeArgument } from './jsHandle';
import type { Page } from './page';
import type { Env, WaitForEventOptions, Headers, BrowserContextOptions } from './types';
import { Waiter } from './waiter';
import { HarRouter } from './harRouter';
type ElectronOptions = Omit<channels.ElectronLaunchOptions, 'env'|'extraHTTPHeaders'|'recordHar'> & {
env?: Env,
extraHTTPHeaders?: Headers,
har?: BrowserContextOptions['har'],
recordHar?: BrowserContextOptions['recordHar'],
};
@ -53,10 +51,8 @@ export class Electron extends ChannelOwner<channels.ElectronChannel> implements
...await prepareBrowserContextParams(options),
env: envObjectToArray(options.env ? options.env : process.env),
};
const harRouter = options.har ? await HarRouter.create(this._connection.localUtils(), options.har) : null;
const app = ElectronApplication.from((await this._channel.launch(params)).electronApplication);
app._context._options = params;
harRouter?.addRoute(app._context);
return app;
}
}

View file

@ -19,32 +19,34 @@ import type { BrowserContext } from './browserContext';
import { Events } from './events';
import type { LocalUtils } from './localUtils';
import type { Route } from './network';
import type { BrowserContextOptions } from './types';
import type { URLMatch } from './types';
import type { Page } from './page';
type HarOptions = NonNullable<BrowserContextOptions['har']>;
type HarNotFoundAction = 'abort' | 'fallback';
export class HarRouter {
private _pattern: string | RegExp;
private _options: HarOptions | undefined;
private _localUtils: LocalUtils;
private _harId: string;
private _notFoundAction: HarNotFoundAction;
private _options: { urlMatch?: URLMatch; baseURL?: string; };
static async create(localUtils: LocalUtils, options: HarOptions): Promise<HarRouter> {
const { harId, error } = await localUtils._channel.harOpen({ file: options.path });
static async create(localUtils: LocalUtils, file: string, notFoundAction: HarNotFoundAction, options: { urlMatch?: URLMatch }): Promise<HarRouter> {
const { harId, error } = await localUtils._channel.harOpen({ file });
if (error)
throw new Error(error);
return new HarRouter(localUtils, harId!, options);
return new HarRouter(localUtils, harId!, notFoundAction, options);
}
constructor(localUtils: LocalUtils, harId: string, options?: HarOptions) {
constructor(localUtils: LocalUtils, harId: string, notFoundAction: HarNotFoundAction, options: { urlMatch?: URLMatch }) {
this._localUtils = localUtils;
this._harId = harId;
this._pattern = options?.urlFilter ?? /.*/;
this._options = options;
this._notFoundAction = notFoundAction;
}
private async _handle(route: Route) {
const request = route.request();
const response = await this._localUtils._channel.harLookup({
harId: this._harId,
url: request.url(),
@ -73,20 +75,24 @@ export class HarRouter {
debugLogger.log('api', 'HAR: ' + response.message!);
// Report the error, but fall through to the default handler.
if (this._options?.fallback === 'continue') {
await route.fallback();
if (this._notFoundAction === 'abort') {
await route.abort();
return;
}
debugLogger.log('api', `HAR: ${route.request().method()} ${route.request().url()} aborted - no such entry in HAR file`);
await route.abort();
await route.fallback();
}
async addRoute(context: BrowserContext) {
await context.route(this._pattern, route => this._handle(route));
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());
}
dispose() {
this._localUtils._channel.harClose({ harId: this._harId }).catch(() => {});
}

View file

@ -43,6 +43,7 @@ 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';
@ -465,6 +466,11 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
await this._channel.setNetworkInterceptionEnabled({ enabled: true });
}
async routeFromHAR(har: string, options: { url?: URLMatch, notFound?: 'abort' | 'fallback' } = {}): Promise<void> {
const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url });
harRouter.addPageRoute(this);
}
async unroute(url: URLMatch, handler?: RouteHandlerCallback): Promise<void> {
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
if (!this._routes.length)

View file

@ -33,6 +33,7 @@ export class HarRecorder {
private _tracer: HarTracer;
private _entries: har.Entry[] = [];
private _zipFile: ZipFile | null = null;
private _writtenZipEntries = new Set<string>();
constructor(context: BrowserContext, options: channels.RecordHarOptions) {
this._artifact = new Artifact(context, path.join(context._browser.options.artifactsDir, `${createGuid()}.har`));
@ -57,7 +58,9 @@ export class HarRecorder {
}
onContentBlob(sha1: string, buffer: Buffer) {
if (this._zipFile)
if (!this._zipFile || this._writtenZipEntries.has(sha1))
return;
this._writtenZipEntries.add(sha1);
this._zipFile!.addBuffer(buffer, sha1);
}

View file

@ -3167,6 +3167,32 @@ export interface Page {
times?: number;
}): Promise<void>;
/**
* If specified the network requests that are made in the page will be served from the HAR file. Read more about
* [Replaying from HAR](https://playwright.dev/docs/network#replaying-from-har).
*
* 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 by setting `Browser.newContext.serviceWorkers` to `'block'`.
* @param har Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If `path` is a relative path, then it is resolved relative to the current working directory.
* @param options
*/
routeFromHAR(har: string, options?: {
/**
* - If set to 'abort' any request not found in the HAR file will be aborted.
* - If set to 'fallback' missing requests will be sent to the network.
*
* Defaults to abort.
*/
notFound?: "abort"|"fallback";
/**
* A glob pattern, regular expression or predicate to match the request URL. 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|((url: URL) => boolean);
}): Promise<void>;
/**
* Returns the buffer with the captured screenshot.
* @param options
@ -7093,6 +7119,32 @@ export interface BrowserContext {
times?: number;
}): Promise<void>;
/**
* If specified the network requests that are made in the context will be served from the HAR file. Read more about
* [Replaying from HAR](https://playwright.dev/docs/network#replaying-from-har).
*
* 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 by setting `Browser.newContext.serviceWorkers` to `'block'`.
* @param har Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If `path` is a relative path, then it is resolved relative to the current working directory.
* @param options
*/
routeFromHAR(har: string, options?: {
/**
* - If set to 'abort' any request not found in the HAR file will be aborted.
* - If set to 'fallback' falls through to the next route handler in the handler chain.
*
* Defaults to abort.
*/
notFound?: "abort"|"fallback";
/**
* A glob pattern, regular expression or predicate to match the request URL. 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|((url: URL) => boolean);
}): Promise<void>;
/**
* > NOTE: Service workers are only supported on Chromium-based browsers.
*
@ -10507,34 +10559,6 @@ export interface BrowserType<Unused = {}> {
*/
handleSIGTERM?: boolean;
/**
* If specified the network requests that are made in the context will be served from the HAR file. Read more about
* [Replaying from HAR](https://playwright.dev/docs/network#replaying-from-har).
*
* > 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 by setting `Browser.newContext.serviceWorkers` to `'block'`.
*/
har?: {
/**
* Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. 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.
*/
@ -11762,34 +11786,6 @@ export interface AndroidDevice {
accuracy?: number;
};
/**
* If specified the network requests that are made in the context will be served from the HAR file. Read more about
* [Replaying from HAR](https://playwright.dev/docs/network#replaying-from-har).
*
* > 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 by setting `Browser.newContext.serviceWorkers` to `'block'`.
*/
har?: {
/**
* Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. 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.
*/
@ -13330,34 +13326,6 @@ 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. Read more about
* [Replaying from HAR](https://playwright.dev/docs/network#replaying-from-har).
*
* > 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 by setting `Browser.newContext.serviceWorkers` to `'block'`.
*/
har?: {
/**
* Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. 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.
*/
@ -14202,34 +14170,6 @@ export interface Electron {
accuracy?: number;
};
/**
* If specified the network requests that are made in the context will be served from the HAR file. Read more about
* [Replaying from HAR](https://playwright.dev/docs/network#replaying-from-har).
*
* > 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 by setting `Browser.newContext.serviceWorkers` to `'block'`.
*/
har?: {
/**
* Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. 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).
*/
@ -15995,34 +15935,6 @@ export interface BrowserContextOptions {
geolocation?: Geolocation;
/**
* If specified the network requests that are made in the context will be served from the HAR file. Read more about
* [Replaying from HAR](https://playwright.dev/docs/network#replaying-from-har).
*
* > 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 by setting `Browser.newContext.serviceWorkers` to `'block'`.
*/
har?: {
/**
* Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. 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.
*/

View file

@ -141,7 +141,6 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
deviceScaleFactor: [ undefined, { option: true } ],
extraHTTPHeaders: [ undefined, { option: true } ],
geolocation: [ undefined, { option: true } ],
har: [undefined, { option: true }],
hasTouch: [ undefined, { option: true } ],
httpCredentials: [ undefined, { option: true } ],
ignoreHTTPSErrors: [ undefined, { option: true } ],
@ -169,7 +168,6 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
colorScheme,
deviceScaleFactor,
extraHTTPHeaders,
har,
hasTouch,
geolocation,
httpCredentials,
@ -201,8 +199,6 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
options.extraHTTPHeaders = extraHTTPHeaders;
if (geolocation !== undefined)
options.geolocation = geolocation;
if (har !== undefined)
options.har = har;
if (hasTouch !== undefined)
options.hasTouch = hasTouch;
if (httpCredentials !== undefined)

View file

@ -2492,7 +2492,6 @@ type BrowserName = 'chromium' | 'firefox' | 'webkit';
type BrowserChannel = Exclude<LaunchOptions['channel'], undefined>;
type ColorScheme = Exclude<BrowserContextOptions['colorScheme'], undefined>;
type ExtraHTTPHeaders = Exclude<BrowserContextOptions['extraHTTPHeaders'], undefined>;
type HAROptions = Exclude<BrowserContextOptions['har'], undefined>;
type Proxy = Exclude<BrowserContextOptions['proxy'], undefined>;
type StorageState = Exclude<BrowserContextOptions['storageState'], undefined>;
type ServiceWorkerPolicy = Exclude<BrowserContextOptions['serviceWorkers'], undefined>;
@ -2700,15 +2699,6 @@ export interface PlaywrightTestOptions {
*/
extraHTTPHeaders: ExtraHTTPHeaders | undefined;
geolocation: Geolocation | undefined;
/**
* If specified the network requests that are made in the context will be served from the HAR file. Read more about
* [Replaying from HAR](https://playwright.dev/docs/network#replaying-from-har).
*
* > 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 by setting `Browser.newContext.serviceWorkers` to `'block'`.
*/
har: HAROptions | undefined;
/**
* Specifies if viewport supports touch events. Defaults to false.
*/

View file

@ -196,8 +196,8 @@ 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 },
});
app.context().routeFromHAR(harPath);
const page = await app.firstWindow();
// await page.goto('https://playwright.dev/');
await page.goto('http://no.playwright/');

View file

@ -19,11 +19,11 @@ import fs from 'fs';
import path from 'path';
import extractZip from '../../packages/playwright-core/bundles/zip/node_modules/extract-zip';
it('should fulfill from har, matching the method and following redirects', async ({ contextFactory, isAndroid, asset }) => {
it('should context.routeFromHAR, matching the method and following redirects', async ({ context, isAndroid, asset }) => {
it.fixme(isAndroid);
const path = asset('har-fulfill.har');
const context = await contextFactory({ har: { path } });
await context.routeFromHAR(path);
const page = await context.newPage();
await page.goto('http://no.playwright/');
// HAR contains a redirect for the script that should be followed automatically.
@ -32,42 +32,55 @@ it('should fulfill from har, matching the method and following redirects', async
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('should page.routeFromHAR, matching the method and following redirects', async ({ context, 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.routeFromHAR(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)');
});
it('fallback:continue should continue when not found in har', async ({ context, server, isAndroid, asset }) => {
it.fixme(isAndroid);
const path = asset('har-fulfill.har');
await context.routeFromHAR(path, { notFound: 'fallback' });
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('by default should abort requests not found in har', async ({ context, server, isAndroid, asset }) => {
it.fixme(isAndroid);
const path = asset('har-fulfill.har');
const context = await contextFactory({ har: { path } });
await context.routeFromHAR(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('fallback:continue should continue requests on bad har', async ({ context, 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' } });
await context.routeFromHAR(path, { notFound: 'fallback' });
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('should only handle requests matching url filter', async ({ context, isAndroid, asset }) => {
it.fixme(isAndroid);
const path = asset('har-fulfill.har');
const context = await contextFactory({ har: { path, urlFilter: '**/*.js' } });
await context.routeFromHAR(path, { notFound: 'fallback', url: '**/*.js' });
const page = await context.newPage();
await context.route('http://no.playwright/', async route => {
expect(route.request().url()).toBe('http://no.playwright/');
@ -83,11 +96,51 @@ it('should only handle requests matching url filter', async ({ contextFactory, i
await expect(page.locator('body')).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
});
it('should support regex filter', async ({ contextFactory, isAndroid, asset }) => {
it('should only context.routeFromHAR requests matching url filter', async ({ context, isAndroid, asset }) => {
it.fixme(isAndroid);
const path = asset('har-fulfill.har');
const context = await contextFactory({ har: { path, urlFilter: /.*(\.js|.*\.css|no.playwright\/)$/ } });
await context.routeFromHAR(path, { url: '**/*.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: '<script src="./script.js"></script><div>hello</div>',
});
});
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 only page.routeFromHAR requests matching url filter', async ({ context, isAndroid, asset }) => {
it.fixme(isAndroid);
const path = asset('har-fulfill.har');
const page = await context.newPage();
await page.routeFromHAR(path, { url: '**/*.js' });
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: '<script src="./script.js"></script><div>hello</div>',
});
});
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 ({ context, isAndroid, asset }) => {
it.fixme(isAndroid);
const path = asset('har-fulfill.har');
await context.routeFromHAR(path, { url: /.*(\.js|.*\.css|no.playwright\/)$/ });
const page = await context.newPage();
await page.goto('http://no.playwright/');
expect(await page.evaluate('window.value')).toBe('foo');
@ -98,7 +151,8 @@ it('newPage should fulfill from har, matching the method and following redirects
it.fixme(isAndroid);
const path = asset('har-fulfill.har');
const page = await browser.newPage({ har: { path } });
const page = await browser.newPage();
await page.routeFromHAR(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');
@ -107,11 +161,11 @@ it('newPage should fulfill from har, matching the method and following redirects
await page.close();
});
it('should change document URL after redirected navigation', async ({ contextFactory, isAndroid, asset }) => {
it('should change document URL after redirected navigation', async ({ context, isAndroid, asset }) => {
it.fixme(isAndroid);
const path = asset('har-redirect.har');
const context = await contextFactory({ har: { path } });
await context.routeFromHAR(path);
const page = await context.newPage();
const [response] = await Promise.all([
page.waitForNavigation(),
@ -123,11 +177,11 @@ it('should change document URL after redirected navigation', async ({ contextFac
expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/');
});
it('should change document URL after redirected navigation on click', async ({ server, contextFactory, isAndroid, asset }) => {
it('should change document URL after redirected navigation on click', async ({ server, context, isAndroid, asset }) => {
it.fixme(isAndroid);
const path = asset('har-redirect.har');
const context = await contextFactory({ har: { path, urlFilter: /.*theverge.*/ } });
await context.routeFromHAR(path, { url: /.*theverge.*/ });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
await page.setContent(`<a href="https://theverge.com/">click me</a>`);
@ -140,11 +194,11 @@ it('should change document URL after redirected navigation on click', async ({ s
expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/');
});
it('should goBack to redirected navigation', async ({ contextFactory, isAndroid, asset, server }) => {
it('should goBack to redirected navigation', async ({ context, isAndroid, asset, server }) => {
it.fixme(isAndroid);
const path = asset('har-redirect.har');
const context = await contextFactory({ har: { path, urlFilter: /.*theverge.*/ } });
await context.routeFromHAR(path, { url: /.*theverge.*/ });
const page = await context.newPage();
await page.goto('https://theverge.com/');
await page.goto(server.EMPTY_PAGE);
@ -155,11 +209,11 @@ it('should goBack to redirected navigation', async ({ contextFactory, isAndroid,
expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/');
});
it('should goForward to redirected navigation', async ({ contextFactory, isAndroid, asset, server }) => {
it('should goForward to redirected navigation', async ({ context, isAndroid, asset, server }) => {
it.fixme(isAndroid);
const path = asset('har-redirect.har');
const context = await contextFactory({ har: { path, urlFilter: /.*theverge.*/ } });
await context.routeFromHAR(path, { url: /.*theverge.*/ });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
await expect(page).toHaveURL(server.EMPTY_PAGE);
@ -173,11 +227,11 @@ it('should goForward to redirected navigation', async ({ contextFactory, isAndro
expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/');
});
it('should reload redirected navigation', async ({ contextFactory, isAndroid, asset, server }) => {
it('should reload redirected navigation', async ({ context, isAndroid, asset, server }) => {
it.fixme(isAndroid);
const path = asset('har-redirect.har');
const context = await contextFactory({ har: { path, urlFilter: /.*theverge.*/ } });
await context.routeFromHAR(path, { url: /.*theverge.*/ });
const page = await context.newPage();
await page.goto('https://theverge.com/');
await expect(page).toHaveURL('https://www.theverge.com/');
@ -187,11 +241,11 @@ it('should reload redirected navigation', async ({ contextFactory, isAndroid, as
expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/');
});
it('should fulfill from har with content in a file', async ({ contextFactory, isAndroid, asset }) => {
it('should fulfill from har with content in a file', async ({ context, isAndroid, asset }) => {
it.fixme(isAndroid);
const path = asset('har-sha1.har');
const context = await contextFactory({ har: { path } });
await context.routeFromHAR(path);
const page = await context.newPage();
await page.goto('http://no.playwright/');
expect(await page.content()).toBe('<html><head></head><body>Hello, world</body></html>');
@ -206,7 +260,8 @@ it('should round-trip har.zip', async ({ contextFactory, isAndroid, server }, te
await page1.goto(server.PREFIX + '/one-style.html');
await context1.close();
const context2 = await contextFactory({ har: { path: harPath, fallback: 'abort' } });
const context2 = await contextFactory();
await context2.routeFromHAR(harPath, { notFound: 'abort' });
const page2 = await context2.newPage();
await page2.goto(server.PREFIX + '/one-style.html');
expect(await page2.content()).toContain('hello, world!');
@ -225,7 +280,8 @@ it('should round-trip extracted har.zip', async ({ contextFactory, isAndroid, se
const harDir = testInfo.outputPath('hardir');
await extractZip(harPath, { dir: harDir });
const context2 = await contextFactory({ har: { path: path.join(harDir, 'har.har') } });
const context2 = await contextFactory();
await context2.routeFromHAR(path.join(harDir, 'har.har'));
const page2 = await context2.newPage();
await page2.goto(server.PREFIX + '/one-style.html');
expect(await page2.content()).toContain('hello, world!');
@ -253,7 +309,8 @@ it('should round-trip har with postData', async ({ contextFactory, isAndroid, se
expect(await page1.evaluate(fetchFunction, '3')).toBe('3');
await context1.close();
const context2 = await contextFactory({ har: { path: harPath } });
const context2 = await contextFactory();
await context2.routeFromHAR(harPath);
const page2 = await context2.newPage();
await page2.goto(server.EMPTY_PAGE);
expect(await page2.evaluate(fetchFunction, '1')).toBe('1');
@ -292,7 +349,8 @@ it('should disambiguate by header', async ({ contextFactory, isAndroid, server }
expect(await page1.evaluate(fetchFunction, 'baz3')).toBe('baz3');
await context1.close();
const context2 = await contextFactory({ har: { path: harPath } });
const context2 = await contextFactory();
await context2.routeFromHAR(harPath);
const page2 = await context2.newPage();
await page2.goto(server.EMPTY_PAGE);
expect(await page2.evaluate(fetchFunction, 'baz1')).toBe('baz1');

View file

@ -228,7 +228,8 @@ it('should support har option', async ({ isAndroid, launchPersistent, asset }) =
it.fixme(isAndroid);
const path = asset('har-fulfill.har');
const { page } = await launchPersistent({ har: { path } });
const { page } = await launchPersistent();
await page.routeFromHAR(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');

View file

@ -602,18 +602,3 @@ test('should pass fixture defaults to tests', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});
test('should support har option', async ({ runInlineTest, asset }) => {
const result = await runInlineTest({
'a.test.ts': `
const { test } = pwt;
test.use({ har: { path: ${JSON.stringify(asset('har-fulfill.har'))} }});
test('pass', async ({ page }) => {
await page.goto('http://no.playwright/');
expect(await page.evaluate('window.value')).toBe('foo');
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});

View file

@ -173,7 +173,6 @@ type BrowserName = 'chromium' | 'firefox' | 'webkit';
type BrowserChannel = Exclude<LaunchOptions['channel'], undefined>;
type ColorScheme = Exclude<BrowserContextOptions['colorScheme'], undefined>;
type ExtraHTTPHeaders = Exclude<BrowserContextOptions['extraHTTPHeaders'], undefined>;
type HAROptions = Exclude<BrowserContextOptions['har'], undefined>;
type Proxy = Exclude<BrowserContextOptions['proxy'], undefined>;
type StorageState = Exclude<BrowserContextOptions['storageState'], undefined>;
type ServiceWorkerPolicy = Exclude<BrowserContextOptions['serviceWorkers'], undefined>;
@ -216,7 +215,6 @@ export interface PlaywrightTestOptions {
deviceScaleFactor: number | undefined;
extraHTTPHeaders: ExtraHTTPHeaders | undefined;
geolocation: Geolocation | undefined;
har: HAROptions | undefined;
hasTouch: boolean | undefined;
httpCredentials: HTTPCredentials | undefined;
ignoreHTTPSErrors: boolean | undefined;