diff --git a/docs/src/api/class-tracing.md b/docs/src/api/class-tracing.md index 49b0e02215..9f96e4179a 100644 --- a/docs/src/api/class-tracing.md +++ b/docs/src/api/class-tracing.md @@ -267,6 +267,13 @@ await context.Tracing.StopChunkAsync(new() Trace name to be shown in the Trace Viewer. +### option: Tracing.startChunk.name +* since: v1.32 +- `name` <[string]> + +If specified, the trace is going to be saved into the file with the +given name inside the [`option: tracesDir`] folder specified in [`method: BrowserType.launch`]. + ## async method: Tracing.stop * since: v1.12 diff --git a/packages/playwright-core/src/client/tracing.ts b/packages/playwright-core/src/client/tracing.ts index 231c5e8d3c..0a93708036 100644 --- a/packages/playwright-core/src/client/tracing.ts +++ b/packages/playwright-core/src/client/tracing.ts @@ -34,13 +34,13 @@ export class Tracing extends ChannelOwner implements ap this._includeSources = !!options.sources; await this._wrapApiCall(async () => { await this._channel.tracingStart(options); - await this._channel.tracingStartChunk({ title: options.title }); + await this._channel.tracingStartChunk({ name: options.name, title: options.title }); }); this._metadataCollector = []; this._connection.startCollectingCallMetadata(this._metadataCollector); } - async startChunk(options: { title?: string } = {}) { + async startChunk(options: { name?: string, title?: string } = {}) { await this._channel.tracingStartChunk(options); this._metadataCollector = []; this._connection.startCollectingCallMetadata(this._metadataCollector); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 93076afb9d..0bac050bfa 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -2092,6 +2092,7 @@ scheme.TracingTracingStartParams = tObject({ }); scheme.TracingTracingStartResult = tOptional(tObject({})); scheme.TracingTracingStartChunkParams = tObject({ + name: tOptional(tString), title: tOptional(tString), }); scheme.TracingTracingStartChunkResult = tOptional(tObject({})); diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index a1c9f27afb..8ba59907db 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -125,6 +125,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps const o = this._state.options; if (!o.screenshots !== !options.screenshots || !o.snapshots !== !options.snapshots) throw new Error('Tracing has been already started with different options'); + if (options.name && options.name !== this._state.traceName) + await this._changeTraceName(this._state, options.name); return; } // TODO: passing the same name for two contexts makes them write into a single file @@ -143,7 +145,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this._harTracer.start(); } - async startChunk(options: { title?: string } = {}) { + async startChunk(options: { name?: string, title?: string } = {}) { if (this._state && this._state.recording) await this.stopChunk({ mode: 'discard' }); @@ -158,6 +160,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps state.traceFile = path.join(state.tracesDir, `${state.traceName}${suffix}.trace`); state.recording = true; + if (options.name && options.name !== this._state.traceName) + this._changeTraceName(this._state, options.name); this._appendTraceOperation(async () => { await mkdirIfNeeded(state.traceFile); await fs.promises.appendFile(state.traceFile, JSON.stringify({ ...this._contextCreatedEvent, title: options.title, wallTime: Date.now() }) + '\n'); @@ -188,6 +192,16 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps page.setScreencastOptions(null); } + private async _changeTraceName(state: RecordingState, name: string) { + await this._appendTraceOperation(async () => { + const oldNetworkFile = state.networkFile; + state.traceFile = path.join(state.tracesDir, name + '.trace'); + state.networkFile = path.join(state.tracesDir, name + '.network'); + // Network file survives across chunks, so make a copy with the new name. + await fs.promises.copyFile(oldNetworkFile, state.networkFile); + }); + } + async stop() { if (!this._state) return; @@ -257,7 +271,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps if (params.mode === 'discard') return {}; - // Har files are live, make a snapshot before returning the resulting entries. + // Network file survives across chunks, make a snapshot before returning the resulting entries. const networkFile = path.join(state.networkFile, '..', createGuid()); await fs.promises.copyFile(state.networkFile, networkFile); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 1a4ff0cd09..de0953a54a 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -18262,6 +18262,13 @@ export interface Tracing { * @param options */ startChunk(options?: { + /** + * If specified, the trace is going to be saved into the file with the given name inside the `tracesDir` folder + * specified in + * [browserType.launch([options])](https://playwright.dev/docs/api/class-browsertype#browser-type-launch). + */ + name?: string; + /** * Trace name to be shown in the Trace Viewer. */ diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 0bfd8b6941..73c382273a 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -3749,9 +3749,11 @@ export type TracingTracingStartOptions = { }; export type TracingTracingStartResult = void; export type TracingTracingStartChunkParams = { + name?: string, title?: string, }; export type TracingTracingStartChunkOptions = { + name?: string, title?: string, }; export type TracingTracingStartChunkResult = void; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index a5acefd06b..65e338b1b4 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2925,6 +2925,7 @@ Tracing: tracingStartChunk: parameters: + name: string? title: string? tracingStopChunk: diff --git a/tests/library/tracing.spec.ts b/tests/library/tracing.spec.ts index 0593e3ecca..26fc70ff01 100644 --- a/tests/library/tracing.spec.ts +++ b/tests/library/tracing.spec.ts @@ -180,6 +180,58 @@ test('should collect two traces', async ({ context, page, server }, testInfo) => } }); +test('should respect tracesDir and name', async ({ browserType, server }, testInfo) => { + const tracesDir = testInfo.outputPath('traces'); + const browser = await browserType.launch({ tracesDir }); + const context = await browser.newContext(); + const page = await context.newPage(); + + await context.tracing.start({ name: 'name1', snapshots: true }); + await page.goto(server.PREFIX + '/one-style.html'); + await context.tracing.stopChunk({ path: testInfo.outputPath('trace1.zip') }); + expect(fs.existsSync(path.join(tracesDir, 'name1.trace'))).toBe(true); + expect(fs.existsSync(path.join(tracesDir, 'name1.network'))).toBe(true); + + await context.tracing.startChunk({ name: 'name2' }); + await page.goto(server.PREFIX + '/har.html'); + await context.tracing.stop({ path: testInfo.outputPath('trace2.zip') }); + expect(fs.existsSync(path.join(tracesDir, 'name2.trace'))).toBe(true); + expect(fs.existsSync(path.join(tracesDir, 'name2.network'))).toBe(true); + + await browser.close(); + + function resourceNames(resources: Map) { + return [...resources.keys()].map(file => { + return file.replace(/^resources\/.*\.(html|css)$/, 'resources/XXX.$1'); + }).sort(); + } + + { + const { resources, actions } = await parseTrace(testInfo.outputPath('trace1.zip')); + expect(actions).toEqual(['page.goto']); + expect(resourceNames(resources)).toEqual([ + 'resources/XXX.css', + 'resources/XXX.html', + 'trace.network', + 'trace.stacks', + 'trace.trace', + ]); + } + + { + const { resources, actions } = await parseTrace(testInfo.outputPath('trace2.zip')); + expect(actions).toEqual(['page.goto']); + expect(resourceNames(resources)).toEqual([ + 'resources/XXX.css', + 'resources/XXX.html', + 'resources/XXX.html', + 'trace.network', + 'trace.stacks', + 'trace.trace', + ]); + } +}); + test('should not include trace resources from the provious chunks', async ({ context, page, server, browserName }, testInfo) => { test.skip(browserName !== 'chromium', 'The number of screenshots is flaky in non-Chromium'); await context.tracing.start({ screenshots: true, snapshots: true, sources: true });