diff --git a/docs/src/api/class-tracing.md b/docs/src/api/class-tracing.md index 065896925f..d7e2721936 100644 --- a/docs/src/api/class-tracing.md +++ b/docs/src/api/class-tracing.md @@ -281,6 +281,29 @@ given name prefix inside the [`option: BrowserType.launch.tracesDir`] directory To specify the final trace zip file name, you need to pass `path` option to [`method: Tracing.stopChunk`] instead. +## async method: Tracing.group +* since: v1.49 + +Creates a new inline group in the trace log, causing any subsequent calls to be indented by an additional level, until [`method: Tracing.groupEnd`] is called. + +### param: Tracing.group.name +* since: v1.49 +- `name` <[string]> + +Group name shown in the trace viewer. + +### option: Tracing.group.location +* since: v1.49 +- `location` ?<[Object]> + - `file` <[string]> Source file path to be shown in the trace viewer source tab. + - `line` ?<[int]> Line number in the source file. + - `column` ?<[int]> Column number in the source file + +## async method: Tracing.groupEnd +* since: v1.49 + +Closes the last opened inline group in the trace log. + ## 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 5c7796c103..1911d60d48 100644 --- a/packages/playwright-core/src/client/tracing.ts +++ b/packages/playwright-core/src/client/tracing.ts @@ -18,6 +18,8 @@ import type * as api from '../../types/types'; import type * as channels from '@protocol/channels'; import { Artifact } from './artifact'; import { ChannelOwner } from './channelOwner'; +import { captureRawStack } from '../utils'; +import { filteredStackTrace } from 'playwright/lib/util'; export class Tracing extends ChannelOwner implements api.Tracing { private _includeSources = false; @@ -52,6 +54,10 @@ export class Tracing extends ChannelOwner implements ap } async group(name: string, options: { location?: { file: string, line?: number, column?: number } } = {}) { + if (!options.location) { + const filteredStack = filteredStackTrace(captureRawStack()); + options.location = filteredStack[0]; + } await this._channel.tracingGroup({ name, options }); } diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 6a770ad2fa..484fe6cf32 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -61,6 +61,8 @@ type RecordingState = { traceSha1s: Set, recording: boolean; callIds: Set; + groupStack: string[]; + groupId: number; }; const kScreencastOptions = { width: 800, height: 600, quality: 90 }; @@ -80,8 +82,6 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps private _allResources = new Set(); private _contextCreatedEvent: trace.ContextCreatedTraceEvent; private _pendingHarEntries = new Set(); - private _groupStack: string[] = []; - private _groupId = 0; constructor(context: BrowserContext | APIRequestContext, tracesDir: string | undefined) { super(context, 'tracing'); @@ -150,6 +150,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps networkSha1s: new Set(), recording: false, callIds: new Set(), + groupStack: [], + groupId: 0, }; this._fs.mkdir(this._state.resourcesDir); this._fs.writeFile(this._state.networkFile, ''); @@ -197,6 +199,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps } async group(name: string, options: { location?: { file: string, line?: number, column?: number } } = {}): Promise { + if (!this._state) + return; const stackFrame: StackFrame = { file: options.location?.file || '', line: options.location?.line || 0, @@ -204,7 +208,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps }; const event: trace.BeforeActionTraceEvent = { type: 'before', - callId: `group-${this._groupId++}`, + callId: `group-${this._state.groupId++}`, startTime: monotonicTime(), apiName: name, class: 'Tracing', @@ -212,13 +216,18 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps params: { }, stack: [stackFrame], }; - this._groupStack.push(event.callId); + if (this._state.groupStack.length) + event.parentId = this._state.groupStack[this._state.groupStack.length - 1]; + this._state.groupStack.push(event.callId); this._appendTraceEvent(event); } async groupEnd(): Promise { - const callId = this._groupStack.pop(); - assert(callId, 'Cannot end group that has not started'); + if (!this._state) + return; + const callId = this._state.groupStack.pop(); + if (!callId) + return; const event: trace.AfterActionTraceEvent = { type: 'after', callId, @@ -297,6 +306,11 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps await this._fs.syncAndGetError(); } + async _closeAllGroups() { + while (this._state?.groupStack.length) + await this.groupEnd(); + } + async stopChunk(params: TracingTracingStopChunkParams): Promise<{ artifact?: Artifact, entries?: NameValue[] }> { if (this._isStopping) throw new Error(`Tracing is already stopping`); @@ -309,6 +323,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps return {}; } + await this._closeAllGroups(); + this._context.instrumentation.removeListener(this); eventsHelper.removeEventListeners(this._eventListeners); if (this._state.options.screenshots) @@ -390,8 +406,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps const event = createBeforeActionTraceEvent(metadata); if (!event) return Promise.resolve(); - if (event.parentId === undefined && this._groupStack.length) - event.parentId = this._groupStack[this._groupStack.length - 1]; + this._applyOpenGroup(event); sdkObject.attribution.page?.temporarilyDisableTracingScreencastThrottling(); event.beforeSnapshot = `before@${metadata.id}`; this._state?.callIds.add(metadata.id); @@ -399,6 +414,11 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps return this._captureSnapshot(event.beforeSnapshot, sdkObject, metadata); } + private _applyOpenGroup(event: trace.BeforeActionTraceEvent) { + if (event.parentId === undefined && this._state?.groupStack.length) + event.parentId = this._state?.groupStack[this._state.groupStack.length - 1]; + } + onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata) { if (!this._state?.callIds.has(metadata.id)) return Promise.resolve(); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 092f75b263..42ce1fb7ad 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -21058,6 +21058,36 @@ export interface Touchscreen { * */ export interface Tracing { + /** + * Creates a new inline group in the trace log, causing any subsequent calls to be indented by an additional level, + * until [tracing.groupEnd()](https://playwright.dev/docs/api/class-tracing#tracing-group-end) is called. + * @param name Group name shown in the trace viewer. + * @param options + */ + group(name: string, options?: { + location?: { + /** + * Source file path to be shown in the trace viewer source tab. + */ + file: string; + + /** + * Line number in the source file. + */ + line?: number; + + /** + * Column number in the source file + */ + column?: number; + }; + }): Promise; + + /** + * Closes the last opened inline group in the trace log. + */ + groupEnd(): Promise; + /** * Start tracing. *