chore: migrate tracing to har (#8417)

chore: migrate tracing to har

- `HarTracer` is used by both `HarRecorder` that implements
  `recordHar` context option, and by tracing.
- We keep the `trace.network` format for now, so it is not
  yet a valid har file, but it contains har entries.
This commit is contained in:
Dmitry Gozman 2021-08-24 21:09:41 -07:00 committed by GitHub
parent cccc2ac4bc
commit 29eb6cb777
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 253 additions and 176 deletions

View file

@ -31,7 +31,7 @@ import path from 'path';
import { CallMetadata, internalCallMetadata, createInstrumentation, SdkObject } from './instrumentation';
import { Debugger } from './supplements/debugger';
import { Tracing } from './trace/recorder/tracing';
import { HarTracer } from './supplements/har/harTracer';
import { HarRecorder } from './supplements/har/harRecorder';
import { RecorderSupplement } from './supplements/recorderSupplement';
import * as consoleApiSource from '../generated/consoleApiSource';
@ -61,7 +61,7 @@ export abstract class BrowserContext extends SdkObject {
readonly _browserContextId: string | undefined;
private _selectors?: Selectors;
private _origins = new Set<string>();
private _harTracer: HarTracer | undefined;
private _harRecorder: HarRecorder | undefined;
readonly tracing: Tracing;
constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) {
@ -74,7 +74,7 @@ export abstract class BrowserContext extends SdkObject {
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
if (this._options.recordHar)
this._harTracer = new HarTracer(this, this._options.recordHar);
this._harRecorder = new HarRecorder(this, this._options.recordHar);
this.tracing = new Tracing(this);
}
@ -272,7 +272,7 @@ export abstract class BrowserContext extends SdkObject {
this.emit(BrowserContext.Events.BeforeClose);
this._closedStatus = 'closing';
await this._harTracer?.flush();
await this._harRecorder?.flush();
await this.tracing.dispose();
// Cleanup.

View file

@ -18,37 +18,45 @@ import { HttpServer } from '../../utils/httpServer';
import { BrowserContext } from '../browserContext';
import { eventsHelper } from '../../utils/eventsHelper';
import { Page } from '../page';
import { FrameSnapshot, ResourceSnapshot } from './snapshotTypes';
import { FrameSnapshot } from './snapshotTypes';
import { SnapshotRenderer } from './snapshotRenderer';
import { SnapshotServer } from './snapshotServer';
import { BaseSnapshotStorage } from './snapshotStorage';
import { Snapshotter, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
import { ElementHandle } from '../dom';
import { HarTracer, HarTracerDelegate } from '../supplements/har/harTracer';
import * as har from '../supplements/har/har';
export class InMemorySnapshotter extends BaseSnapshotStorage implements SnapshotterDelegate {
export class InMemorySnapshotter extends BaseSnapshotStorage implements SnapshotterDelegate, HarTracerDelegate {
private _blobs = new Map<string, Buffer>();
private _server: HttpServer;
private _snapshotter: Snapshotter;
private _harTracer: HarTracer;
constructor(context: BrowserContext) {
super();
this._server = new HttpServer();
new SnapshotServer(this._server, this);
this._snapshotter = new Snapshotter(context, this);
this._harTracer = new HarTracer(context, this, { content: 'sha1', waitForContentOnStop: false, skipScripts: true });
}
async initialize(): Promise<string> {
await this._snapshotter.start();
this._harTracer.start();
return await this._server.start();
}
async reset() {
await this._snapshotter.reset();
await this._harTracer.stop();
this._harTracer.start();
this.clear();
}
async dispose() {
this._snapshotter.dispose();
await this._harTracer.stop();
await this._server.stop();
}
@ -67,12 +75,19 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
});
}
onBlob(blob: SnapshotterBlob): void {
this._blobs.set(blob.sha1, blob.buffer);
onEntryStarted(entry: har.Entry) {
}
onResourceSnapshot(resource: ResourceSnapshot): void {
this.addResource(resource);
onEntryFinished(entry: har.Entry) {
this.addResource(entry);
}
onContentBlob(sha1: string, buffer: Buffer) {
this._blobs.set(sha1, buffer);
}
onSnapshotterBlob(blob: SnapshotterBlob): void {
this._blobs.set(blob.sha1, blob.buffer);
}
onFrameSnapshot(snapshot: FrameSnapshot): void {

View file

@ -14,27 +14,9 @@
* limitations under the License.
*/
export type ResourceSnapshot = {
_frameref: string,
request: {
url: string,
method: string,
headers: { name: string, value: string }[],
postData?: {
text: string,
_sha1?: string,
},
},
response: {
status: number,
headers: { name: string, value: string }[],
content: {
mimeType: string,
_sha1?: string,
},
},
_monotonicTime: number,
};
import { Entry as HAREntry } from '../supplements/har/har';
export type ResourceSnapshot = HAREntry;
export type NodeSnapshot =
// Text node.

View file

@ -16,13 +16,12 @@
import { BrowserContext } from '../browserContext';
import { Page } from '../page';
import * as network from '../network';
import { eventsHelper, RegisteredListener } from '../../utils/eventsHelper';
import { debugLogger } from '../../utils/debugLogger';
import { Frame } from '../frames';
import { frameSnapshotStreamer, SnapshotData } from './snapshotterInjected';
import { calculateSha1, createGuid, monotonicTime } from '../../utils/utils';
import { FrameSnapshot, ResourceSnapshot } from './snapshotTypes';
import { FrameSnapshot } from './snapshotTypes';
import { ElementHandle } from '../dom';
export type SnapshotterBlob = {
@ -31,8 +30,7 @@ export type SnapshotterBlob = {
};
export interface SnapshotterDelegate {
onBlob(blob: SnapshotterBlob): void;
onResourceSnapshot(resource: ResourceSnapshot): void;
onSnapshotterBlob(blob: SnapshotterBlob): void;
onFrameSnapshot(snapshot: FrameSnapshot): void;
}
@ -43,7 +41,6 @@ export class Snapshotter {
private _snapshotStreamer: string;
private _initialized = false;
private _started = false;
private _fetchedResponses = new Map<network.Response, string>();
constructor(context: BrowserContext, delegate: SnapshotterDelegate) {
this._context = context;
@ -63,12 +60,6 @@ export class Snapshotter {
await this._initialize();
}
await this.reset();
// Replay resources loaded in all pages.
for (const page of this._context.pages()) {
for (const response of page._frameManager._responses)
this._saveResource(response).catch(e => debugLogger.log('error', e));
}
}
async reset() {
@ -85,9 +76,6 @@ export class Snapshotter {
this._onPage(page);
this._eventListeners = [
eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, this._onPage.bind(this)),
eventsHelper.addEventListener(this._context, BrowserContext.Events.Response, (response: network.Response) => {
this._saveResource(response).catch(e => debugLogger.log('error', e));
}),
];
const initScript = `(${frameSnapshotStreamer})("${this._snapshotStreamer}")`;
@ -141,7 +129,7 @@ export class Snapshotter {
if (typeof content === 'string') {
const buffer = Buffer.from(content);
const sha1 = calculateSha1(buffer) + mimeToExtension(contentType);
this._delegate.onBlob({ sha1, buffer });
this._delegate.onSnapshotterBlob({ sha1, buffer });
snapshot.resourceOverrides.push({ url, sha1 });
} else {
snapshot.resourceOverrides.push({ url, ref: content });
@ -159,72 +147,6 @@ export class Snapshotter {
this._eventListeners.push(eventsHelper.addEventListener(page, Page.Events.FrameAttached, frame => this._annotateFrameHierarchy(frame)));
}
private async _saveResource(response: network.Response) {
if (!this._started)
return;
const isRedirect = response.status() >= 300 && response.status() <= 399;
if (isRedirect)
return;
// We do not need scripts for snapshots.
if (response.request().resourceType() === 'script')
return;
// Shortcut all redirects - we cannot intercept them properly.
let original = response.request();
while (original.redirectedFrom())
original = original.redirectedFrom()!;
const url = original.url();
let contentType = '';
for (const { name, value } of response.headers()) {
if (name.toLowerCase() === 'content-type')
contentType = value;
}
const method = original.method();
const status = response.status();
const requestBody = original.postDataBuffer();
const requestSha1 = requestBody ? calculateSha1(requestBody) + mimeToExtension(contentType) : '';
if (requestBody)
this._delegate.onBlob({ sha1: requestSha1, buffer: requestBody });
const requestHeaders = original.headers();
// Only fetch response bodies once.
let responseSha1 = this._fetchedResponses.get(response);
{
if (responseSha1 === undefined) {
const body = await response.body().catch(e => debugLogger.log('error', e));
// Bail out after each async hop.
if (!this._started)
return;
responseSha1 = body ? calculateSha1(body) + mimeToExtension(contentType) : '';
if (body)
this._delegate.onBlob({ sha1: responseSha1, buffer: body });
this._fetchedResponses.set(response, responseSha1);
}
}
const resource: ResourceSnapshot = {
_frameref: response.frame().guid,
request: {
url,
method,
headers: requestHeaders,
postData: requestSha1 ? { text: '', _sha1: requestSha1 } : undefined,
},
response: {
status,
headers: response.headers(),
content: {
mimeType: contentType,
_sha1: responseSha1,
},
},
_monotonicTime: monotonicTime()
};
this._delegate.onResourceSnapshot(resource);
}
private async _annotateFrameHierarchy(frame: Frame) {
try {
const frameElement = await frame.frameElement();

View file

@ -0,0 +1,57 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs';
import { BrowserContext } from '../../browserContext';
import * as har from './har';
import { HarTracer } from './harTracer';
type HarOptions = {
path: string;
omitContent?: boolean;
};
export class HarRecorder {
private _options: HarOptions;
private _tracer: HarTracer;
private _entries: har.Entry[] = [];
constructor(context: BrowserContext, options: HarOptions) {
this._options = options;
this._tracer = new HarTracer(context, this, {
content: options.omitContent ? 'omit' : 'embedded',
waitForContentOnStop: true,
skipScripts: false,
});
this._tracer.start();
}
onEntryStarted(entry: har.Entry) {
this._entries.push(entry);
}
onEntryFinished(entry: har.Entry) {
}
onContentBlob(sha1: string, buffer: Buffer) {
}
async flush() {
const log = await this._tracer.stop();
log.entries = this._entries;
await fs.promises.writeFile(this._options.path, JSON.stringify({ log }, undefined, 2));
}
}

View file

@ -15,49 +15,60 @@
*/
import { URL } from 'url';
import fs from 'fs';
import { BrowserContext } from '../../browserContext';
import { helper } from '../../helper';
import * as network from '../../network';
import { Page } from '../../page';
import * as har from './har';
import * as types from '../../types';
import { monotonicTime } from '../../../utils/utils';
import { calculateSha1, monotonicTime } from '../../../utils/utils';
import { eventsHelper, RegisteredListener } from '../../../utils/eventsHelper';
const FALLBACK_HTTP_VERSION = 'HTTP/1.1';
type HarOptions = {
path: string;
omitContent?: boolean;
export interface HarTracerDelegate {
onEntryStarted(entry: har.Entry): void;
onEntryFinished(entry: har.Entry): void;
onContentBlob(sha1: string, buffer: Buffer): void;
}
type HarTracerOptions = {
content: 'omit' | 'sha1' | 'embedded';
skipScripts: boolean;
waitForContentOnStop: boolean;
};
export class HarTracer {
private _options: HarOptions;
private _log: har.Log;
private _pageEntries = new Map<Page, har.Page>();
private _entries = new Map<network.Request, har.Entry>();
private _lastPage = 0;
private _context: BrowserContext;
private _barrierPromises = new Set<Promise<void>>();
private _delegate: HarTracerDelegate;
private _options: HarTracerOptions;
private _pageEntries = new Map<Page, har.Page>();
private _eventListeners: RegisteredListener[] = [];
private _started = false;
private _entrySymbol: symbol;
constructor(context: BrowserContext, options: HarOptions) {
constructor(context: BrowserContext, delegate: HarTracerDelegate, options: HarTracerOptions) {
this._context = context;
this._delegate = delegate;
this._options = options;
this._log = {
version: '1.2',
creator: {
name: 'Playwright',
version: require('../../../../package.json')['version'],
},
browser: {
name: context._browser.options.name,
version: context._browser.version()
},
pages: [],
entries: []
};
context.on(BrowserContext.Events.Page, (page: Page) => this._ensurePageEntry(page));
context.on(BrowserContext.Events.Request, (request: network.Request) => this._onRequest(request));
context.on(BrowserContext.Events.RequestFinished, (request: network.Request) => this._onRequestFinished(request).catch(() => {}));
context.on(BrowserContext.Events.Response, (response: network.Response) => this._onResponse(response));
this._entrySymbol = Symbol('requestHarEntry');
}
start() {
if (this._started)
return;
this._started = true;
this._eventListeners = [
eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, (page: Page) => this._ensurePageEntry(page)),
eventsHelper.addEventListener(this._context, BrowserContext.Events.Request, (request: network.Request) => this._onRequest(request)),
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFinished, (request: network.Request) => this._onRequestFinished(request).catch(() => {})),
eventsHelper.addEventListener(this._context, BrowserContext.Events.Response, (response: network.Response) => this._onResponse(response)),
];
}
private _entryForRequest(request: network.Request): har.Entry | undefined {
return (request as any)[this._entrySymbol];
}
private _ensurePageEntry(page: Page) {
@ -68,7 +79,7 @@ export class HarTracer {
pageEntry = {
startedDateTime: new Date(),
id: `page_${this._lastPage++}`,
id: page.guid,
title: '',
pageTimings: {
onContentLoad: -1,
@ -76,7 +87,6 @@ export class HarTracer {
},
};
this._pageEntries.set(page, pageEntry);
this._log.pages.push(pageEntry);
}
return pageEntry;
}
@ -110,6 +120,8 @@ export class HarTracer {
}
private _addBarrier(page: Page, promise: Promise<void>) {
if (!this._options.waitForContentOnStop)
return;
const race = Promise.race([
new Promise<void>(f => page.on('close', () => {
this._barrierPromises.delete(race);
@ -121,6 +133,9 @@ export class HarTracer {
}
private _onRequest(request: network.Request) {
if (this._options.skipScripts && request.resourceType() === 'script')
return;
const page = request.frame()._page;
const url = network.parsedURL(request.url());
if (!url)
@ -140,7 +155,7 @@ export class HarTracer {
cookies: [],
headers: [],
queryString: [...url.searchParams].map(e => ({ name: e[0], value: e[1] })),
postData: postDataForHar(request),
postData: postDataForHar(request, this._options.content),
headersSize: -1,
bodySize: calculateRequestBodySize(request) || 0,
},
@ -170,18 +185,21 @@ export class HarTracer {
},
};
if (request.redirectedFrom()) {
const fromEntry = this._entries.get(request.redirectedFrom()!)!;
fromEntry.response.redirectURL = request.url();
const fromEntry = this._entryForRequest(request.redirectedFrom()!);
if (fromEntry)
fromEntry.response.redirectURL = request.url();
}
this._log.entries.push(harEntry);
this._entries.set(request, harEntry);
(request as any)[this._entrySymbol] = harEntry;
if (this._started)
this._delegate.onEntryStarted(harEntry);
}
private async _onRequestFinished(request: network.Request) {
const page = request.frame()._page;
const harEntry = this._entries.get(request)!;
const harEntry = this._entryForRequest(request);
if (!harEntry)
return;
const response = await request.response();
if (!response)
return;
@ -200,25 +218,41 @@ export class HarTracer {
const content = harEntry.response.content;
content.size = buffer.length;
content.compression = harEntry.response.bodySize !== -1 ? buffer.length - harEntry.response.bodySize : 0;
if (!this._options.omitContent && buffer && buffer.length > 0) {
content.text = buffer.toString('base64');
content.encoding = 'base64';
if (buffer && buffer.length > 0) {
if (this._options.content === 'embedded') {
content.text = buffer.toString('base64');
content.encoding = 'base64';
} else if (this._options.content === 'sha1') {
content._sha1 = calculateSha1(buffer) + mimeToExtension(content.mimeType);
if (this._started)
this._delegate.onContentBlob(content._sha1, buffer);
}
}
}).catch(() => {});
}).catch(() => {}).then(() => {
const postData = response.request().postDataBuffer();
if (postData && harEntry.request.postData && this._options.content === 'sha1') {
harEntry.request.postData._sha1 = calculateSha1(postData) + mimeToExtension(harEntry.request.postData.mimeType);
if (this._started)
this._delegate.onContentBlob(harEntry.request.postData._sha1, postData);
}
if (this._started)
this._delegate.onEntryFinished(harEntry);
});
this._addBarrier(page, promise);
}
private _onResponse(response: network.Response) {
const page = response.frame()._page;
const pageEntry = this._ensurePageEntry(page);
const harEntry = this._entries.get(response.request())!;
// Rewrite provisional headers with actual
const harEntry = this._entryForRequest(response.request());
if (!harEntry)
return;
const request = response.request();
// Rewrite provisional headers with actual
harEntry.request.headers = request.headers().map(header => ({ name: header.name, value: header.value }));
harEntry.request.cookies = cookiesForHar(request.headerValue('cookie'), ';');
harEntry.request.postData = postDataForHar(request);
harEntry.request.postData = postDataForHar(request, this._options.content);
harEntry.response = {
status: response.status(),
@ -252,7 +286,6 @@ export class HarTracer {
receive,
};
harEntry.time = [dns, connect, ssl, wait, receive].reduce((pre, cur) => cur > 0 ? cur + pre : pre, 0);
this._addBarrier(page, response.serverAddr().then(server => {
if (server?.ipAddress)
harEntry.serverIPAddress = server.ipAddress;
@ -265,9 +298,27 @@ export class HarTracer {
}));
}
async flush() {
async stop() {
this._started = false;
eventsHelper.removeEventListeners(this._eventListeners);
await Promise.all(this._barrierPromises);
for (const pageEntry of this._log.pages) {
this._barrierPromises.clear();
const log: har.Log = {
version: '1.2',
creator: {
name: 'Playwright',
version: require('../../../../package.json')['version'],
},
browser: {
name: this._context._browser.options.name,
version: this._context._browser.version()
},
pages: Array.from(this._pageEntries.values()),
entries: [],
};
for (const pageEntry of log.pages) {
if (pageEntry.pageTimings.onContentLoad >= 0)
pageEntry.pageTimings.onContentLoad -= pageEntry.startedDateTime.valueOf();
else
@ -277,11 +328,12 @@ export class HarTracer {
else
pageEntry.pageTimings.onLoad = -1;
}
await fs.promises.writeFile(this._options.path, JSON.stringify({ log: this._log }, undefined, 2));
this._pageEntries.clear();
return log;
}
}
function postDataForHar(request: network.Request): har.PostData | undefined {
function postDataForHar(request: network.Request, content: 'omit' | 'sha1' | 'embedded'): har.PostData | undefined {
const postData = request.postDataBuffer();
if (!postData)
return;
@ -289,9 +341,13 @@ function postDataForHar(request: network.Request): har.PostData | undefined {
const contentType = request.headerValue('content-type') || 'application/octet-stream';
const result: har.PostData = {
mimeType: contentType,
text: contentType === 'application/octet-stream' ? '' : postData.toString(),
text: '',
params: []
};
if (content === 'embedded' && contentType !== 'application/octet-stream')
result.text = postData.toString();
if (contentType === 'application/x-www-form-urlencoded') {
const parsed = new URLSearchParams(postData.toString());
for (const [name, value] of parsed.entries())
@ -370,3 +426,31 @@ function calculateRequestBodySize(request: network.Request): number|undefined {
return;
return new TextEncoder().encode(postData.toString('utf8')).length;
}
const kMimeToExtension: { [key: string]: string } = {
'application/javascript': 'js',
'application/json': 'json',
'application/json5': 'json5',
'application/pdf': 'pdf',
'application/xhtml+xml': 'xhtml',
'application/zip': 'zip',
'font/otf': 'otf',
'font/woff': 'woff',
'font/woff2': 'woff2',
'image/bmp': 'bmp',
'image/gif': 'gif',
'image/jpeg': 'jpeg',
'image/png': 'png',
'image/tiff': 'tiff',
'image/svg+xml': 'svg',
'text/css': 'css',
'text/csv': 'csv',
'text/html': 'html',
'text/plain': 'text',
'video/mp4': 'mp4',
'video/mpeg': 'mpeg',
};
function mimeToExtension(contentType: string): string {
return '.' + (kMimeToExtension[contentType] || 'dat');
}

View file

@ -28,7 +28,9 @@ import { Page } from '../../page';
import * as trace from '../common/traceEvents';
import { commandsWithTracingSnapshots } from '../../../protocol/channels';
import { Snapshotter, SnapshotterBlob, SnapshotterDelegate } from '../../snapshot/snapshotter';
import { FrameSnapshot, ResourceSnapshot } from '../../snapshot/snapshotTypes';
import { FrameSnapshot } from '../../snapshot/snapshotTypes';
import { HarTracer, HarTracerDelegate } from '../../supplements/har/harTracer';
import * as har from '../../supplements/har/har';
export type TracerOptions = {
name?: string;
@ -49,9 +51,10 @@ type RecordingState = {
const kScreencastOptions = { width: 800, height: 600, quality: 90 };
export class Tracing implements InstrumentationListener, SnapshotterDelegate {
export class Tracing implements InstrumentationListener, SnapshotterDelegate, HarTracerDelegate {
private _writeChain = Promise.resolve();
private _snapshotter: Snapshotter;
private _harTracer: HarTracer;
private _screencastListeners: RegisteredListener[] = [];
private _pendingCalls = new Map<string, { sdkObject: SdkObject, metadata: CallMetadata, beforeSnapshot: Promise<void>, actionSnapshot?: Promise<void>, afterSnapshot?: Promise<void> }>();
private _context: BrowserContext;
@ -67,6 +70,11 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate {
this._tracesDir = context._browser.options.tracesDir;
this._resourcesDir = path.join(this._tracesDir, 'resources');
this._snapshotter = new Snapshotter(context, this);
this._harTracer = new HarTracer(context, this, {
content: 'sha1',
waitForContentOnStop: false,
skipScripts: true,
});
this._contextCreatedEvent = {
version: VERSION,
type: 'context-options',
@ -109,8 +117,10 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate {
await this._snapshotter.reset();
} else if (options.snapshots) {
await this._snapshotter.start();
this._harTracer.start();
} else if (state?.options?.snapshots) {
await this._snapshotter.stop();
await this._harTracer.stop();
}
if (state) {
@ -145,6 +155,7 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate {
this._context.instrumentation.removeListener(this);
this._stopScreencast();
await this._snapshotter.stop();
await this._harTracer.stop();
// Ensure all writes are finished.
await this._writeChain;
this._recording = undefined;
@ -243,18 +254,25 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate {
this._appendTraceEvent(event);
}
onBlob(blob: SnapshotterBlob): void {
this._appendResource(blob.sha1, blob.buffer);
onEntryStarted(entry: har.Entry) {
}
onResourceSnapshot(snapshot: ResourceSnapshot): void {
const event: trace.ResourceSnapshotTraceEvent = { type: 'resource-snapshot', snapshot };
onEntryFinished(entry: har.Entry) {
const event: trace.ResourceSnapshotTraceEvent = { type: 'resource-snapshot', snapshot: entry };
this._appendTraceOperation(async () => {
visitSha1s(event, this._recording!.sha1s);
await fs.promises.appendFile(this._recording!.networkFile, JSON.stringify(event) + '\n');
});
}
onContentBlob(sha1: string, buffer: Buffer) {
this._appendResource(sha1, buffer);
}
onSnapshotterBlob(blob: SnapshotterBlob): void {
this._appendResource(blob.sha1, blob.buffer);
}
onFrameSnapshot(snapshot: FrameSnapshot): void {
this._appendTraceEvent({ type: 'frame-snapshot', snapshot });
}

View file

@ -21,6 +21,7 @@ import fs from 'fs';
import http2 from 'http2';
import type { BrowserContext, BrowserContextOptions } from '../index';
import type { AddressInfo } from 'net';
import type { Log } from '../src/server/supplements/har/har';
async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>, testInfo: any) {
const harPath = testInfo.outputPath('test.har');
@ -31,7 +32,7 @@ async function pageWithHar(contextFactory: (options?: BrowserContextOptions) =>
context,
getLog: async () => {
await context.close();
return JSON.parse(fs.readFileSync(harPath).toString())['log'];
return JSON.parse(fs.readFileSync(harPath).toString())['log'] as Log;
}
};
}
@ -66,7 +67,7 @@ it('should have pages', async ({ contextFactory, server }, testInfo) => {
const log = await getLog();
expect(log.pages.length).toBe(1);
const pageEntry = log.pages[0];
expect(pageEntry.id).toBe('page_0');
expect(pageEntry.id).toBeTruthy();
expect(pageEntry.title).toBe('Hello');
expect(new Date(pageEntry.startedDateTime).valueOf()).toBeGreaterThan(Date.now() - 3600 * 1000);
expect(pageEntry.pageTimings.onContentLoad).toBeGreaterThan(0);
@ -83,7 +84,7 @@ it('should have pages in persistent context', async ({ launchPersistent }, testI
const log = JSON.parse(fs.readFileSync(harPath).toString())['log'];
expect(log.pages.length).toBe(1);
const pageEntry = log.pages[0];
expect(pageEntry.id).toBe('page_0');
expect(pageEntry.id).toBeTruthy();
expect(pageEntry.title).toBe('Hello');
});
@ -93,7 +94,7 @@ it('should include request', async ({ contextFactory, server }, testInfo) => {
const log = await getLog();
expect(log.entries.length).toBe(1);
const entry = log.entries[0];
expect(entry.pageref).toBe('page_0');
expect(entry.pageref).toBe(log.pages[0].id);
expect(entry.request.url).toBe(server.EMPTY_PAGE);
expect(entry.request.method).toBe('GET');
expect(entry.request.httpVersion).toBe('HTTP/1.1');
@ -339,10 +340,8 @@ it('should have popup requests', async ({ contextFactory, server }, testInfo) =>
const log = await getLog();
expect(log.pages.length).toBe(2);
expect(log.pages[0].id).toBe('page_0');
expect(log.pages[1].id).toBe('page_1');
const entries = log.entries.filter(entry => entry.pageref === 'page_1');
const entries = log.entries.filter(entry => entry.pageref === log.pages[1].id);
expect(entries.length).toBe(2);
expect(entries[0].request.url).toBe(server.PREFIX + '/one-style.html');
expect(entries[0].response.status).toBe(200);