feat(tracing): tracing.{start,stop}Chunk instead of tracing._export (#8521)
This commit is contained in:
parent
bee8ed117b
commit
b8f8ca7493
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -706,11 +706,15 @@ BrowserContext:
|
|||
snapshots: boolean?
|
||||
screenshots: boolean?
|
||||
|
||||
tracingStop:
|
||||
tracingStartChunk:
|
||||
|
||||
tracingExport:
|
||||
tracingStopChunk:
|
||||
parameters:
|
||||
save: boolean
|
||||
returns:
|
||||
artifact: Artifact
|
||||
artifact: Artifact?
|
||||
|
||||
tracingStop:
|
||||
|
||||
harExport:
|
||||
returns:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
47
types/types.d.ts
vendored
|
|
@ -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>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue