chore: allow updating har while routing (#15197)

This commit is contained in:
Pavel Feldman 2022-06-28 15:09:36 -07:00 committed by GitHub
parent 51fd212906
commit 6a8d835145
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 270 additions and 46 deletions

View File

@ -1044,8 +1044,13 @@ Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerec
Defaults to abort.
### option: BrowserContext.routeFromHAR.update
- `update` ?<boolean>
If specified, updates the given HAR with the actual network information instead of serving from file.
### option: BrowserContext.routeFromHAR.url
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
- `url` <[string]|[RegExp]>
A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern will be served from the HAR file. If not specified, all requests are served from the HAR file.

View File

@ -2751,8 +2751,13 @@ Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerec
Defaults to abort.
### option: Page.routeFromHAR.update
- `update` ?<boolean>
If specified, updates the given HAR with the actual network information instead of serving from file.
### option: Page.routeFromHAR.url
- `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]>
- `url` <[string]|[RegExp]>
A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern will be served from the HAR file. If not specified, all requests are served from the HAR file.

View File

@ -561,8 +561,8 @@ Logger sink for Playwright logging.
- `recordHar` <[Object]>
- `omitContent` ?<[boolean]> Optional setting to control whether to omit request content from the HAR. Defaults to
`false`. Deprecated, use `content` policy instead.
- `content` ?<[HarContentPolicy]<"omit"|"embed"|"attach">> Optional setting to control resource content management. If `omit` is specified, content is not persisted. If `attach` is specified, resources are persistet as separate files and all of these files are archived along with the HAR file. Defaults to `embed`, which stores content inline the HAR file as per HAR specification.
- `path` <[path]> Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `attach` mode is used by default.
- `content` ?<[HarContentPolicy]<"omit"|"embed"|"attach">> Optional setting to control resource content management. If `omit` is specified, content is not persisted. If `attach` is specified, resources are persistet as separate files or entries in the ZIP archive. If `embed` is specified, content is stored inline the HAR file as per HAR specification. Defaults to `attach` for `.zip` output files and to `embed` for all other file extensions.
- `path` <[path]> Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `content: 'attach'` is used by default.
- `mode` ?<[HarMode]<"full"|"minimal">> When set to `minimal`, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies, security and other types of HAR information that are not used when replaying from HAR. Defaults to `full`.
- `urlFilter` ?<[string]|[RegExp]> A glob or regex pattern to filter requests that are stored in the HAR. When a [`option: baseURL`] via the context options was provided and the passed URL is a path, it gets merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.

View File

@ -58,6 +58,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
readonly _backgroundPages = new Set<Page>();
readonly _serviceWorkers = new Set<Worker>();
readonly _isChromium: boolean;
private _harRecorders = new Map<string, { path: string, content: 'embed' | 'attach' | 'omit' | undefined }>();
static from(context: channels.BrowserContextChannel): BrowserContext {
return (context as any)._object;
@ -100,6 +101,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
_setBrowserType(browserType: BrowserType) {
this._browserType = browserType;
browserType._contexts.add(this);
if (this._options.recordHar)
this._harRecorders.set('', { path: this._options.recordHar.path, content: this._options.recordHar.content });
}
private _onPage(page: Page): void {
@ -270,7 +273,24 @@ 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> {
async _recordIntoHAR(har: string, page: Page | null, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean } = {}): Promise<void> {
const { harId } = await this._channel.harStart({
page: page?._channel,
options: prepareRecordHarOptions({
path: har,
content: 'attach',
mode: 'minimal',
urlFilter: options.url
})!
});
this._harRecorders.set(harId, { path: har, content: 'attach' });
}
async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean } = {}): Promise<void> {
if (options.update) {
await this._recordIntoHAR(har, null, options);
return;
}
const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url });
harRouter.addContextRoute(this);
}
@ -340,10 +360,18 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
try {
await this._wrapApiCall(async () => {
await this._browserType?._onWillCloseContext?.(this);
if (this._options.recordHar) {
const har = await this._channel.harExport();
for (const [harId, harParams] of this._harRecorders) {
const har = await this._channel.harExport({ harId });
const artifact = Artifact.from(har.artifact);
await artifact.saveAs(this._options.recordHar.path);
// Server side will compress artifact if content is attach or if file is .zip.
const isCompressed = harParams.content === 'attach' || harParams.path.endsWith('.zip');
const needCompressed = harParams.path.endsWith('.zip');
if (isCompressed && !needCompressed) {
await artifact.saveAs(harParams.path + '.tmp');
await this._connection.localUtils()._channel.harUnzip({ zipFile: harParams.path + '.tmp', harFile: harParams.path });
} else {
await artifact.saveAs(harParams.path);
}
await artifact.delete();
}
}, true);

View File

@ -468,7 +468,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> {
async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean } = {}): Promise<void> {
if (options.update) {
await this._browserContext._recordIntoHAR(har, this, options);
return;
}
const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url });
harRouter.addPageRoute(this);
}

View File

@ -382,6 +382,7 @@ export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel {
harOpen(params: LocalUtilsHarOpenParams, metadata?: Metadata): Promise<LocalUtilsHarOpenResult>;
harLookup(params: LocalUtilsHarLookupParams, metadata?: Metadata): Promise<LocalUtilsHarLookupResult>;
harClose(params: LocalUtilsHarCloseParams, metadata?: Metadata): Promise<LocalUtilsHarCloseResult>;
harUnzip(params: LocalUtilsHarUnzipParams, metadata?: Metadata): Promise<LocalUtilsHarUnzipResult>;
}
export type LocalUtilsZipParams = {
zipFile: string,
@ -427,6 +428,14 @@ export type LocalUtilsHarCloseOptions = {
};
export type LocalUtilsHarCloseResult = void;
export type LocalUtilsHarUnzipParams = {
zipFile: string,
harFile: string,
};
export type LocalUtilsHarUnzipOptions = {
};
export type LocalUtilsHarUnzipResult = void;
export interface LocalUtilsEvents {
}
@ -1119,7 +1128,8 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
pause(params?: BrowserContextPauseParams, metadata?: Metadata): Promise<BrowserContextPauseResult>;
recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: Metadata): Promise<BrowserContextRecorderSupplementEnableResult>;
newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: Metadata): Promise<BrowserContextNewCDPSessionResult>;
harExport(params?: BrowserContextHarExportParams, metadata?: Metadata): Promise<BrowserContextHarExportResult>;
harStart(params: BrowserContextHarStartParams, metadata?: Metadata): Promise<BrowserContextHarStartResult>;
harExport(params: BrowserContextHarExportParams, metadata?: Metadata): Promise<BrowserContextHarExportResult>;
createTempFile(params: BrowserContextCreateTempFileParams, metadata?: Metadata): Promise<BrowserContextCreateTempFileResult>;
}
export type BrowserContextBindingCallEvent = {
@ -1325,8 +1335,22 @@ export type BrowserContextNewCDPSessionOptions = {
export type BrowserContextNewCDPSessionResult = {
session: CDPSessionChannel,
};
export type BrowserContextHarExportParams = {};
export type BrowserContextHarExportOptions = {};
export type BrowserContextHarStartParams = {
page?: PageChannel,
options: RecordHarOptions,
};
export type BrowserContextHarStartOptions = {
page?: PageChannel,
};
export type BrowserContextHarStartResult = {
harId: string,
};
export type BrowserContextHarExportParams = {
harId?: string,
};
export type BrowserContextHarExportOptions = {
harId?: string,
};
export type BrowserContextHarExportResult = {
artifact: ArtifactChannel,
};

View File

@ -520,6 +520,11 @@ LocalUtils:
parameters:
harId: string
harUnzip:
parameters:
zipFile: string
harFile: string
Root:
type: interface
@ -926,7 +931,16 @@ BrowserContext:
returns:
session: CDPSession
harStart:
parameters:
page: Page?
options: RecordHarOptions
returns:
harId: string
harExport:
parameters:
harId: string?
returns:
artifact: Artifact

View File

@ -220,6 +220,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
scheme.LocalUtilsHarCloseParams = tObject({
harId: tString,
});
scheme.LocalUtilsHarUnzipParams = tObject({
zipFile: tString,
harFile: tString,
});
scheme.RootInitializeParams = tObject({
sdkLanguage: tString,
});
@ -527,7 +531,13 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
page: tOptional(tChannel('Page')),
frame: tOptional(tChannel('Frame')),
});
scheme.BrowserContextHarExportParams = tOptional(tObject({}));
scheme.BrowserContextHarStartParams = tObject({
page: tOptional(tChannel('Page')),
options: tType('RecordHarOptions'),
});
scheme.BrowserContextHarExportParams = tObject({
harId: tOptional(tString),
});
scheme.BrowserContextCreateTempFileParams = tObject({
name: tString,
});

View File

@ -17,7 +17,7 @@
import * as os from 'os';
import { TimeoutSettings } from '../common/timeoutSettings';
import { debugMode } from '../utils';
import { createGuid, debugMode } from '../utils';
import { mkdirIfNeeded } from '../utils/fileUtils';
import type { Browser, BrowserOptions } from './browser';
import type { Download } from './download';
@ -40,6 +40,7 @@ import { HarRecorder } from './har/harRecorder';
import { Recorder } from './recorder';
import * as consoleApiSource from '../generated/consoleApiSource';
import { BrowserContextAPIRequestContext } from './fetch';
import type { Artifact } from './artifact';
export abstract class BrowserContext extends SdkObject {
static Events = {
@ -67,7 +68,7 @@ export abstract class BrowserContext extends SdkObject {
readonly _browserContextId: string | undefined;
private _selectors?: Selectors;
private _origins = new Set<string>();
readonly _harRecorder: HarRecorder | undefined;
readonly _harRecorders = new Map<string, HarRecorder>();
readonly tracing: Tracing;
readonly fetchRequest: BrowserContextAPIRequestContext;
private _customCloseHandler?: () => Promise<any>;
@ -87,7 +88,7 @@ export abstract class BrowserContext extends SdkObject {
this.fetchRequest = new BrowserContextAPIRequestContext(this);
if (this._options.recordHar)
this._harRecorder = new HarRecorder(this, this._options.recordHar);
this._harRecorders.set('', new HarRecorder(this, null, this._options.recordHar));
this.tracing = new Tracing(this, browser.options.tracesDir);
}
@ -316,7 +317,8 @@ export abstract class BrowserContext extends SdkObject {
this.emit(BrowserContext.Events.BeforeClose);
this._closedStatus = 'closing';
await this._harRecorder?.flush();
for (const harRecorder of this._harRecorders.values())
await harRecorder.flush();
await this.tracing.flush();
// Cleanup.
@ -442,6 +444,17 @@ export abstract class BrowserContext extends SdkObject {
this.on(BrowserContext.Events.Page, installInPage);
return Promise.all(this.pages().map(installInPage));
}
async _harStart(page: Page | null, options: channels.RecordHarOptions): Promise<string> {
const harId = createGuid();
this._harRecorders.set(harId, new HarRecorder(this, page, options));
return harId;
}
async _harExport(harId: string | undefined): Promise<Artifact> {
const recorder = this._harRecorders.get(harId || '')!;
return recorder.export();
}
}
export function assertBrowserContextIsNotOwned(context: BrowserContext) {

View File

@ -213,8 +213,13 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
return { session: new CDPSessionDispatcher(this._scope, await crBrowserContext.newCDPSession((params.page ? params.page as PageDispatcher : params.frame as FrameDispatcher)._object)) };
}
async harStart(params: channels.BrowserContextHarStartParams): Promise<channels.BrowserContextHarStartResult> {
const harId = await this._context._harStart(params.page ? (params.page as PageDispatcher)._object : null, params.options);
return { harId };
}
async harExport(params: channels.BrowserContextHarExportParams): Promise<channels.BrowserContextHarExportResult> {
const artifact = await this._context._harRecorder?.export();
const artifact = await this._context._harExport(params.harId);
if (!artifact)
throw new Error('No HAR artifact. Ensure record.harPath is set.');
return { artifact: new ArtifactDispatcher(this._scope, artifact) };

View File

@ -124,6 +124,20 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
harBackend.dispose();
}
}
async harUnzip(params: channels.LocalUtilsHarUnzipParams, metadata?: channels.Metadata): Promise<void> {
const dir = path.dirname(params.zipFile);
const zipFile = new ZipFile(params.zipFile);
for (const entry of await zipFile.entries()) {
const buffer = await zipFile.read(entry);
if (entry === 'har.har')
await fs.promises.writeFile(params.harFile, buffer);
else
await fs.promises.writeFile(path.join(dir, entry), buffer);
}
zipFile.close();
await fs.promises.unlink(params.zipFile);
}
}
const redirectStatus = [301, 302, 303, 307, 308];

View File

@ -26,6 +26,7 @@ import type { ZipFile } from '../../zipBundle';
import { ManualPromise } from '../../utils/manualPromise';
import type EventEmitter from 'events';
import { createGuid } from '../../utils';
import type { Page } from '../page';
export class HarRecorder {
private _artifact: Artifact;
@ -35,12 +36,12 @@ export class HarRecorder {
private _zipFile: ZipFile | null = null;
private _writtenZipEntries = new Set<string>();
constructor(context: BrowserContext, options: channels.RecordHarOptions) {
constructor(context: BrowserContext, page: Page | null, options: channels.RecordHarOptions) {
this._artifact = new Artifact(context, path.join(context._browser.options.artifactsDir, `${createGuid()}.har`));
const urlFilterRe = options.urlRegexSource !== undefined && options.urlRegexFlags !== undefined ? new RegExp(options.urlRegexSource, options.urlRegexFlags) : undefined;
const expectsZip = options.path.endsWith('.zip');
const content = options.content || (expectsZip ? 'attach' : 'embed');
this._tracer = new HarTracer(context, this, {
this._tracer = new HarTracer(context, page, this, {
content,
slimMode: options.mode === 'minimal',
includeTraceInfo: false,

View File

@ -64,9 +64,11 @@ export class HarTracer {
private _started = false;
private _entrySymbol: symbol;
private _baseURL: string | undefined;
private _page: Page | null;
constructor(context: BrowserContext | APIRequestContext, delegate: HarTracerDelegate, options: HarTracerOptions) {
constructor(context: BrowserContext | APIRequestContext, page: Page | null, delegate: HarTracerDelegate, options: HarTracerOptions) {
this._context = context;
this._page = page;
this._delegate = delegate;
this._options = options;
if (options.slimMode) {
@ -92,7 +94,7 @@ export class HarTracer {
];
if (this._context instanceof BrowserContext) {
this._eventListeners.push(
eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, (page: Page) => this._ensurePageEntry(page)),
eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, (page: Page) => this._createPageEntryIfNeeded(page)),
eventsHelper.addEventListener(this._context, BrowserContext.Events.Request, (request: network.Request) => this._onRequest(request)),
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFinished, ({ request, response }) => this._onRequestFinished(request, response).catch(() => {})),
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFailed, request => this._onRequestFailed(request)),
@ -108,9 +110,11 @@ export class HarTracer {
return (request as any)[this._entrySymbol];
}
private _ensurePageEntry(page: Page): har.Page | undefined {
private _createPageEntryIfNeeded(page: Page): har.Page | undefined {
if (this._options.omitPages)
return;
if (this._page && page !== this._page)
return;
let pageEntry = this._pageEntries.get(page);
if (!pageEntry) {
pageEntry = {
@ -228,11 +232,13 @@ export class HarTracer {
if (!this._shouldIncludeEntryWithUrl(request.url()))
return;
const page = request.frame()._page;
if (this._page && page !== this._page)
return;
const url = network.parsedURL(request.url());
if (!url)
return;
const pageEntry = this._ensurePageEntry(page);
const pageEntry = this._createPageEntryIfNeeded(page);
const harEntry = createHarEntry(request.method(), url, request.frame().guid, this._options);
if (pageEntry)
harEntry.pageref = pageEntry.id;
@ -252,10 +258,10 @@ export class HarTracer {
private async _onRequestFinished(request: network.Request, response: network.Response | null) {
if (!response)
return;
const page = request.frame()._page;
const harEntry = this._entryForRequest(request);
if (!harEntry)
return;
const page = request.frame()._page;
const httpVersion = response.httpVersion();
harEntry.request.httpVersion = httpVersion;
@ -353,11 +359,11 @@ export class HarTracer {
}
private _onResponse(response: network.Response) {
const page = response.frame()._page;
const pageEntry = this._ensurePageEntry(page);
const harEntry = this._entryForRequest(response.request());
if (!harEntry)
return;
const page = response.frame()._page;
const pageEntry = this._createPageEntryIfNeeded(page);
const request = response.request();
harEntry.response = {

View File

@ -88,7 +88,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
super(context, 'Tracing');
this._context = context;
this._precreatedTracesDir = tracesDir;
this._harTracer = new HarTracer(context, this, {
this._harTracer = new HarTracer(context, null, this, {
content: 'attach',
includeTraceInfo: true,
waitForContentOnStop: false,

View File

@ -34,7 +34,7 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
constructor(context: BrowserContext) {
super();
this._snapshotter = new Snapshotter(context, this);
this._harTracer = new HarTracer(context, this, { content: 'attach', includeTraceInfo: true, waitForContentOnStop: false, skipScripts: true });
this._harTracer = new HarTracer(context, null, this, { content: 'attach', includeTraceInfo: true, waitForContentOnStop: false, skipScripts: true });
}
async initialize(): Promise<void> {

View File

@ -3185,11 +3185,16 @@ export interface Page {
*/
notFound?: "abort"|"fallback";
/**
* If specified, updates the given HAR with the actual network information instead of serving from file.
*/
update?: boolean;
/**
* A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern
* will be served from the HAR file. If not specified, all requests are served from the HAR file.
*/
url?: string|RegExp|((url: URL) => boolean);
url?: string|RegExp;
}): Promise<void>;
/**
@ -7137,11 +7142,16 @@ export interface BrowserContext {
*/
notFound?: "abort"|"fallback";
/**
* If specified, updates the given HAR with the actual network information instead of serving from file.
*/
update?: boolean;
/**
* A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern
* will be served from the HAR file. If not specified, all requests are served from the HAR file.
*/
url?: string|RegExp|((url: URL) => boolean);
url?: string|RegExp;
}): Promise<void>;
/**
@ -10666,13 +10676,15 @@ export interface BrowserType<Unused = {}> {
/**
* Optional setting to control resource content management. If `omit` is specified, content is not persisted. If `attach`
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to `embed`, which stores content inline the HAR file as per HAR specification.
* is specified, resources are persistet as separate files or entries in the ZIP archive. If `embed` is specified, content
* is stored inline the HAR file as per HAR specification. Defaults to `attach` for `.zip` output files and to `embed` for
* all other file extensions.
*/
content?: "omit"|"embed"|"attach";
/**
* Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `attach` mode is used by default.
* Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `content: 'attach'` is used by
* default.
*/
path: string;
@ -11859,13 +11871,15 @@ export interface AndroidDevice {
/**
* Optional setting to control resource content management. If `omit` is specified, content is not persisted. If `attach`
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to `embed`, which stores content inline the HAR file as per HAR specification.
* is specified, resources are persistet as separate files or entries in the ZIP archive. If `embed` is specified, content
* is stored inline the HAR file as per HAR specification. Defaults to `attach` for `.zip` output files and to `embed` for
* all other file extensions.
*/
content?: "omit"|"embed"|"attach";
/**
* Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `attach` mode is used by default.
* Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `content: 'attach'` is used by
* default.
*/
path: string;
@ -13435,13 +13449,15 @@ export interface Browser extends EventEmitter {
/**
* Optional setting to control resource content management. If `omit` is specified, content is not persisted. If `attach`
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to `embed`, which stores content inline the HAR file as per HAR specification.
* is specified, resources are persistet as separate files or entries in the ZIP archive. If `embed` is specified, content
* is stored inline the HAR file as per HAR specification. Defaults to `attach` for `.zip` output files and to `embed` for
* all other file extensions.
*/
content?: "omit"|"embed"|"attach";
/**
* Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `attach` mode is used by default.
* Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `content: 'attach'` is used by
* default.
*/
path: string;
@ -14227,13 +14243,15 @@ export interface Electron {
/**
* Optional setting to control resource content management. If `omit` is specified, content is not persisted. If `attach`
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to `embed`, which stores content inline the HAR file as per HAR specification.
* is specified, resources are persistet as separate files or entries in the ZIP archive. If `embed` is specified, content
* is stored inline the HAR file as per HAR specification. Defaults to `attach` for `.zip` output files and to `embed` for
* all other file extensions.
*/
content?: "omit"|"embed"|"attach";
/**
* Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `attach` mode is used by default.
* Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `content: 'attach'` is used by
* default.
*/
path: string;
@ -16054,13 +16072,15 @@ export interface BrowserContextOptions {
/**
* Optional setting to control resource content management. If `omit` is specified, content is not persisted. If `attach`
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
* Defaults to `embed`, which stores content inline the HAR file as per HAR specification.
* is specified, resources are persistet as separate files or entries in the ZIP archive. If `embed` is specified, content
* is stored inline the HAR file as per HAR specification. Defaults to `attach` for `.zip` output files and to `embed` for
* all other file extensions.
*/
content?: "omit"|"embed"|"attach";
/**
* Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `attach` mode is used by default.
* Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `content: 'attach'` is used by
* default.
*/
path: string;

View File

@ -269,6 +269,27 @@ it('should round-trip har.zip', async ({ contextFactory, isAndroid, server }, te
await expect(page2.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)');
});
it('should produce extracted zip', async ({ contextFactory, isAndroid, server }, testInfo) => {
it.fixme(isAndroid);
const harPath = testInfo.outputPath('har.har');
const context1 = await contextFactory({ recordHar: { mode: 'minimal', path: harPath, content: 'attach' } });
const page1 = await context1.newPage();
await page1.goto(server.PREFIX + '/one-style.html');
await context1.close();
expect(fs.existsSync(harPath)).toBeTruthy();
const har = fs.readFileSync(harPath, 'utf-8');
expect(har).not.toContain('background-color');
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!');
await expect(page2.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)');
});
it('should round-trip extracted har.zip', async ({ contextFactory, isAndroid, server }, testInfo) => {
it.fixme(isAndroid);
@ -359,3 +380,57 @@ it('should disambiguate by header', async ({ contextFactory, isAndroid, server }
expect(await page2.evaluate(fetchFunction, 'baz3')).toBe('baz3');
expect(await page2.evaluate(fetchFunction, 'baz4')).toBe('baz1');
});
it('should update har.zip for context', async ({ contextFactory, isAndroid, server }, testInfo) => {
it.fixme(isAndroid);
const harPath = testInfo.outputPath('har.zip');
const context1 = await contextFactory();
await context1.routeFromHAR(harPath, { update: true });
const page1 = await context1.newPage();
await page1.goto(server.PREFIX + '/one-style.html');
await context1.close();
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!');
await expect(page2.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)');
});
it('should update har.zip for page', async ({ contextFactory, isAndroid, server }, testInfo) => {
it.fixme(isAndroid);
const harPath = testInfo.outputPath('har.zip');
const context1 = await contextFactory();
const page1 = await context1.newPage();
await page1.routeFromHAR(harPath, { update: true });
await page1.goto(server.PREFIX + '/one-style.html');
await context1.close();
const context2 = await contextFactory();
const page2 = await context2.newPage();
await page2.routeFromHAR(harPath, { notFound: 'abort' });
await page2.goto(server.PREFIX + '/one-style.html');
expect(await page2.content()).toContain('hello, world!');
await expect(page2.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)');
});
it('should update extracted har.zip for page', async ({ contextFactory, isAndroid, server }, testInfo) => {
it.fixme(isAndroid);
const harPath = testInfo.outputPath('har.har');
const context1 = await contextFactory();
const page1 = await context1.newPage();
await page1.routeFromHAR(harPath, { update: true });
await page1.goto(server.PREFIX + '/one-style.html');
await context1.close();
const context2 = await contextFactory();
const page2 = await context2.newPage();
await page2.routeFromHAR(harPath, { notFound: 'abort' });
await page2.goto(server.PREFIX + '/one-style.html');
expect(await page2.content()).toContain('hello, world!');
await expect(page2.locator('body')).toHaveCSS('background-color', 'rgb(255, 192, 203)');
});