feat(har): introduce the slim mode (#15053)

This commit is contained in:
Pavel Feldman 2022-06-22 14:44:12 -07:00 committed by GitHub
parent 033c250f6d
commit 7bd72716f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 257 additions and 166 deletions

View file

@ -563,6 +563,7 @@ Logger sink for Playwright logging.
`false`. Deprecated, use `content` policy instead. `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. - `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. - `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.
- `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. - `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.
Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into `recordHar.path` file. If not Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into `recordHar.path` file. If not

View file

@ -468,7 +468,7 @@ async function launchContext(options: Options, headless: boolean, executablePath
// HAR // HAR
if (options.saveHar) { if (options.saveHar) {
contextOptions.recordHar = { path: path.resolve(process.cwd(), options.saveHar) }; contextOptions.recordHar = { path: path.resolve(process.cwd(), options.saveHar), mode: 'minimal' };
if (options.saveHarGlob) if (options.saveHarGlob)
contextOptions.recordHar.urlFilter = options.saveHarGlob; contextOptions.recordHar.urlFilter = options.saveHarGlob;
contextOptions.serviceWorkers = 'block'; contextOptions.serviceWorkers = 'block';

View file

@ -393,6 +393,7 @@ function prepareRecordHarOptions(options: BrowserContextOptions['recordHar']): c
urlGlob: isString(options.urlFilter) ? options.urlFilter : undefined, urlGlob: isString(options.urlFilter) ? options.urlFilter : undefined,
urlRegexSource: isRegExp(options.urlFilter) ? options.urlFilter.source : undefined, urlRegexSource: isRegExp(options.urlFilter) ? options.urlFilter.source : undefined,
urlRegexFlags: isRegExp(options.urlFilter) ? options.urlFilter.flags : undefined, urlRegexFlags: isRegExp(options.urlFilter) ? options.urlFilter.flags : undefined,
mode: options.mode
}; };
} }

View file

@ -63,6 +63,7 @@ export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'vie
path: string, path: string,
omitContent?: boolean, omitContent?: boolean,
content?: 'omit' | 'embed' | 'attach', content?: 'omit' | 'embed' | 'attach',
mode?: 'full' | 'minimal',
urlFilter?: string | RegExp, urlFilter?: string | RegExp,
}, },
}; };

View file

@ -266,6 +266,7 @@ export type SerializedError = {
export type RecordHarOptions = { export type RecordHarOptions = {
path: string, path: string,
content?: 'embed' | 'attach' | 'omit', content?: 'embed' | 'attach' | 'omit',
mode?: 'full' | 'minimal',
urlGlob?: string, urlGlob?: string,
urlRegexSource?: string, urlRegexSource?: string,
urlRegexFlags?: string, urlRegexFlags?: string,

View file

@ -231,6 +231,11 @@ RecordHarOptions:
- embed - embed
- attach - attach
- omit - omit
mode:
type: enum?
literals:
- full
- minimal
urlGlob: string? urlGlob: string?
urlRegexSource: string? urlRegexSource: string?
urlRegexFlags: string? urlRegexFlags: string?

View file

@ -156,6 +156,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
scheme.RecordHarOptions = tObject({ scheme.RecordHarOptions = tObject({
path: tString, path: tString,
content: tOptional(tEnum(['embed', 'attach', 'omit'])), content: tOptional(tEnum(['embed', 'attach', 'omit'])),
mode: tOptional(tEnum(['full', 'minimal'])),
urlGlob: tOptional(tString), urlGlob: tOptional(tString),
urlRegexSource: tOptional(tString), urlRegexSource: tOptional(tString),
urlRegexFlags: tOptional(tString), urlRegexFlags: tOptional(tString),

View file

@ -176,14 +176,14 @@ class HarBackend {
} }
} }
private async _loadContent(content: { text?: string, encoding?: string, _sha1?: string }): Promise<Buffer> { private async _loadContent(content: { text?: string, encoding?: string, _file?: string }): Promise<Buffer> {
const sha1 = content._sha1; const file = content._file;
let buffer: Buffer; let buffer: Buffer;
if (sha1) { if (file) {
if (this._zipFile) if (this._zipFile)
buffer = await this._zipFile.read(sha1); buffer = await this._zipFile.read(file);
else else
buffer = await fs.promises.readFile(path.resolve(this._baseDir!, sha1)); buffer = await fs.promises.readFile(path.resolve(this._baseDir!, file));
} else { } else {
buffer = Buffer.from(content.text || '', content.encoding === 'base64' ? 'base64' : 'utf-8'); buffer = Buffer.from(content.text || '', content.encoding === 'base64' ? 'base64' : 'utf-8');
} }

View file

@ -64,9 +64,8 @@ export type Entry = {
timings: Timings; timings: Timings;
serverIPAddress?: string; serverIPAddress?: string;
connection?: string; connection?: string;
_requestref: string; _frameref?: string;
_frameref: string; _monotonicTime?: number;
_monotonicTime: number;
_serverPort?: number; _serverPort?: number;
_securityDetails?: SecurityDetails; _securityDetails?: SecurityDetails;
}; };
@ -95,7 +94,7 @@ export type Response = {
headersSize: number; headersSize: number;
bodySize: number; bodySize: number;
comment?: string; comment?: string;
_transferSize: number; _transferSize?: number;
_failureText?: string _failureText?: string
}; };
@ -129,6 +128,7 @@ export type PostData = {
text: string; text: string;
comment?: string; comment?: string;
_sha1?: string; _sha1?: string;
_file?: string;
}; };
export type Param = { export type Param = {
@ -147,6 +147,7 @@ export type Content = {
encoding?: string; encoding?: string;
comment?: string; comment?: string;
_sha1?: string; _sha1?: string;
_file?: string;
}; };
export type Cache = { export type Cache = {

View file

@ -42,6 +42,8 @@ export class HarRecorder {
const content = options.content || (expectsZip ? 'attach' : 'embed'); const content = options.content || (expectsZip ? 'attach' : 'embed');
this._tracer = new HarTracer(context, this, { this._tracer = new HarTracer(context, this, {
content, content,
slimMode: options.mode === 'minimal',
includeTraceInfo: false,
waitForContentOnStop: true, waitForContentOnStop: true,
skipScripts: false, skipScripts: false,
urlFilter: urlFilterRe ?? options.urlGlob, urlFilter: urlFilterRe ?? options.urlGlob,
@ -73,7 +75,7 @@ export class HarRecorder {
const log = this._tracer.stop(); const log = this._tracer.stop();
log.entries = this._entries; log.entries = this._entries;
const harFileContent = JSON.stringify({ log }, undefined, 2); const harFileContent = jsonStringify({ log });
if (this._zipFile) { if (this._zipFile) {
const result = new ManualPromise<void>(); const result = new ManualPromise<void>();
@ -95,3 +97,50 @@ export class HarRecorder {
return this._artifact; return this._artifact;
} }
} }
function jsonStringify(object: any): string {
const tokens: string[] = [];
innerJsonStringify(object, tokens, '', false, undefined);
return tokens.join('');
}
function innerJsonStringify(object: any, tokens: string[], indent: string, flat: boolean, parentKey: string | undefined) {
if (typeof object !== 'object' || object === null) {
tokens.push(JSON.stringify(object));
return;
}
const isArray = Array.isArray(object);
if (!isArray && object.constructor.name !== 'Object') {
tokens.push(JSON.stringify(object));
return;
}
const entries = isArray ? object : Object.entries(object).filter(e => e[1] !== undefined);
if (!entries.length) {
tokens.push(isArray ? `[]` : `{}`);
return;
}
const childIndent = `${indent} `;
let brackets: { open: string, close: string };
if (isArray)
brackets = flat ? { open: '[', close: ']' } : { open: `[\n${childIndent}`, close: `\n${indent}]` };
else
brackets = flat ? { open: '{ ', close: ' }' } : { open: `{\n${childIndent}`, close: `\n${indent}}` };
tokens.push(brackets.open);
for (let i = 0; i < entries.length; ++i) {
const entry = entries[i];
if (i)
tokens.push(flat ? `, ` : `,\n${childIndent}`);
if (!isArray)
tokens.push(`${JSON.stringify(entry[0])}: `);
const key = isArray ? undefined : entry[0];
const flatten = flat || key === 'timings' || parentKey === 'headers';
innerJsonStringify(isArray ? entry : entry[1], tokens, childIndent, flatten, key);
}
tokens.push(brackets.close);
}

View file

@ -42,8 +42,16 @@ export interface HarTracerDelegate {
type HarTracerOptions = { type HarTracerOptions = {
content: 'omit' | 'attach' | 'embed'; content: 'omit' | 'attach' | 'embed';
skipScripts: boolean; skipScripts: boolean;
includeTraceInfo: boolean;
waitForContentOnStop: boolean; waitForContentOnStop: boolean;
urlFilter?: string | RegExp; urlFilter?: string | RegExp;
slimMode?: boolean;
omitSecurityDetails?: boolean;
omitCookies?: boolean;
omitTiming?: boolean;
omitServerIP?: boolean;
omitPages?: boolean;
omitSizes?: boolean;
}; };
export class HarTracer { export class HarTracer {
@ -61,6 +69,14 @@ export class HarTracer {
this._context = context; this._context = context;
this._delegate = delegate; this._delegate = delegate;
this._options = options; this._options = options;
if (options.slimMode) {
options.omitSecurityDetails = true;
options.omitCookies = true;
options.omitTiming = true;
options.omitServerIP = true;
options.omitSizes = true;
options.omitPages = true;
}
this._entrySymbol = Symbol('requestHarEntry'); this._entrySymbol = Symbol('requestHarEntry');
this._baseURL = context instanceof APIRequestContext ? context._defaultOptions().baseURL : context._options.baseURL; this._baseURL = context instanceof APIRequestContext ? context._defaultOptions().baseURL : context._options.baseURL;
} }
@ -92,32 +108,34 @@ export class HarTracer {
return (request as any)[this._entrySymbol]; return (request as any)[this._entrySymbol];
} }
private _ensurePageEntry(page: Page) { private _ensurePageEntry(page: Page): har.Page | undefined {
if (this._options.omitPages)
return;
let pageEntry = this._pageEntries.get(page); let pageEntry = this._pageEntries.get(page);
if (!pageEntry) { if (!pageEntry) {
page.mainFrame().on(Frame.Events.AddLifecycle, (event: LifecycleEvent) => {
if (event === 'load')
this._onLoad(page);
if (event === 'domcontentloaded')
this._onDOMContentLoaded(page);
});
pageEntry = { pageEntry = {
startedDateTime: new Date(), startedDateTime: new Date(),
id: page.guid, id: page.guid,
title: '', title: '',
pageTimings: { pageTimings: this._options.omitTiming ? {} : {
onContentLoad: -1, onContentLoad: -1,
onLoad: -1, onLoad: -1,
}, },
}; };
page.mainFrame().on(Frame.Events.AddLifecycle, (event: LifecycleEvent) => {
if (event === 'load')
this._onLoad(page, pageEntry!);
if (event === 'domcontentloaded')
this._onDOMContentLoaded(page, pageEntry!);
});
this._pageEntries.set(page, pageEntry); this._pageEntries.set(page, pageEntry);
} }
return pageEntry; return pageEntry;
} }
private _onDOMContentLoaded(page: Page) { private _onDOMContentLoaded(page: Page, pageEntry: har.Page) {
const pageEntry = this._ensurePageEntry(page);
const promise = page.mainFrame().evaluateExpression(String(() => { const promise = page.mainFrame().evaluateExpression(String(() => {
return { return {
title: document.title, title: document.title,
@ -125,13 +143,13 @@ export class HarTracer {
}; };
}), true, undefined, 'utility').then(result => { }), true, undefined, 'utility').then(result => {
pageEntry.title = result.title; pageEntry.title = result.title;
if (!this._options.omitTiming)
pageEntry.pageTimings.onContentLoad = result.domContentLoaded; pageEntry.pageTimings.onContentLoad = result.domContentLoaded;
}).catch(() => {}); }).catch(() => {});
this._addBarrier(page, promise); this._addBarrier(page, promise);
} }
private _onLoad(page: Page) { private _onLoad(page: Page, pageEntry: har.Page) {
const pageEntry = this._ensurePageEntry(page);
const promise = page.mainFrame().evaluateExpression(String(() => { const promise = page.mainFrame().evaluateExpression(String(() => {
return { return {
title: document.title, title: document.title,
@ -139,6 +157,7 @@ export class HarTracer {
}; };
}), true, undefined, 'utility').then(result => { }), true, undefined, 'utility').then(result => {
pageEntry.title = result.title; pageEntry.title = result.title;
if (!this._options.omitTiming)
pageEntry.pageTimings.onLoad = result.loaded; pageEntry.pageTimings.onLoad = result.loaded;
}).catch(() => {}); }).catch(() => {});
this._addBarrier(page, promise); this._addBarrier(page, promise);
@ -161,10 +180,12 @@ export class HarTracer {
private _onAPIRequest(event: APIRequestEvent) { private _onAPIRequest(event: APIRequestEvent) {
if (!this._shouldIncludeEntryWithUrl(event.url.toString())) if (!this._shouldIncludeEntryWithUrl(event.url.toString()))
return; return;
const harEntry = createHarEntry(event.method, event.url, '', ''); const harEntry = createHarEntry(event.method, event.url, undefined, this._options);
if (!this._options.omitCookies)
harEntry.request.cookies = event.cookies; harEntry.request.cookies = event.cookies;
harEntry.request.headers = Object.entries(event.headers).map(([name, value]) => ({ name, value })); harEntry.request.headers = Object.entries(event.headers).map(([name, value]) => ({ name, value }));
harEntry.request.postData = this._postDataForBuffer(event.postData || null, event.headers['content-type'], this._options.content); harEntry.request.postData = this._postDataForBuffer(event.postData || null, event.headers['content-type'], this._options.content);
if (!this._options.omitSizes)
harEntry.request.bodySize = event.postData?.length || 0; harEntry.request.bodySize = event.postData?.length || 0;
(event as any)[this._entrySymbol] = harEntry; (event as any)[this._entrySymbol] = harEntry;
if (this._started) if (this._started)
@ -186,7 +207,7 @@ export class HarTracer {
value: event.rawHeaders[i + 1] value: event.rawHeaders[i + 1]
}); });
} }
harEntry.response.cookies = event.cookies.map(c => { harEntry.response.cookies = this._options.omitCookies ? [] : event.cookies.map(c => {
return { return {
...c, ...c,
expires: c.expires === -1 ? undefined : new Date(c.expires) expires: c.expires === -1 ? undefined : new Date(c.expires)
@ -212,9 +233,11 @@ export class HarTracer {
return; return;
const pageEntry = this._ensurePageEntry(page); const pageEntry = this._ensurePageEntry(page);
const harEntry = createHarEntry(request.method(), url, request.guid, request.frame().guid); const harEntry = createHarEntry(request.method(), url, request.frame().guid, this._options);
if (pageEntry)
harEntry.pageref = pageEntry.id; harEntry.pageref = pageEntry.id;
harEntry.request.postData = this._postDataForRequest(request, this._options.content); harEntry.request.postData = this._postDataForRequest(request, this._options.content);
if (!this._options.omitSizes)
harEntry.request.bodySize = request.bodySize(); harEntry.request.bodySize = request.bodySize();
if (request.redirectedFrom()) { if (request.redirectedFrom()) {
const fromEntry = this._entryForRequest(request.redirectedFrom()!); const fromEntry = this._entryForRequest(request.redirectedFrom()!);
@ -238,7 +261,7 @@ export class HarTracer {
harEntry.request.httpVersion = httpVersion; harEntry.request.httpVersion = httpVersion;
harEntry.response.httpVersion = httpVersion; harEntry.response.httpVersion = httpVersion;
const compressionCalculationBarrier = { const compressionCalculationBarrier = this._options.omitSizes ? undefined : {
_encodedBodySize: -1, _encodedBodySize: -1,
_decodedBodySize: -1, _decodedBodySize: -1,
barrier: new ManualPromise<void>(), barrier: new ManualPromise<void>(),
@ -257,33 +280,37 @@ export class HarTracer {
this._check(); this._check();
} }
}; };
if (compressionCalculationBarrier)
this._addBarrier(page, compressionCalculationBarrier.barrier); this._addBarrier(page, compressionCalculationBarrier.barrier);
const promise = response.body().then(buffer => { const promise = response.body().then(buffer => {
if (this._options.skipScripts && request.resourceType() === 'script') { if (this._options.skipScripts && request.resourceType() === 'script') {
compressionCalculationBarrier.setDecodedBodySize(0); compressionCalculationBarrier?.setDecodedBodySize(0);
return; return;
} }
const content = harEntry.response.content; const content = harEntry.response.content;
compressionCalculationBarrier.setDecodedBodySize(buffer.length); compressionCalculationBarrier?.setDecodedBodySize(buffer.length);
this._storeResponseContent(buffer, content, request.resourceType()); this._storeResponseContent(buffer, content, request.resourceType());
}).catch(() => { }).catch(() => {
compressionCalculationBarrier.setDecodedBodySize(0); compressionCalculationBarrier?.setDecodedBodySize(0);
}).then(() => { }).then(() => {
if (this._started) if (this._started)
this._delegate.onEntryFinished(harEntry); this._delegate.onEntryFinished(harEntry);
}); });
this._addBarrier(page, promise); this._addBarrier(page, promise);
if (!this._options.omitSizes) {
this._addBarrier(page, response.sizes().then(sizes => { this._addBarrier(page, response.sizes().then(sizes => {
harEntry.response.bodySize = sizes.responseBodySize; harEntry.response.bodySize = sizes.responseBodySize;
harEntry.response.headersSize = sizes.responseHeadersSize; harEntry.response.headersSize = sizes.responseHeadersSize;
// Fallback for WebKit by calculating it manually // Fallback for WebKit by calculating it manually
harEntry.response._transferSize = response.request().responseSize.transferSize || (sizes.responseHeadersSize + sizes.responseBodySize); harEntry.response._transferSize = response.request().responseSize.transferSize || (sizes.responseHeadersSize + sizes.responseBodySize);
harEntry.request.headersSize = sizes.requestHeadersSize; harEntry.request.headersSize = sizes.requestHeadersSize;
compressionCalculationBarrier.setEncodedBodySize(sizes.responseBodySize); compressionCalculationBarrier?.setEncodedBodySize(sizes.responseBodySize);
})); }));
} }
}
private async _onRequestFailed(request: network.Request) { private async _onRequestFailed(request: network.Request) {
const harEntry = this._entryForRequest(request); const harEntry = this._entryForRequest(request);
@ -301,7 +328,10 @@ export class HarTracer {
content.size = 0; content.size = 0;
return; return;
} }
if (!this._options.omitSizes)
content.size = buffer.length; content.size = buffer.length;
if (this._options.content === 'embed') { if (this._options.content === 'embed') {
// Sometimes, we can receive a font/media file with textual mime type. Browser // Sometimes, we can receive a font/media file with textual mime type. Browser
// still interprets them correctly, but the 'content-type' header is obviously wrong. // still interprets them correctly, but the 'content-type' header is obviously wrong.
@ -312,9 +342,13 @@ export class HarTracer {
content.encoding = 'base64'; content.encoding = 'base64';
} }
} else if (this._options.content === 'attach') { } else if (this._options.content === 'attach') {
content._sha1 = calculateSha1(buffer) + '.' + (mime.getExtension(content.mimeType) || 'dat'); const sha1 = calculateSha1(buffer) + '.' + (mime.getExtension(content.mimeType) || 'dat');
if (this._options.includeTraceInfo)
content._sha1 = sha1;
else
content._file = sha1;
if (this._started) if (this._started)
this._delegate.onContentBlob(content._sha1, buffer); this._delegate.onContentBlob(sha1, buffer);
} }
} }
@ -340,16 +374,19 @@ export class HarTracer {
headersSize: -1, headersSize: -1,
bodySize: -1, bodySize: -1,
redirectURL: '', redirectURL: '',
_transferSize: -1 _transferSize: this._options.omitSizes ? undefined : -1
}; };
if (!this._options.omitTiming) {
const timing = response.timing(); const timing = response.timing();
if (pageEntry.startedDateTime.valueOf() > timing.startTime) if (pageEntry && pageEntry.startedDateTime.valueOf() > timing.startTime)
pageEntry.startedDateTime = new Date(timing.startTime); pageEntry.startedDateTime = new Date(timing.startTime);
const dns = timing.domainLookupEnd !== -1 ? helper.millisToRoundishMillis(timing.domainLookupEnd - timing.domainLookupStart) : -1; const dns = timing.domainLookupEnd !== -1 ? helper.millisToRoundishMillis(timing.domainLookupEnd - timing.domainLookupStart) : -1;
const connect = timing.connectEnd !== -1 ? helper.millisToRoundishMillis(timing.connectEnd - timing.connectStart) : -1; const connect = timing.connectEnd !== -1 ? helper.millisToRoundishMillis(timing.connectEnd - timing.connectStart) : -1;
const ssl = timing.connectEnd !== -1 ? helper.millisToRoundishMillis(timing.connectEnd - timing.secureConnectionStart) : -1; const ssl = timing.connectEnd !== -1 ? helper.millisToRoundishMillis(timing.connectEnd - timing.secureConnectionStart) : -1;
const wait = timing.responseStart !== -1 ? helper.millisToRoundishMillis(timing.responseStart - timing.requestStart) : -1; const wait = timing.responseStart !== -1 ? helper.millisToRoundishMillis(timing.responseStart - timing.requestStart) : -1;
const receive = response.request()._responseEndTiming !== -1 ? helper.millisToRoundishMillis(response.request()._responseEndTiming - timing.responseStart) : -1; const receive = response.request()._responseEndTiming !== -1 ? helper.millisToRoundishMillis(response.request()._responseEndTiming - timing.responseStart) : -1;
harEntry.timings = { harEntry.timings = {
dns, dns,
connect, connect,
@ -359,24 +396,34 @@ export class HarTracer {
receive, receive,
}; };
harEntry.time = [dns, connect, ssl, wait, receive].reduce((pre, cur) => cur > 0 ? cur + pre : pre, 0); harEntry.time = [dns, connect, ssl, wait, receive].reduce((pre, cur) => cur > 0 ? cur + pre : pre, 0);
}
if (!this._options.omitServerIP) {
this._addBarrier(page, response.serverAddr().then(server => { this._addBarrier(page, response.serverAddr().then(server => {
if (server?.ipAddress) if (server?.ipAddress)
harEntry.serverIPAddress = server.ipAddress; harEntry.serverIPAddress = server.ipAddress;
if (server?.port) if (server?.port)
harEntry._serverPort = server.port; harEntry._serverPort = server.port;
})); }));
}
if (!this._options.omitSecurityDetails) {
this._addBarrier(page, response.securityDetails().then(details => { this._addBarrier(page, response.securityDetails().then(details => {
if (details) if (details)
harEntry._securityDetails = details; harEntry._securityDetails = details;
})); }));
}
this._addBarrier(page, request.rawRequestHeaders().then(headers => { this._addBarrier(page, request.rawRequestHeaders().then(headers => {
if (!this._options.omitCookies) {
for (const header of headers.filter(header => header.name.toLowerCase() === 'cookie')) for (const header of headers.filter(header => header.name.toLowerCase() === 'cookie'))
harEntry.request.cookies.push(...header.value.split(';').map(parseCookie)); harEntry.request.cookies.push(...header.value.split(';').map(parseCookie));
}
harEntry.request.headers = headers; harEntry.request.headers = headers;
})); }));
this._addBarrier(page, response.rawResponseHeaders().then(headers => { this._addBarrier(page, response.rawResponseHeaders().then(headers => {
if (!this._options.omitCookies) {
for (const header of headers.filter(header => header.name.toLowerCase() === 'set-cookie')) for (const header of headers.filter(header => header.name.toLowerCase() === 'set-cookie'))
harEntry.response.cookies.push(parseCookie(header.value)); harEntry.response.cookies.push(parseCookie(header.value));
}
harEntry.response.headers = headers; harEntry.response.headers = headers;
const contentType = headers.find(header => header.name.toLowerCase() === 'content-type'); const contentType = headers.find(header => header.name.toLowerCase() === 'content-type');
if (contentType) if (contentType)
@ -404,19 +451,21 @@ export class HarTracer {
name: context?._browser.options.name || '', name: context?._browser.options.name || '',
version: context?._browser.version() || '' version: context?._browser.version() || ''
}, },
pages: Array.from(this._pageEntries.values()), pages: this._pageEntries.size ? Array.from(this._pageEntries.values()) : undefined,
entries: [], entries: [],
}; };
for (const pageEntry of log.pages!) { if (!this._options.omitTiming) {
if (pageEntry.pageTimings.onContentLoad! >= 0) for (const pageEntry of log.pages || []) {
pageEntry.pageTimings.onContentLoad! -= pageEntry.startedDateTime.valueOf(); if (typeof pageEntry.pageTimings.onContentLoad === 'number' && pageEntry.pageTimings.onContentLoad >= 0)
pageEntry.pageTimings.onContentLoad -= pageEntry.startedDateTime.valueOf();
else else
pageEntry.pageTimings.onContentLoad = -1; pageEntry.pageTimings.onContentLoad = -1;
if (pageEntry.pageTimings.onLoad! >= 0) if (typeof pageEntry.pageTimings.onLoad === 'number' && pageEntry.pageTimings.onLoad >= 0)
pageEntry.pageTimings.onLoad! -= pageEntry.startedDateTime.valueOf(); pageEntry.pageTimings.onLoad -= pageEntry.startedDateTime.valueOf();
else else
pageEntry.pageTimings.onLoad = -1; pageEntry.pageTimings.onLoad = -1;
} }
}
this._pageEntries.clear(); this._pageEntries.clear();
return log; return log;
} }
@ -446,8 +495,12 @@ export class HarTracer {
result.text = postData.toString(); result.text = postData.toString();
if (content === 'attach') { if (content === 'attach') {
result._sha1 = calculateSha1(postData) + '.' + (mime.getExtension(contentType) || 'dat'); const sha1 = calculateSha1(postData) + '.' + (mime.getExtension(contentType) || 'dat');
this._delegate.onContentBlob(result._sha1, postData); if (this._options.includeTraceInfo)
result._sha1 = sha1;
else
result._file = sha1;
this._delegate.onContentBlob(sha1, postData);
} }
if (contentType === 'application/x-www-form-urlencoded') { if (contentType === 'application/x-www-form-urlencoded') {
@ -461,11 +514,10 @@ export class HarTracer {
} }
function createHarEntry(method: string, url: URL, requestref: string, frameref: string): har.Entry { function createHarEntry(method: string, url: URL, frameref: string | undefined, options: HarTracerOptions): har.Entry {
const harEntry: har.Entry = { const harEntry: har.Entry = {
_requestref: requestref, _frameref: options.includeTraceInfo ? frameref : undefined,
_frameref: frameref, _monotonicTime: options.includeTraceInfo ? monotonicTime() : undefined,
_monotonicTime: monotonicTime(),
startedDateTime: new Date(), startedDateTime: new Date(),
time: -1, time: -1,
request: { request: {
@ -476,7 +528,7 @@ function createHarEntry(method: string, url: URL, requestref: string, frameref:
headers: [], headers: [],
queryString: [...url.searchParams].map(e => ({ name: e[0], value: e[1] })), queryString: [...url.searchParams].map(e => ({ name: e[0], value: e[1] })),
headersSize: -1, headersSize: -1,
bodySize: 0, bodySize: -1,
}, },
response: { response: {
status: -1, status: -1,
@ -491,12 +543,9 @@ function createHarEntry(method: string, url: URL, requestref: string, frameref:
headersSize: -1, headersSize: -1,
bodySize: -1, bodySize: -1,
redirectURL: '', redirectURL: '',
_transferSize: -1 _transferSize: options.omitSizes ? undefined : -1
},
cache: {
beforeRequest: null,
afterRequest: null,
}, },
cache: {},
timings: { timings: {
send: -1, send: -1,
wait: -1, wait: -1,

View file

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

View file

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

View file

@ -10676,6 +10676,12 @@ export interface BrowserType<Unused = {}> {
*/ */
path: string; path: string;
/**
* 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`.
*/
mode?: "full"|"minimal";
/** /**
* A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was * A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was
* provided and the passed URL is a path, it gets merged via the * provided and the passed URL is a path, it gets merged via the
@ -11863,6 +11869,12 @@ export interface AndroidDevice {
*/ */
path: string; path: string;
/**
* 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`.
*/
mode?: "full"|"minimal";
/** /**
* A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was * A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was
* provided and the passed URL is a path, it gets merged via the * provided and the passed URL is a path, it gets merged via the
@ -13433,6 +13445,12 @@ export interface Browser extends EventEmitter {
*/ */
path: string; path: string;
/**
* 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`.
*/
mode?: "full"|"minimal";
/** /**
* A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was * A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was
* provided and the passed URL is a path, it gets merged via the * provided and the passed URL is a path, it gets merged via the
@ -14219,6 +14237,12 @@ export interface Electron {
*/ */
path: string; path: string;
/**
* 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`.
*/
mode?: "full"|"minimal";
/** /**
* A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was * A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was
* provided and the passed URL is a path, it gets merged via the * provided and the passed URL is a path, it gets merged via the
@ -16038,6 +16062,12 @@ export interface BrowserContextOptions {
*/ */
path: string; path: string;
/**
* 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`.
*/
mode?: "full"|"minimal";
/** /**
* A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was * A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was
* provided and the passed URL is a path, it gets merged via the * provided and the passed URL is a path, it gets merged via the

View file

@ -108,7 +108,7 @@ export class SnapshotRenderer {
// First try locating exact resource belonging to this frame. // First try locating exact resource belonging to this frame.
for (const resource of this._resources) { for (const resource of this._resources) {
if (resource._monotonicTime >= snapshot.timestamp) if (typeof resource._monotonicTime === 'number' && resource._monotonicTime >= snapshot.timestamp)
break; break;
if (resource._frameref !== snapshot.frameId) if (resource._frameref !== snapshot.frameId)
continue; continue;
@ -121,7 +121,7 @@ export class SnapshotRenderer {
if (!result) { if (!result) {
// Then fall back to resource with this URL to account for memory cache. // Then fall back to resource with this URL to account for memory cache.
for (const resource of this._resources) { for (const resource of this._resources) {
if (resource._monotonicTime >= snapshot.timestamp) if (typeof resource._monotonicTime === 'number' && resource._monotonicTime >= snapshot.timestamp)
break; break;
if (resource.request.url === url) if (resource.request.url === url)
return resource; return resource;

View file

@ -116,7 +116,7 @@ export function resourcesForAction(action: ActionTraceEvent): ResourceSnapshot[]
const nextAction = next(action); const nextAction = next(action);
result = context(action).resources.filter(resource => { result = context(action).resources.filter(resource => {
return resource._monotonicTime > action.metadata.startTime && (!nextAction || resource._monotonicTime < nextAction.metadata.startTime); return typeof resource._monotonicTime === 'number' && resource._monotonicTime > action.metadata.startTime && (!nextAction || resource._monotonicTime < nextAction.metadata.startTime);
}); });
(action as any)[resourcesSymbol] = result; (action as any)[resourcesSymbol] = result;
return result; return result;

View file

@ -22,7 +22,6 @@
], ],
"entries": [ "entries": [
{ {
"_requestref": "request@ee2a0dc164935fcd4d9432d37b245f3c",
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
"_monotonicTime": 270572145.898, "_monotonicTime": 270572145.898,
"startedDateTime": "2022-06-10T04:27:32.146Z", "startedDateTime": "2022-06-10T04:27:32.146Z",
@ -92,7 +91,6 @@
"_securityDetails": {} "_securityDetails": {}
}, },
{ {
"_requestref": "request@f2ff0fd79321ff90d0bc1b5d6fc13bad",
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
"_monotonicTime": 270572174.683, "_monotonicTime": 270572174.683,
"startedDateTime": "2022-06-10T04:27:32.172Z", "startedDateTime": "2022-06-10T04:27:32.172Z",
@ -162,7 +160,6 @@
"_securityDetails": {} "_securityDetails": {}
}, },
{ {
"_requestref": "request@f2ff0fd79321ff90d0bc1b5d6fc13bac",
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
"_monotonicTime": 270572174.683, "_monotonicTime": 270572174.683,
"startedDateTime": "2022-06-10T04:27:32.174Z", "startedDateTime": "2022-06-10T04:27:32.174Z",
@ -232,7 +229,6 @@
"_securityDetails": {} "_securityDetails": {}
}, },
{ {
"_requestref": "request@9626f59acb1f4a95f25112d32e9f7f60",
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
"_monotonicTime": 270572175.042, "_monotonicTime": 270572175.042,
"startedDateTime": "2022-06-10T04:27:32.175Z", "startedDateTime": "2022-06-10T04:27:32.175Z",
@ -297,7 +293,6 @@
"_securityDetails": {} "_securityDetails": {}
}, },
{ {
"_requestref": "request@d7ee53396148a663b819c348c53b03fb",
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
"_monotonicTime": 270572181.822, "_monotonicTime": 270572181.822,
"startedDateTime": "2022-06-10T04:27:32.182Z", "startedDateTime": "2022-06-10T04:27:32.182Z",

View file

@ -22,7 +22,6 @@
], ],
"entries": [ "entries": [
{ {
"_requestref": "request@7d6e0ddb1e1e25f6e5c4a7c943c0bae1",
"_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f", "_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f",
"_monotonicTime": 110928357.437, "_monotonicTime": 110928357.437,
"startedDateTime": "2022-06-16T21:41:23.951Z", "startedDateTime": "2022-06-16T21:41:23.951Z",
@ -201,7 +200,6 @@
} }
}, },
{ {
"_requestref": "request@5c7a316ee46a095bda80c23ddc8c740d",
"_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f", "_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f",
"_monotonicTime": 110928427.603, "_monotonicTime": 110928427.603,
"startedDateTime": "2022-06-16T21:41:24.022Z", "startedDateTime": "2022-06-16T21:41:24.022Z",
@ -358,7 +356,6 @@
"_securityDetails": {} "_securityDetails": {}
}, },
{ {
"_requestref": "request@17664a6093c12c97d41efbff3a502adb",
"_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f", "_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f",
"_monotonicTime": 110928455.901, "_monotonicTime": 110928455.901,
"startedDateTime": "2022-06-16T21:41:24.050Z", "startedDateTime": "2022-06-16T21:41:24.050Z",

View file

@ -22,7 +22,6 @@
], ],
"entries": [ "entries": [
{ {
"_requestref": "request@ee2a0dc164935fcd4d9432d37b245f3c",
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
"_monotonicTime": 270572145.898, "_monotonicTime": 270572145.898,
"startedDateTime": "2022-06-10T04:27:32.146Z", "startedDateTime": "2022-06-10T04:27:32.146Z",
@ -69,7 +68,7 @@
"size": 12, "size": 12,
"mimeType": "text/html", "mimeType": "text/html",
"compression": 0, "compression": 0,
"_sha1": "har-sha1-main-response.txt" "_file": "har-sha1-main-response.txt"
}, },
"headersSize": 64, "headersSize": 64,
"bodySize": 71, "bodySize": 71,

View file

@ -255,7 +255,7 @@ it('should round-trip har.zip', async ({ contextFactory, isAndroid, server }, te
it.fixme(isAndroid); it.fixme(isAndroid);
const harPath = testInfo.outputPath('har.zip'); const harPath = testInfo.outputPath('har.zip');
const context1 = await contextFactory({ recordHar: { path: harPath } }); const context1 = await contextFactory({ recordHar: { mode: 'minimal', path: harPath } });
const page1 = await context1.newPage(); const page1 = await context1.newPage();
await page1.goto(server.PREFIX + '/one-style.html'); await page1.goto(server.PREFIX + '/one-style.html');
await context1.close(); await context1.close();
@ -272,7 +272,7 @@ it('should round-trip extracted har.zip', async ({ contextFactory, isAndroid, se
it.fixme(isAndroid); it.fixme(isAndroid);
const harPath = testInfo.outputPath('har.zip'); const harPath = testInfo.outputPath('har.zip');
const context1 = await contextFactory({ recordHar: { path: harPath } }); const context1 = await contextFactory({ recordHar: { mode: 'minimal', path: harPath } });
const page1 = await context1.newPage(); const page1 = await context1.newPage();
await page1.goto(server.PREFIX + '/one-style.html'); await page1.goto(server.PREFIX + '/one-style.html');
await context1.close(); await context1.close();
@ -296,7 +296,7 @@ it('should round-trip har with postData', async ({ contextFactory, isAndroid, se
}); });
const harPath = testInfo.outputPath('har.zip'); const harPath = testInfo.outputPath('har.zip');
const context1 = await contextFactory({ recordHar: { path: harPath } }); const context1 = await contextFactory({ recordHar: { mode: 'minimal', path: harPath } });
const page1 = await context1.newPage(); const page1 = await context1.newPage();
await page1.goto(server.EMPTY_PAGE); await page1.goto(server.EMPTY_PAGE);
const fetchFunction = async (body: string) => { const fetchFunction = async (body: string) => {
@ -327,7 +327,7 @@ it('should disambiguate by header', async ({ contextFactory, isAndroid, server }
}); });
const harPath = testInfo.outputPath('har.zip'); const harPath = testInfo.outputPath('har.zip');
const context1 = await contextFactory({ recordHar: { path: harPath } }); const context1 = await contextFactory({ recordHar: { mode: 'minimal', path: harPath } });
const page1 = await context1.newPage(); const page1 = await context1.newPage();
await page1.goto(server.EMPTY_PAGE); await page1.goto(server.EMPTY_PAGE);

View file

@ -291,7 +291,7 @@ it('should omit content', async ({ contextFactory, server }, testInfo) => {
await page.evaluate(() => fetch('/pptr.png').then(r => r.arrayBuffer())); await page.evaluate(() => fetch('/pptr.png').then(r => r.arrayBuffer()));
const log = await getLog(); const log = await getLog();
expect(log.entries[0].response.content.text).toBe(undefined); expect(log.entries[0].response.content.text).toBe(undefined);
expect(log.entries[0].response.content._sha1).toBe(undefined); expect(log.entries[0].response.content._file).toBe(undefined);
}); });
it('should omit content legacy', async ({ contextFactory, server }, testInfo) => { it('should omit content legacy', async ({ contextFactory, server }, testInfo) => {
@ -300,7 +300,7 @@ it('should omit content legacy', async ({ contextFactory, server }, testInfo) =>
await page.evaluate(() => fetch('/pptr.png').then(r => r.arrayBuffer())); await page.evaluate(() => fetch('/pptr.png').then(r => r.arrayBuffer()));
const log = await getLog(); const log = await getLog();
expect(log.entries[0].response.content.text).toBe(undefined); expect(log.entries[0].response.content.text).toBe(undefined);
expect(log.entries[0].response.content._sha1).toBe(undefined); expect(log.entries[0].response.content._file).toBe(undefined);
}); });
it('should attach content', async ({ contextFactory, server }, testInfo) => { it('should attach content', async ({ contextFactory, server }, testInfo) => {
@ -312,19 +312,19 @@ it('should attach content', async ({ contextFactory, server }, testInfo) => {
expect(log.entries[0].response.content.encoding).toBe(undefined); expect(log.entries[0].response.content.encoding).toBe(undefined);
expect(log.entries[0].response.content.mimeType).toBe('text/html; charset=utf-8'); expect(log.entries[0].response.content.mimeType).toBe('text/html; charset=utf-8');
expect(log.entries[0].response.content._sha1).toContain('75841480e2606c03389077304342fac2c58ccb1b'); expect(log.entries[0].response.content._file).toContain('75841480e2606c03389077304342fac2c58ccb1b');
expect(log.entries[0].response.content.size).toBeGreaterThanOrEqual(96); expect(log.entries[0].response.content.size).toBeGreaterThanOrEqual(96);
expect(log.entries[0].response.content.compression).toBe(0); expect(log.entries[0].response.content.compression).toBe(0);
expect(log.entries[1].response.content.encoding).toBe(undefined); expect(log.entries[1].response.content.encoding).toBe(undefined);
expect(log.entries[1].response.content.mimeType).toBe('text/css; charset=utf-8'); expect(log.entries[1].response.content.mimeType).toBe('text/css; charset=utf-8');
expect(log.entries[1].response.content._sha1).toContain('79f739d7bc88e80f55b9891a22bf13a2b4e18adb'); expect(log.entries[1].response.content._file).toContain('79f739d7bc88e80f55b9891a22bf13a2b4e18adb');
expect(log.entries[1].response.content.size).toBeGreaterThanOrEqual(37); expect(log.entries[1].response.content.size).toBeGreaterThanOrEqual(37);
expect(log.entries[1].response.content.compression).toBe(0); expect(log.entries[1].response.content.compression).toBe(0);
expect(log.entries[2].response.content.encoding).toBe(undefined); expect(log.entries[2].response.content.encoding).toBe(undefined);
expect(log.entries[2].response.content.mimeType).toBe('image/png'); expect(log.entries[2].response.content.mimeType).toBe('image/png');
expect(log.entries[2].response.content._sha1).toContain('a4c3a18f0bb83f5d9fe7ce561e065c36205762fa'); expect(log.entries[2].response.content._file).toContain('a4c3a18f0bb83f5d9fe7ce561e065c36205762fa');
expect(log.entries[2].response.content.size).toBeGreaterThanOrEqual(6000); expect(log.entries[2].response.content.size).toBeGreaterThanOrEqual(6000);
expect(log.entries[2].response.content.compression).toBe(0); expect(log.entries[2].response.content.compression).toBe(0);
@ -689,45 +689,6 @@ it('should have different hars for concurrent contexts', async ({ contextFactory
} }
}); });
it('should include _requestref', async ({ contextFactory, server }, testInfo) => {
const { page, getLog } = await pageWithHar(contextFactory, testInfo);
const resp = await page.goto(server.EMPTY_PAGE);
const log = await getLog();
expect(log.entries.length).toBe(1);
const entry = log.entries[0];
expect(entry._requestref).toMatch(/^request@[a-f0-9]{32}$/);
expect(entry._requestref).toBe((resp.request() as any)._guid);
});
it('should include _requestref for redirects', async ({ contextFactory, server }, testInfo) => {
server.setRedirect('/start', '/one-more');
server.setRedirect('/one-more', server.EMPTY_PAGE);
const { page, getLog, context } = await pageWithHar(contextFactory, testInfo);
const requests = new Map<string, string>();
context.on('request', request => {
requests.set(request.url(), (request as any)._guid);
});
await page.goto(server.PREFIX + '/start');
const log = await getLog();
expect(log.entries.length).toBe(3);
const entryStart = log.entries[0];
expect(entryStart.request.url).toBe(server.PREFIX + '/start');
expect(entryStart._requestref).toBe(requests.get(entryStart.request.url));
const entryOneMore = log.entries[1];
expect(entryOneMore.request.url).toBe(server.PREFIX + '/one-more');
expect(entryOneMore._requestref).toBe(requests.get(entryOneMore.request.url));
const entryEmptyPage = log.entries[2];
expect(entryEmptyPage.request.url).toBe(server.EMPTY_PAGE);
expect(entryEmptyPage._requestref).toBe(requests.get(entryEmptyPage.request.url));
});
it('should include API request', async ({ contextFactory, server }, testInfo) => { it('should include API request', async ({ contextFactory, server }, testInfo) => {
const { page, getLog } = await pageWithHar(contextFactory, testInfo); const { page, getLog } = await pageWithHar(contextFactory, testInfo);
const url = server.PREFIX + '/simple.json'; const url = server.PREFIX + '/simple.json';

View file

@ -528,7 +528,6 @@ test('should store postData for global request', async ({ request, server }, tes
const actions = trace.events.filter(e => e.type === 'resource-snapshot'); const actions = trace.events.filter(e => e.type === 'resource-snapshot');
expect(actions).toHaveLength(1); expect(actions).toHaveLength(1);
const req = actions[0].snapshot.request; const req = actions[0].snapshot.request;
console.log(JSON.stringify(req, null, 2));
expect(req.postData?._sha1).toBeTruthy(); expect(req.postData?._sha1).toBeTruthy();
expect(req).toEqual(expect.objectContaining({ expect(req).toEqual(expect.objectContaining({
method: 'POST', method: 'POST',