From ecd0f927f4f0efb396aec1a839b4252b229f8ae0 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 16 Mar 2023 18:17:07 -0700 Subject: [PATCH] chore: make stacks rendering live in ui mode (#21728) Co-authored-by: Max Schmitt --- .../playwright-core/src/client/connection.ts | 18 ++--- .../playwright-core/src/client/tracing.ts | 48 +++++++----- .../playwright-core/src/protocol/validator.ts | 21 +++++- .../dispatchers/localUtilsDispatcher.ts | 73 ++++++++++++++++--- .../server/dispatchers/tracingDispatcher.ts | 2 +- .../src/server/trace/recorder/tracing.ts | 16 ++-- packages/playwright-test/src/index.ts | 2 +- packages/playwright-test/src/runner/uiMode.ts | 1 + packages/protocol/src/channels.ts | 35 ++++++++- packages/protocol/src/protocol.yml | 21 +++++- .../playwright-test/playwright.trace.spec.ts | 2 + 11 files changed, 186 insertions(+), 53 deletions(-) diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index 45a6c34e5c..4894c68e32 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -71,7 +71,7 @@ export class Connection extends EventEmitter { private _localUtils?: LocalUtils; // Some connections allow resolving in-process dispatchers. toImpl: ((client: ChannelOwner) => any) | undefined; - private _stackCollectors = new Set(); + private _tracingCount = 0; constructor(localUtils?: LocalUtils) { super(); @@ -103,12 +103,11 @@ export class Connection extends EventEmitter { return this._objects.get(guid)!; } - startCollectingCallMetadata(collector: channels.ClientSideCallMetadata[]) { - this._stackCollectors.add(collector); - } - - stopCollectingCallMetadata(collector: channels.ClientSideCallMetadata[]) { - this._stackCollectors.delete(collector); + async setIsTracing(isTracing: boolean) { + if (isTracing) + this._tracingCount++; + else + this._tracingCount--; } async sendMessageToServer(object: ChannelOwner, type: string, method: string, params: any, stackTrace: ParsedStackTrace | null, wallTime: number | undefined): Promise { @@ -121,12 +120,11 @@ export class Connection extends EventEmitter { const converted = { id, guid, method, params }; // Do not include metadata in debug logs to avoid noise. debugLogger.log('channel:command', converted); - for (const collector of this._stackCollectors) - collector.push({ stack: frames, id: id }); const location = frames[0] ? { file: frames[0].file, line: frames[0].line, column: frames[0].column } : undefined; const metadata: channels.Metadata = { wallTime, apiName, location, internal: !apiName }; this.onmessage({ ...converted, metadata }); - + if (this._tracingCount && frames && type !== 'LocalUtils') + this._localUtils?._channel.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {}); return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, stackTrace, type, method })); } diff --git a/packages/playwright-core/src/client/tracing.ts b/packages/playwright-core/src/client/tracing.ts index 134e526715..b31a94ab72 100644 --- a/packages/playwright-core/src/client/tracing.ts +++ b/packages/playwright-core/src/client/tracing.ts @@ -21,8 +21,9 @@ import { ChannelOwner } from './channelOwner'; export class Tracing extends ChannelOwner implements api.Tracing { private _includeSources = false; - private _metadataCollector: channels.ClientSideCallMetadata[] = []; _tracesDir: string | undefined; + private _stacksId: string | undefined; + private _isTracing = false; static from(channel: channels.TracingChannel): Tracing { return (channel as any)._object; @@ -34,18 +35,26 @@ export class Tracing extends ChannelOwner implements ap async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean } = {}) { this._includeSources = !!options.sources; - await this._wrapApiCall(async () => { + const traceName = await this._wrapApiCall(async () => { await this._channel.tracingStart(options); - await this._channel.tracingStartChunk({ name: options.name, title: options.title }); + const response = await this._channel.tracingStartChunk({ name: options.name, title: options.title }); + return response.traceName; }); - this._metadataCollector = []; - this._connection.startCollectingCallMetadata(this._metadataCollector); + await this._startCollectingStacks(traceName); } async startChunk(options: { name?: string, title?: string } = {}) { - await this._channel.tracingStartChunk(options); - this._metadataCollector = []; - this._connection.startCollectingCallMetadata(this._metadataCollector); + const { traceName } = await this._channel.tracingStartChunk(options); + await this._startCollectingStacks(traceName); + } + + private async _startCollectingStacks(traceName: string) { + if (!this._isTracing) { + this._isTracing = true; + this._connection.setIsTracing(true); + } + const result = await this._connection.localUtils()._channel.tracingStarted({ tracesDir: this._tracesDir, traceName }); + this._stacksId = result.stacksId; } async stopChunk(options: { path?: string } = {}) { @@ -60,12 +69,16 @@ export class Tracing extends ChannelOwner implements ap } private async _doStopChunk(filePath: string | undefined) { - this._connection.stopCollectingCallMetadata(this._metadataCollector); - const metadata = this._metadataCollector; - this._metadataCollector = []; + if (this._isTracing) { + this._isTracing = false; + this._connection.setIsTracing(false); + } + if (!filePath) { - await this._channel.tracingStopChunk({ mode: 'discard' }); // Not interested in artifacts. + await this._channel.tracingStopChunk({ mode: 'discard' }); + if (this._stacksId) + await this._connection.localUtils()._channel.traceDiscarded({ stacksId: this._stacksId }); return; } @@ -73,23 +86,24 @@ export class Tracing extends ChannelOwner implements ap if (isLocal) { const result = await this._channel.tracingStopChunk({ mode: 'entries' }); - await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: result.entries!, metadata, mode: 'write', includeSources: this._includeSources }); + await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: result.entries!, mode: 'write', stacksId: this._stacksId, includeSources: this._includeSources }); return; } const result = await this._channel.tracingStopChunk({ mode: 'archive' }); // The artifact may be missing if the browser closed while stopping tracing. - if (!result.artifact) + if (!result.artifact) { + if (this._stacksId) + await this._connection.localUtils()._channel.traceDiscarded({ stacksId: this._stacksId }); return; + } // Save trace to the final local file. const artifact = Artifact.from(result.artifact); await artifact.saveAs(filePath); await artifact.delete(); - // Add local sources to the remote trace if necessary. - if (metadata.length) - await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: [], metadata, mode: 'append', includeSources: this._includeSources }); + await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: [], mode: 'append', stacksId: this._stacksId, includeSources: this._includeSources }); } } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 0bac050bfa..272ffc3635 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -220,8 +220,8 @@ scheme.LocalUtilsInitializer = tOptional(tObject({})); scheme.LocalUtilsZipParams = tObject({ zipFile: tString, entries: tArray(tType('NameValue')), + stacksId: tOptional(tString), mode: tEnum(['write', 'append']), - metadata: tArray(tType('ClientSideCallMetadata')), includeSources: tBoolean, }); scheme.LocalUtilsZipResult = tOptional(tObject({})); @@ -268,6 +268,21 @@ scheme.LocalUtilsConnectParams = tObject({ scheme.LocalUtilsConnectResult = tObject({ pipe: tChannel(['JsonPipe']), }); +scheme.LocalUtilsTracingStartedParams = tObject({ + tracesDir: tOptional(tString), + traceName: tString, +}); +scheme.LocalUtilsTracingStartedResult = tObject({ + stacksId: tString, +}); +scheme.LocalUtilsAddStackToTracingNoReplyParams = tObject({ + callData: tType('ClientSideCallMetadata'), +}); +scheme.LocalUtilsAddStackToTracingNoReplyResult = tOptional(tObject({})); +scheme.LocalUtilsTraceDiscardedParams = tObject({ + stacksId: tString, +}); +scheme.LocalUtilsTraceDiscardedResult = tOptional(tObject({})); scheme.RootInitializer = tOptional(tObject({})); scheme.RootInitializeParams = tObject({ sdkLanguage: tEnum(['javascript', 'python', 'java', 'csharp']), @@ -2095,7 +2110,9 @@ scheme.TracingTracingStartChunkParams = tObject({ name: tOptional(tString), title: tOptional(tString), }); -scheme.TracingTracingStartChunkResult = tOptional(tObject({})); +scheme.TracingTracingStartChunkResult = tObject({ + traceName: tString, +}); scheme.TracingTracingStopChunkParams = tObject({ mode: tEnum(['archive', 'discard', 'entries']), }); diff --git a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts index ea04d54b1d..cbefc5a96a 100644 --- a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts @@ -17,9 +17,10 @@ import type EventEmitter from 'events'; import fs from 'fs'; import path from 'path'; +import os from 'os'; import type * as channels from '@protocol/channels'; import { ManualPromise } from '../../utils/manualPromise'; -import { assert, calculateSha1, createGuid } from '../../utils'; +import { assert, calculateSha1, createGuid, removeFolders } from '../../utils'; import type { RootDispatcher } from './dispatcher'; import { Dispatcher } from './dispatcher'; import { yazl, yauzl } from '../../zipBundle'; @@ -42,7 +43,13 @@ import { serializeClientSideCallMetadata } from '../../utils'; export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel { _type_LocalUtils: boolean; - private _harBakends = new Map(); + private _harBackends = new Map(); + private _stackSessions = new Map, + tmpDir: string | undefined, + callStacks: channels.ClientSideCallMetadata[] + }>(); constructor(scope: RootDispatcher, playwright: Playwright) { const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils'); @@ -67,12 +74,21 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. addFile(entry.value, entry.name); // Add stacks and the sources. - zipFile.addBuffer(Buffer.from(JSON.stringify(serializeClientSideCallMetadata(params.metadata))), 'trace.stacks'); + const stackSession = params.stacksId ? this._stackSessions.get(params.stacksId) : undefined; + if (stackSession?.callStacks.length) { + await stackSession.writer; + if (process.env.PW_LIVE_TRACE_STACKS) { + zipFile.addFile(stackSession.file, 'trace.stacks'); + } else { + const buffer = Buffer.from(JSON.stringify(serializeClientSideCallMetadata(stackSession.callStacks))); + zipFile.addBuffer(buffer, 'trace.stacks'); + } + } // Collect sources from stacks. if (params.includeSources) { const sourceFiles = new Set(); - for (const { stack } of params.metadata) { + for (const { stack } of stackSession?.callStacks || []) { if (!stack) continue; for (const { file } of stack) @@ -88,7 +104,9 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. zipFile.end(undefined, () => { zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile)).on('close', () => promise.resolve()); }); - return promise; + await promise; + await this._deleteStackSession(params.stacksId); + return; } // File already exists. Repack and add new entries. @@ -121,7 +139,8 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. }); }); }); - return promise; + await promise; + await this._deleteStackSession(params.stacksId); } async harOpen(params: channels.LocalUtilsHarOpenParams, metadata: CallMetadata): Promise { @@ -139,21 +158,21 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. const harFile = JSON.parse(await fs.promises.readFile(params.file, 'utf-8')) as har.HARFile; harBackend = new HarBackend(harFile, path.dirname(params.file), null); } - this._harBakends.set(harBackend.id, harBackend); + this._harBackends.set(harBackend.id, harBackend); return { harId: harBackend.id }; } async harLookup(params: channels.LocalUtilsHarLookupParams, metadata: CallMetadata): Promise { - const harBackend = this._harBakends.get(params.harId); + const harBackend = this._harBackends.get(params.harId); if (!harBackend) return { action: 'error', message: `Internal error: har was not opened` }; return await harBackend.lookup(params.url, params.method, params.headers, params.postData, params.isNavigationRequest); } async harClose(params: channels.LocalUtilsHarCloseParams, metadata: CallMetadata): Promise { - const harBackend = this._harBakends.get(params.harId); + const harBackend = this._harBackends.get(params.harId); if (harBackend) { - this._harBakends.delete(harBackend.id); + this._harBackends.delete(harBackend.id); harBackend.dispose(); } } @@ -213,6 +232,40 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. }, params.timeout || 0); } + async tracingStarted(params: channels.LocalUtilsTracingStartedParams, metadata?: CallMetadata | undefined): Promise { + let tmpDir = undefined; + if (!params.tracesDir) + tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-tracing-')); + const traceStacksFile = path.join(params.tracesDir || tmpDir!, params.traceName + '.stacks'); + this._stackSessions.set(traceStacksFile, { callStacks: [], file: traceStacksFile, writer: Promise.resolve(), tmpDir }); + return { stacksId: traceStacksFile }; + } + + async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams, metadata?: CallMetadata | undefined): Promise { + await this._deleteStackSession(params.stacksId); + } + + async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata | undefined): Promise { + for (const session of this._stackSessions.values()) { + session.callStacks.push(params.callData); + if (process.env.PW_LIVE_TRACE_STACKS) { + session.writer = session.writer.then(() => { + const buffer = Buffer.from(JSON.stringify(serializeClientSideCallMetadata(session.callStacks))); + return fs.promises.writeFile(session.file, buffer); + }); + } + } + } + + private async _deleteStackSession(stacksId?: string) { + const session = stacksId ? this._stackSessions.get(stacksId) : undefined; + if (!session) + return; + await session.writer; + if (session.tmpDir) + await removeFolders([session.tmpDir]); + this._stackSessions.delete(stacksId!); + } } const redirectStatus = [301, 302, 303, 307, 308]; diff --git a/packages/playwright-core/src/server/dispatchers/tracingDispatcher.ts b/packages/playwright-core/src/server/dispatchers/tracingDispatcher.ts index f6c52cefb7..b8214fbe31 100644 --- a/packages/playwright-core/src/server/dispatchers/tracingDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/tracingDispatcher.ts @@ -38,7 +38,7 @@ export class TracingDispatcher extends Dispatcher { - await this._object.startChunk(params); + return await this._object.startChunk(params); } async tracingStopChunk(params: channels.TracingTracingStopChunkParams): Promise { diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 37fe87836f..1ee1f774a8 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -58,7 +58,7 @@ type RecordingState = { traceFile: string, tracesDir: string, resourcesDir: string, - filesCount: number, + chunkOrdinal: number, networkSha1s: Set, traceSha1s: Set, recording: boolean; @@ -132,7 +132,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps // and conflict. const traceName = options.name || createGuid(); // Init the state synchronously. - this._state = { options, traceName, traceFile: '', networkFile: '', tracesDir: '', resourcesDir: '', filesCount: 0, traceSha1s: new Set(), networkSha1s: new Set(), recording: false }; + this._state = { options, traceName, traceFile: '', networkFile: '', tracesDir: '', resourcesDir: '', chunkOrdinal: 0, traceSha1s: new Set(), networkSha1s: new Set(), recording: false }; const state = this._state; state.tracesDir = await this._createTracesDirIfNeeded(); @@ -144,7 +144,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this._harTracer.start(); } - async startChunk(options: { name?: string, title?: string } = {}) { + async startChunk(options: { name?: string, title?: string } = {}): Promise<{ traceName: string }> { if (this._state && this._state.recording) await this.stopChunk({ mode: 'discard' }); @@ -154,13 +154,14 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps throw new Error('Cannot start a trace chunk while stopping'); const state = this._state; - const suffix = state.filesCount ? `-${state.filesCount}` : ``; - state.filesCount++; + const suffix = state.chunkOrdinal ? `-${state.chunkOrdinal}` : ``; + state.chunkOrdinal++; 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'); @@ -171,6 +172,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this._startScreencast(); if (state.options.snapshots) await this._snapshotter?.start(); + return { traceName: state.traceName }; } private _startScreencast() { @@ -194,6 +196,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps private async _changeTraceName(state: RecordingState, name: string) { await this._appendTraceOperation(async () => { const oldNetworkFile = state.networkFile; + state.traceName = name; 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. @@ -258,7 +261,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps return {}; // Network file survives across chunks, make a snapshot before returning the resulting entries. - const networkFile = path.join(state.networkFile, '..', createGuid()); + const suffix = state.chunkOrdinal ? `-${state.chunkOrdinal}` : ``; + const networkFile = path.join(state.tracesDir, state.traceName + `${suffix}.network`); await fs.promises.copyFile(state.networkFile, networkFile); const entries: NameValue[] = []; diff --git a/packages/playwright-test/src/index.ts b/packages/playwright-test/src/index.ts index 7ad2e389d6..f85df8a9a8 100644 --- a/packages/playwright-test/src/index.ts +++ b/packages/playwright-test/src/index.ts @@ -18,7 +18,7 @@ import * as fs from 'fs'; import * as path from 'path'; import type { APIRequestContext, BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core'; import * as playwrightLibrary from 'playwright-core'; -import { createGuid, debugMode, removeFolders, addInternalStackPrefix, mergeTraceFiles, saveTraceFile } from 'playwright-core/lib/utils'; +import { createGuid, debugMode, addInternalStackPrefix, mergeTraceFiles, saveTraceFile, removeFolders } from 'playwright-core/lib/utils'; import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, TraceMode, VideoMode } from '../types/test'; import type { TestInfoImpl } from './worker/testInfo'; import { rootTestType } from './common/testType'; diff --git a/packages/playwright-test/src/runner/uiMode.ts b/packages/playwright-test/src/runner/uiMode.ts index bfd7f79b87..c5492ab10a 100644 --- a/packages/playwright-test/src/runner/uiMode.ts +++ b/packages/playwright-test/src/runner/uiMode.ts @@ -39,6 +39,7 @@ class UIMode { constructor(config: FullConfigInternal) { this._config = config; + process.env.PW_LIVE_TRACE_STACKS = '1'; config._internal.configCLIOverrides.forbidOnly = false; config._internal.configCLIOverrides.globalTimeout = 0; config._internal.configCLIOverrides.repeatEach = 0; diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 73c382273a..a1ecdbb591 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -402,16 +402,19 @@ export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel { harClose(params: LocalUtilsHarCloseParams, metadata?: CallMetadata): Promise; harUnzip(params: LocalUtilsHarUnzipParams, metadata?: CallMetadata): Promise; connect(params: LocalUtilsConnectParams, metadata?: CallMetadata): Promise; + tracingStarted(params: LocalUtilsTracingStartedParams, metadata?: CallMetadata): Promise; + addStackToTracingNoReply(params: LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata): Promise; + traceDiscarded(params: LocalUtilsTraceDiscardedParams, metadata?: CallMetadata): Promise; } export type LocalUtilsZipParams = { zipFile: string, entries: NameValue[], + stacksId?: string, mode: 'write' | 'append', - metadata: ClientSideCallMetadata[], includeSources: boolean, }; export type LocalUtilsZipOptions = { - + stacksId?: string, }; export type LocalUtilsZipResult = void; export type LocalUtilsHarOpenParams = { @@ -476,6 +479,30 @@ export type LocalUtilsConnectOptions = { export type LocalUtilsConnectResult = { pipe: JsonPipeChannel, }; +export type LocalUtilsTracingStartedParams = { + tracesDir?: string, + traceName: string, +}; +export type LocalUtilsTracingStartedOptions = { + tracesDir?: string, +}; +export type LocalUtilsTracingStartedResult = { + stacksId: string, +}; +export type LocalUtilsAddStackToTracingNoReplyParams = { + callData: ClientSideCallMetadata, +}; +export type LocalUtilsAddStackToTracingNoReplyOptions = { + +}; +export type LocalUtilsAddStackToTracingNoReplyResult = void; +export type LocalUtilsTraceDiscardedParams = { + stacksId: string, +}; +export type LocalUtilsTraceDiscardedOptions = { + +}; +export type LocalUtilsTraceDiscardedResult = void; export interface LocalUtilsEvents { } @@ -3756,7 +3783,9 @@ export type TracingTracingStartChunkOptions = { name?: string, title?: string, }; -export type TracingTracingStartChunkResult = void; +export type TracingTracingStartChunkResult = { + traceName: string, +}; export type TracingTracingStopChunkParams = { mode: 'archive' | 'discard' | 'entries', }; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 65e338b1b4..2707baf994 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -500,14 +500,12 @@ LocalUtils: entries: type: array items: NameValue + stacksId: string? mode: type: enum literals: - write - append - metadata: - type: array - items: ClientSideCallMetadata includeSources: boolean harOpen: @@ -563,6 +561,21 @@ LocalUtils: returns: pipe: JsonPipe + tracingStarted: + parameters: + tracesDir: string? + traceName: string + returns: + stacksId: string + + addStackToTracingNoReply: + parameters: + callData: ClientSideCallMetadata + + traceDiscarded: + parameters: + stacksId: string + Root: type: interface @@ -2927,6 +2940,8 @@ Tracing: parameters: name: string? title: string? + returns: + traceName: string tracingStopChunk: parameters: diff --git a/tests/playwright-test/playwright.trace.spec.ts b/tests/playwright-test/playwright.trace.spec.ts index b8c397b678..a6adee127c 100644 --- a/tests/playwright-test/playwright.trace.spec.ts +++ b/tests/playwright-test/playwright.trace.spec.ts @@ -18,6 +18,8 @@ import { test, expect } from './playwright-test-fixtures'; import { parseTrace } from '../config/utils'; import fs from 'fs'; +test.describe.configure({ mode: 'parallel' }); + test('should stop tracing with trace: on-first-retry, when not retrying', async ({ runInlineTest }, testInfo) => { const result = await runInlineTest({ 'playwright.config.ts': `