feat(tracing): tracing.{start,stop}Chunk instead of tracing._export (#8521)

This commit is contained in:
Dmitry Gozman 2021-08-31 17:03:31 -07:00 committed by GitHub
parent bee8ed117b
commit b8f8ca7493
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 353 additions and 144 deletions

View file

@ -1,9 +1,8 @@
# class: Tracing
API for collecting and saving Playwright traces. Playwright traces can be opened using the Playwright CLI after
Playwright script runs.
API for collecting and saving Playwright traces. Playwright traces can be opened in [Trace Viewer](./trace-viewer.md) after Playwright script runs.
Start with specifying the folder traces will be stored in:
Start recording a trace before performing actions. At the end, stop tracing and save it to a file.
```js
const browser = await chromium.launch();
@ -30,6 +29,7 @@ context.tracing().stop(new Tracing.StopOptions()
browser = await chromium.launch()
context = await browser.new_context()
await context.tracing.start(screenshots=True, snapshots=True)
page = await context.new_page()
await page.goto("https://playwright.dev")
await context.tracing.stop(path = "trace.zip")
```
@ -38,6 +38,7 @@ await context.tracing.stop(path = "trace.zip")
browser = chromium.launch()
context = browser.new_context()
context.tracing.start(screenshots=True, snapshots=True)
page = context.new_page()
page.goto("https://playwright.dev")
context.tracing.stop(path = "trace.zip")
```
@ -81,15 +82,15 @@ context.tracing().stop(new Tracing.StopOptions()
```python async
await context.tracing.start(name="trace", screenshots=True, snapshots=True)
page = await context.new_page()
await page.goto("https://playwright.dev")
await context.tracing.stop()
await context.tracing.stop(path = "trace.zip")
```
```python sync
context.tracing.start(name="trace", screenshots=True, snapshots=True)
page = context.new_page()
page.goto("https://playwright.dev")
context.tracing.stop()
context.tracing.stop(path = "trace.zip")
```
@ -126,6 +127,110 @@ a timeline preview.
Whether to capture DOM snapshot on every action.
## async method: Tracing.startChunk
Start a new trace chunk. If you'd like to record multiple traces on the same [BrowserContext], use [`method: Tracing.start`] once, and then create multiple trace chunks with [`method: Tracing.startChunk`] and [`method: Tracing.stopChunk`].
```js
await context.tracing.start({ screenshots: true, snapshots: true });
const page = await context.newPage();
await page.goto('https://playwright.dev');
await context.tracing.startChunk();
await page.click('text=Get Started');
// Everything between startChunk and stopChunk will be recorded in the trace.
await context.tracing.stopChunk({ path: 'trace1.zip' });
await context.tracing.startChunk();
await page.goto('http://example.com');
// Save a second trace file with different actions.
await context.tracing.stopChunk({ path: 'trace2.zip' });
```
```java
context.tracing().start(new Tracing.StartOptions()
.setScreenshots(true)
.setSnapshots(true));
Page page = context.newPage();
page.navigate("https://playwright.dev");
context.tracing().startChunk();
page.click("text=Get Started");
// Everything between startChunk and stopChunk will be recorded in the trace.
context.tracing().stopChunk(new Tracing.StopChunkOptions()
.setPath(Paths.get("trace1.zip")));
context.tracing().startChunk();
page.navigate("http://example.com");
// Save a second trace file with different actions.
context.tracing().stopChunk(new Tracing.StopChunkOptions()
.setPath(Paths.get("trace2.zip")));
```
```python async
await context.tracing.start(name="trace", screenshots=True, snapshots=True)
page = await context.new_page()
await page.goto("https://playwright.dev")
await context.tracing.start_chunk()
await page.click("text=Get Started")
# Everything between start_chunk and stop_chunk will be recorded in the trace.
await context.tracing.stop_chunk(path = "trace1.zip")
await context.tracing.start_chunk()
await page.goto("http://example.com")
# Save a second trace file with different actions.
await context.tracing.stop_chunk(path = "trace2.zip")
```
```python sync
context.tracing.start(name="trace", screenshots=True, snapshots=True)
page = context.new_page()
page.goto("https://playwright.dev")
context.tracing.start_chunk()
page.click("text=Get Started")
# Everything between start_chunk and stop_chunk will be recorded in the trace.
context.tracing.stop_chunk(path = "trace1.zip")
context.tracing.start_chunk()
page.goto("http://example.com")
# Save a second trace file with different actions.
context.tracing.stop_chunk(path = "trace2.zip")
```
```csharp
await using var browser = playwright.Chromium.LaunchAsync();
await using var context = await browser.NewContextAsync();
await context.Tracing.StartAsync(new TracingStartOptions
{
Screenshots: true,
Snapshots: true
});
var page = context.NewPageAsync();
await page.GotoAsync("https://playwright.dev");
await context.Tracing.StartChunkAsync();
await page.ClickAsync("text=Get Started");
// Everything between StartChunkAsync and StopChunkAsync will be recorded in the trace.
await context.Tracing.StopChunkAsync(new TracingStopChunkOptions
{
Path: "trace1.zip"
});
await context.Tracing.StartChunkAsync();
await page.GotoAsync("http://example.com");
// Save a second trace file with different actions.
await context.Tracing.StopChunkAsync(new TracingStopChunkOptions
{
Path: "trace2.zip"
});
```
## async method: Tracing.stop
Stop tracing.
@ -133,4 +238,15 @@ Stop tracing.
### option: Tracing.stop.path
- `path` <[path]>
Export trace into the file with the given name.
Export trace into the file with the given path.
## async method: Tracing.stopChunk
Stop the trace chunk. See [`method: Tracing.startChunk`] for more details about multiple trace chunks.
### option: Tracing.stopChunk.path
- `path` <[path]>
Export trace collected since the last [`method: Tracing.startChunk`] call into the file with the given path.

View file

@ -28,30 +28,38 @@ export class Tracing implements api.Tracing {
async start(options: { name?: string, snapshots?: boolean, screenshots?: boolean } = {}) {
await this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
return await channel.tracingStart(options);
await channel.tracingStart(options);
await channel.tracingStartChunk();
});
}
async _export(options: { path: string }) {
async startChunk() {
await this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
await this._doExport(channel, options.path);
await channel.tracingStartChunk();
});
}
async stopChunk(options: { path: string }) {
await this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
await this._doStopChunk(channel, options.path);
});
}
async stop(options: { path?: string } = {}) {
await this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
if (options.path)
await this._doExport(channel, options.path);
await this._doStopChunk(channel, options.path);
await channel.tracingStop();
});
}
private async _doExport(channel: channels.BrowserContextChannel, path: string) {
const result = await channel.tracingExport();
private async _doStopChunk(channel: channels.BrowserContextChannel, path: string | undefined) {
const result = await channel.tracingStopChunk({ save: !!path });
if (!result.artifact)
return;
const artifact = Artifact.from(result.artifact);
if (this._context.browser()?._remoteType)
if (this._context._browser?._remoteType)
artifact._isRemote = true;
await artifact.saveAs(path);
await artifact.saveAs(path!);
await artifact.delete();
}
}

View file

@ -212,13 +212,17 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
await this._context.tracing.start(params);
}
async tracingStop(params: channels.BrowserContextTracingStopParams): Promise<channels.BrowserContextTracingStopResult> {
await this._context.tracing.stop();
async tracingStartChunk(params: channels.BrowserContextTracingStartChunkParams): Promise<channels.BrowserContextTracingStartChunkResult> {
await this._context.tracing.startChunk();
}
async tracingExport(params: channels.BrowserContextTracingExportParams): Promise<channels.BrowserContextTracingExportResult> {
const artifact = await this._context.tracing.export();
return { artifact: new ArtifactDispatcher(this._scope, artifact) };
async tracingStopChunk(params: channels.BrowserContextTracingStopChunkParams): Promise<channels.BrowserContextTracingStopChunkResult> {
const artifact = await this._context.tracing.stopChunk(params.save);
return { artifact: artifact ? new ArtifactDispatcher(this._scope, artifact) : undefined };
}
async tracingStop(params: channels.BrowserContextTracingStopParams): Promise<channels.BrowserContextTracingStopResult> {
await this._context.tracing.stop();
}
async harExport(params: channels.BrowserContextHarExportParams): Promise<channels.BrowserContextHarExportResult> {

View file

@ -763,8 +763,9 @@ export interface BrowserContextChannel extends EventTargetChannel {
recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: Metadata): Promise<BrowserContextRecorderSupplementEnableResult>;
newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: Metadata): Promise<BrowserContextNewCDPSessionResult>;
tracingStart(params: BrowserContextTracingStartParams, metadata?: Metadata): Promise<BrowserContextTracingStartResult>;
tracingStartChunk(params?: BrowserContextTracingStartChunkParams, metadata?: Metadata): Promise<BrowserContextTracingStartChunkResult>;
tracingStopChunk(params: BrowserContextTracingStopChunkParams, metadata?: Metadata): Promise<BrowserContextTracingStopChunkResult>;
tracingStop(params?: BrowserContextTracingStopParams, metadata?: Metadata): Promise<BrowserContextTracingStopResult>;
tracingExport(params?: BrowserContextTracingExportParams, metadata?: Metadata): Promise<BrowserContextTracingExportResult>;
harExport(params?: BrowserContextHarExportParams, metadata?: Metadata): Promise<BrowserContextHarExportResult>;
}
export type BrowserContextBindingCallEvent = {
@ -992,14 +993,21 @@ export type BrowserContextTracingStartOptions = {
screenshots?: boolean,
};
export type BrowserContextTracingStartResult = void;
export type BrowserContextTracingStartChunkParams = {};
export type BrowserContextTracingStartChunkOptions = {};
export type BrowserContextTracingStartChunkResult = void;
export type BrowserContextTracingStopChunkParams = {
save: boolean,
};
export type BrowserContextTracingStopChunkOptions = {
};
export type BrowserContextTracingStopChunkResult = {
artifact?: ArtifactChannel,
};
export type BrowserContextTracingStopParams = {};
export type BrowserContextTracingStopOptions = {};
export type BrowserContextTracingStopResult = void;
export type BrowserContextTracingExportParams = {};
export type BrowserContextTracingExportOptions = {};
export type BrowserContextTracingExportResult = {
artifact: ArtifactChannel,
};
export type BrowserContextHarExportParams = {};
export type BrowserContextHarExportOptions = {};
export type BrowserContextHarExportResult = {

View file

@ -706,11 +706,15 @@ BrowserContext:
snapshots: boolean?
screenshots: boolean?
tracingStop:
tracingStartChunk:
tracingExport:
tracingStopChunk:
parameters:
save: boolean
returns:
artifact: Artifact
artifact: Artifact?
tracingStop:
harExport:
returns:

View file

@ -449,8 +449,11 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
snapshots: tOptional(tBoolean),
screenshots: tOptional(tBoolean),
});
scheme.BrowserContextTracingStartChunkParams = tOptional(tObject({}));
scheme.BrowserContextTracingStopChunkParams = tObject({
save: tBoolean,
});
scheme.BrowserContextTracingStopParams = tOptional(tObject({}));
scheme.BrowserContextTracingExportParams = tOptional(tObject({}));
scheme.BrowserContextHarExportParams = tOptional(tObject({}));
scheme.PageSetDefaultNavigationTimeoutNoReplyParams = tObject({
timeout: tNumber,

View file

@ -49,14 +49,16 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
async reset() {
await this._snapshotter.reset();
await this._harTracer.stop();
await this._harTracer.flush();
this._harTracer.stop();
this._harTracer.start();
this.clear();
}
async dispose() {
this._snapshotter.dispose();
await this._harTracer.stop();
await this._harTracer.flush();
this._harTracer.stop();
await this._server.stop();
}

View file

@ -57,7 +57,8 @@ export class HarRecorder {
if (this._isFlushed)
return;
this._isFlushed = true;
const log = await this._tracer.stop();
await this._tracer.flush();
const log = this._tracer.stop();
log.entries = this._entries;
await fs.promises.writeFile(this._options.path, JSON.stringify({ log }, undefined, 2));
}

View file

@ -295,11 +295,13 @@ export class HarTracer {
}));
}
async stop() {
async flush() {
await Promise.all(this._barrierPromises);
}
stop() {
this._started = false;
eventsHelper.removeEventListeners(this._eventListeners);
await Promise.all(this._barrierPromises);
this._barrierPromises.clear();
const log: har.Log = {

View file

@ -45,8 +45,9 @@ type RecordingState = {
traceName: string,
networkFile: string,
traceFile: string,
lastReset: number,
filesCount: number,
sha1s: Set<string>,
recording: boolean;
};
const kScreencastOptions = { width: 800, height: 600, quality: 90 };
@ -59,7 +60,7 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
private _pendingCalls = new Map<string, { sdkObject: SdkObject, metadata: CallMetadata, beforeSnapshot: Promise<void>, actionSnapshot?: Promise<void>, afterSnapshot?: Promise<void> }>();
private _context: BrowserContext;
private _resourcesDir: string;
private _recording: RecordingState | undefined;
private _state: RecordingState | undefined;
private _isStopping = false;
private _tracesDir: string;
private _allResources = new Set<string>();
@ -83,55 +84,53 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
};
}
async start(options: TracerOptions): Promise<void> {
start(options: TracerOptions) {
if (this._isStopping)
throw new Error('Cannot start tracing while stopping');
// context + page must be the first events added, this method can't have awaits before them.
const state = this._recording;
if (!state) {
// TODO: passing the same name for two contexts makes them write into a single file
// and conflict.
const traceName = options.name || createGuid();
const traceFile = path.join(this._tracesDir, traceName + '.trace');
const networkFile = path.join(this._tracesDir, traceName + '.network');
this._recording = { options, traceName, traceFile, networkFile, lastReset: 0, sha1s: new Set() };
this._writeChain = mkdirIfNeeded(traceFile).then(() => fs.promises.writeFile(networkFile, ''));
this._appendTraceEvent(this._contextCreatedEvent);
if (this._state) {
const o = this._state.options;
if (o.name !== options.name || !o.screenshots !== !options.screenshots || !o.snapshots !== !options.snapshots)
throw new Error('Tracing has been already started with different options');
return;
}
if (!state?.options?.screenshots && options.screenshots)
this._startScreencast();
else if (state?.options?.screenshots && !options.screenshots)
this._stopScreencast();
// TODO: passing the same name for two contexts makes them write into a single file
// and conflict.
const traceName = options.name || createGuid();
const traceFile = path.join(this._tracesDir, traceName + '.trace');
const networkFile = path.join(this._tracesDir, traceName + '.network');
this._state = { options, traceName, traceFile, networkFile, filesCount: 0, sha1s: new Set(), recording: false };
// context + page must be the first events added, no awaits above this line.
await fs.promises.mkdir(this._resourcesDir, { recursive: true });
this._writeChain = fs.promises.mkdir(this._resourcesDir, { recursive: true }).then(() => fs.promises.writeFile(networkFile, ''));
if (options.snapshots)
this._harTracer.start();
}
if (!state)
this._context.instrumentation.addListener(this);
async startChunk() {
if (this._state && this._state.recording)
await this.stopChunk(false);
await this._appendTraceOperation(async () => {
if (options.snapshots && state?.options?.snapshots) {
// Reset snapshots to avoid back-references.
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 (!this._state)
throw new Error('Must start tracing before starting a new chunk');
if (this._isStopping)
throw new Error('Cannot start a trace chunk while stopping');
if (state) {
state.lastReset++;
state.traceFile = path.join(this._tracesDir, `${state.traceName}-${state.lastReset}.trace`);
await fs.promises.appendFile(state.traceFile, JSON.stringify(this._contextCreatedEvent) + '\n');
}
const state = this._state;
const suffix = state.filesCount ? `-${state.filesCount}` : ``;
state.filesCount++;
state.traceFile = path.join(this._tracesDir, `${state.traceName}${suffix}.trace`);
state.recording = true;
this._appendTraceOperation(async () => {
await mkdirIfNeeded(state.traceFile);
await fs.promises.appendFile(state.traceFile, JSON.stringify(this._contextCreatedEvent) + '\n');
});
if (this._recording)
this._recording.options = options;
this._context.instrumentation.addListener(this);
if (state.options.screenshots)
this._startScreencast();
if (state.options.snapshots)
await this._snapshotter.start();
}
private _startScreencast() {
@ -148,18 +147,16 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
page.setScreencastOptions(null);
}
async stop(): Promise<void> {
if (!this._recording || this._isStopping)
async stop() {
if (!this._state)
return;
this._isStopping = true;
this._context.instrumentation.removeListener(this);
this._stopScreencast();
await this._snapshotter.stop();
await this._harTracer.stop();
// Ensure all writes are finished.
if (this._isStopping)
throw new Error(`Tracing is already stopping`);
if (this._state.recording)
throw new Error(`Must stop trace file before stopping tracing`);
this._harTracer.stop();
await this._writeChain;
this._recording = undefined;
this._isStopping = false;
this._state = undefined;
}
async dispose() {
@ -167,7 +164,11 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
await this._writeChain;
}
async export(): Promise<Artifact> {
async stopChunk(save: boolean): Promise<Artifact | null> {
if (this._isStopping)
throw new Error(`Tracing is already stopping`);
this._isStopping = true;
for (const { sdkObject, metadata, beforeSnapshot, actionSnapshot, afterSnapshot } of this._pendingCalls.values()) {
await Promise.all([beforeSnapshot, actionSnapshot, afterSnapshot]);
let callMetadata = metadata;
@ -181,32 +182,50 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
await this.onAfterCall(sdkObject, callMetadata);
}
if (!this._recording)
throw new Error('Must start tracing before exporting');
if (!this._state || !this._state.recording) {
this._isStopping = false;
if (save)
throw new Error(`Must start tracing before stopping`);
return null;
}
const state = this._state!;
this._context.instrumentation.removeListener(this);
if (state.options.screenshots)
this._stopScreencast();
if (state.options.snapshots)
await this._snapshotter.stop();
// Chain the export operation against write operations,
// so that neither trace files nor sha1s change during the export.
return await this._appendTraceOperation(async () => {
const state = this._recording!;
const zipFile = new yazl.ZipFile();
const failedPromise = new Promise<Artifact>((_, reject) => (zipFile as any as EventEmitter).on('error', reject));
const succeededPromise = new Promise<Artifact>(fulfill => {
zipFile.addFile(state.traceFile, 'trace.trace');
zipFile.addFile(state.networkFile, 'trace.network');
const zipFileName = state.traceFile + '.zip';
for (const sha1 of state.sha1s)
zipFile.addFile(path.join(this._resourcesDir, sha1), path.join('resources', sha1));
zipFile.end();
zipFile.outputStream.pipe(fs.createWriteStream(zipFileName)).on('close', () => {
const artifact = new Artifact(this._context, zipFileName);
artifact.reportFinished();
fulfill(artifact);
});
const result = save ? this._export(state) : Promise.resolve(null);
return result.finally(async () => {
this._isStopping = false;
state.recording = false;
});
return Promise.race([failedPromise, succeededPromise]);
});
}
private async _export(state: RecordingState): Promise<Artifact> {
const zipFile = new yazl.ZipFile();
const failedPromise = new Promise<Artifact>((_, reject) => (zipFile as any as EventEmitter).on('error', reject));
const succeededPromise = new Promise<Artifact>(fulfill => {
zipFile.addFile(state.traceFile, 'trace.trace');
zipFile.addFile(state.networkFile, 'trace.network');
const zipFileName = state.traceFile + '.zip';
for (const sha1 of state.sha1s)
zipFile.addFile(path.join(this._resourcesDir, sha1), path.join('resources', sha1));
zipFile.end();
zipFile.outputStream.pipe(fs.createWriteStream(zipFileName)).on('close', () => {
const artifact = new Artifact(this._context, zipFileName);
artifact.reportFinished();
fulfill(artifact);
});
});
return Promise.race([failedPromise, succeededPromise]);
}
async _captureSnapshot(name: 'before' | 'after' | 'action' | 'event', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle) {
if (!sdkObject.attribution.page)
return;
@ -259,8 +278,8 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
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');
visitSha1s(event, this._state!.sha1s);
await fs.promises.appendFile(this._state!.networkFile, JSON.stringify(event) + '\n');
});
}
@ -301,8 +320,8 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha
private _appendTraceEvent(event: trace.TraceEvent) {
this._appendTraceOperation(async () => {
visitSha1s(event, this._recording!.sha1s);
await fs.promises.appendFile(this._recording!.traceFile, JSON.stringify(event) + '\n');
visitSha1s(event, this._state!.sha1s);
await fs.promises.appendFile(this._state!.traceFile, JSON.stringify(event) + '\n');
});
}

View file

@ -193,10 +193,15 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
const onDidCreateContext = async (context: BrowserContext) => {
context.setDefaultTimeout(actionTimeout || 0);
context.setDefaultNavigationTimeout(navigationTimeout || actionTimeout || 0);
if (captureTrace)
await context.tracing.start({ screenshots: true, snapshots: true });
else
if (captureTrace) {
if (!(context.tracing as any)[kTracingStarted]) {
await context.tracing.start({ screenshots: true, snapshots: true });
(context.tracing as any)[kTracingStarted] = true;
}
await context.tracing.startChunk();
} else {
await context.tracing.stop();
}
(context as any)._csi = {
onApiCall: (stackTrace: ParsedStackTrace) => {
if ((testInfo as TestInfoImpl)._currentSteps().some(step => step.category === 'pw:api' || step.category === 'expect'))
@ -212,7 +217,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
// after the test finishes.
const tracePath = path.join(_artifactsDir(), createGuid() + '.zip');
temporaryTraceFiles.push(tracePath);
await (context.tracing as any)._export({ path: tracePath });
await context.tracing.stopChunk({ path: tracePath });
}
if (screenshot === 'on' || screenshot === 'only-on-failure') {
// Capture screenshot for now. We'll know whether we have to preserve them
@ -267,7 +272,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
// 5. Collect artifacts from any non-closed contexts.
await Promise.all(leftoverContexts.map(async context => {
if (preserveTrace)
await (context.tracing as any)._export({ path: addTraceAttachment() });
await context.tracing.stopChunk({ path: addTraceAttachment() });
if (captureScreenshots)
await Promise.all(context.pages().map(page => page.screenshot({ timeout: 5000, path: addScreenshotAttachment() }).catch(() => {})));
}));
@ -374,3 +379,5 @@ type ParsedStackTrace = {
frameTexts: string[];
apiName: string;
};
const kTracingStarted = Symbol('kTracingStarted');

View file

@ -38,7 +38,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{
const readResources = async () => {
if (resource.request.postData) {
if (resource.request.postData._sha1) {
const response = await fetch(`/sha1/${resource.request.postData}`);
const response = await fetch(`/sha1/${resource.request.postData._sha1}`);
const requestResource = await response.text();
setRequestBody(requestResource);
} else {

View file

@ -301,8 +301,8 @@ test('should stop tracing with trace: on-first-retry, when not retrying', async
});
test('no tracing', async ({}, testInfo) => {
const error = await page.context().tracing._export({ path: testInfo.outputPath('none.zip') }).catch(e => e);
expect(error.message).toContain('Must start tracing before exporting');
const e = await page.context().tracing.stop({ path: 'ignored' }).catch(e => e);
expect(e.message).toContain('Must start tracing before stopping');
});
});
`,
@ -314,7 +314,6 @@ test('should stop tracing with trace: on-first-retry, when not retrying', async
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
'a-shared-flaky-retry1',
' trace.zip',
'a-shared-no-tracing', // Empty dir created because of testInfo.outputPath() call.
'report.json',
]);
});

View file

@ -198,38 +198,34 @@ test('should include interrupted actions', async ({ context, page, server }, tes
expect(clickEvent.metadata.error.error.message).toBe('Action was interrupted');
});
test('should reset to different options', async ({ context, page, server }, testInfo) => {
test('should throw when starting with different options', async ({ context }) => {
await context.tracing.start({ screenshots: true, snapshots: true });
await page.goto(server.PREFIX + '/frames/frame.html');
await context.tracing.start({ screenshots: false, snapshots: false });
await page.setContent('<button>Click</button>');
await page.click('"Click"');
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
const { events } = await parseTrace(testInfo.outputPath('trace.zip'));
expect(events[0].type).toBe('context-options');
expect(events.find(e => e.metadata?.apiName === 'page.goto')).toBeFalsy();
expect(events.find(e => e.metadata?.apiName === 'page.setContent')).toBeTruthy();
expect(events.find(e => e.metadata?.apiName === 'page.click')).toBeTruthy();
expect(events.some(e => e.type === 'frame-snapshot')).toBeFalsy();
expect(events.some(e => e.type === 'resource-snapshot')).toBeTruthy();
const error = await context.tracing.start({ screenshots: false, snapshots: false }).catch(e => e);
expect(error.message).toContain('Tracing has been already started with different options');
});
test('should reset and export', async ({ context, page, server }, testInfo) => {
test('should throw when stopping without start', async ({ context }, testInfo) => {
const error = await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }).catch(e => e);
expect(error.message).toContain('Must start tracing before stopping');
});
test('should not throw when stopping without start but not exporting', async ({ context }, testInfo) => {
await context.tracing.stop();
});
test('should work with multiple chunks', async ({ context, page, server }, testInfo) => {
await context.tracing.start({ screenshots: true, snapshots: true });
await page.goto(server.PREFIX + '/frames/frame.html');
await context.tracing.start({ screenshots: true, snapshots: true });
await context.tracing.startChunk();
await page.setContent('<button>Click</button>');
await page.click('"Click"');
page.click('"ClickNoButton"').catch(() => {});
// @ts-expect-error
await context.tracing._export({ path: testInfo.outputPath('trace.zip') });
await context.tracing.stopChunk({ path: testInfo.outputPath('trace.zip') });
await context.tracing.start({ screenshots: true, snapshots: true });
await context.tracing.startChunk();
await page.hover('"Click"');
await context.tracing.stop({ path: testInfo.outputPath('trace2.zip') });
await context.tracing.stopChunk({ path: testInfo.outputPath('trace2.zip') });
const trace1 = await parseTrace(testInfo.outputPath('trace.zip'));
expect(trace1.events[0].type).toBe('context-options');
@ -248,6 +244,7 @@ test('should reset and export', async ({ context, page, server }, testInfo) => {
expect(trace2.events.find(e => e.metadata?.apiName === 'page.click')).toBeFalsy();
expect(trace2.events.find(e => e.metadata?.apiName === 'page.hover')).toBeTruthy();
expect(trace2.events.some(e => e.type === 'frame-snapshot')).toBeTruthy();
expect(trace2.events.some(e => e.type === 'resource-snapshot' && e.snapshot.request.url.endsWith('style.css'))).toBeTruthy();
});
test('should export trace concurrently to second navigation', async ({ context, page, server }, testInfo) => {

47
types/types.d.ts vendored
View file

@ -13518,10 +13518,10 @@ export interface Touchscreen {
}
/**
* API for collecting and saving Playwright traces. Playwright traces can be opened using the Playwright CLI after
* Playwright script runs.
* API for collecting and saving Playwright traces. Playwright traces can be opened in [Trace Viewer](https://playwright.dev/docs/trace-viewer)
* after Playwright script runs.
*
* Start with specifying the folder traces will be stored in:
* Start recording a trace before performing actions. At the end, stop tracing and save it to a file.
*
* ```js
* const browser = await chromium.launch();
@ -13564,13 +13564,52 @@ export interface Tracing {
snapshots?: boolean;
}): Promise<void>;
/**
* Start a new trace chunk. If you'd like to record multiple traces on the same [BrowserContext], use
* [tracing.start([options])](https://playwright.dev/docs/api/class-tracing#tracing-start) once, and then create multiple
* trace chunks with [tracing.startChunk()](https://playwright.dev/docs/api/class-tracing#tracing-start-chunk) and
* [tracing.stopChunk([options])](https://playwright.dev/docs/api/class-tracing#tracing-stop-chunk).
*
* ```js
* await context.tracing.start({ screenshots: true, snapshots: true });
* const page = await context.newPage();
* await page.goto('https://playwright.dev');
*
* await context.tracing.startChunk();
* await page.click('text=Get Started');
* // Everything between startChunk and stopChunk will be recorded in the trace.
* await context.tracing.stopChunk({ path: 'trace1.zip' });
*
* await context.tracing.startChunk();
* await page.goto('http://example.com');
* // Save a second trace file with different actions.
* await context.tracing.stopChunk({ path: 'trace2.zip' });
* ```
*
*/
startChunk(): Promise<void>;
/**
* Stop tracing.
* @param options
*/
stop(options?: {
/**
* Export trace into the file with the given name.
* Export trace into the file with the given path.
*/
path?: string;
}): Promise<void>;
/**
* Stop the trace chunk. See [tracing.startChunk()](https://playwright.dev/docs/api/class-tracing#tracing-start-chunk) for
* more details about multiple trace chunks.
* @param options
*/
stopChunk(options?: {
/**
* Export trace collected since the last
* [tracing.startChunk()](https://playwright.dev/docs/api/class-tracing#tracing-start-chunk) call into the file with the
* given path.
*/
path?: string;
}): Promise<void>;