Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions docs/src/api/class-tracing.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,56 @@ 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 within the trace, assigning any subsequent calls to this group until [method: Tracing.groupEnd] is invoked.

Groups can be nested and are similar to `test.step` in trace.
However, groups are only visualized in the trace viewer and, unlike test.step, have no effect on the test reports.

:::note Groups should not be used with Playwright Test!

This API is intended for Playwright API users that can not use `test.step`.
:::

**Usage**

```js
await context.tracing.start({ screenshots: true, snapshots: true });
await context.tracing.group('Open Playwright.dev');
// All actions between group and groupEnd will be shown in the trace viewer as a group.
const page = await context.newPage();
await page.goto('https://playwright.dev/');
await context.tracing.groupEnd();
await context.tracing.group('Open API Docs of Tracing');
await page.getByRole('link', { name: 'API' }).click();
await page.getByRole('link', { name: 'Tracing' }).click();
await context.tracing.groupEnd();
// This Trace will have two groups: 'Open Playwright.dev' and 'Open API Docs of Tracing'.
```

### param: Tracing.group.name
* since: v1.49
- `name` <[string]>

Group name shown in the actions tree in 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

Specifies a custom location for the group start to be shown in source tab in trace viewer.
By default, location of the tracing.group() call is shown.

## async method: Tracing.groupEnd
* since: v1.49

Closes the currently open inline group in the trace.

## async method: Tracing.stop
* since: v1.12

Expand Down
5 changes: 3 additions & 2 deletions packages/playwright-core/src/client/channelOwner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
return channel;
}

async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal = false): Promise<R> {
async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal?: boolean): Promise<R> {
const logger = this._logger;
const apiZone = zones.zoneData<ApiZone>('apiZone');
if (apiZone)
Expand All @@ -178,7 +178,8 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
let apiName: string | undefined = stackTrace.apiName;
const frames: channels.StackFrame[] = stackTrace.frames;

isInternal = isInternal || this._isInternalType;
if (isInternal === undefined)
isInternal = this._isInternalType;
if (isInternal)
apiName = undefined;

Expand Down
12 changes: 12 additions & 0 deletions packages/playwright-core/src/client/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
await this._startCollectingStacks(traceName);
}

async group(name: string, options: { location?: { file: string, line?: number, column?: number } } = {}) {
await this._wrapApiCall(async () => {
await this._channel.tracingGroup({ name, location: options.location });
}, false);
}

async groupEnd() {
await this._wrapApiCall(async () => {
await this._channel.tracingGroupEnd();
}, false);
}

private async _startCollectingStacks(traceName: string) {
if (!this._isTracing) {
this._isTracing = true;
Expand Down
11 changes: 11 additions & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2295,6 +2295,17 @@ scheme.TracingTracingStartChunkParams = tObject({
scheme.TracingTracingStartChunkResult = tObject({
traceName: tString,
});
scheme.TracingTracingGroupParams = tObject({
name: tString,
location: tOptional(tObject({
file: tString,
line: tOptional(tNumber),
column: tOptional(tNumber),
})),
});
scheme.TracingTracingGroupResult = tOptional(tObject({}));
scheme.TracingTracingGroupEndParams = tOptional(tObject({}));
scheme.TracingTracingGroupEndResult = tOptional(tObject({}));
scheme.TracingTracingStopChunkParams = tObject({
mode: tEnum(['archive', 'discard', 'entries']),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import type * as channels from '@protocol/channels';
import type { CallMetadata } from '@protocol/callMetadata';
import type { Tracing } from '../trace/recorder/tracing';
import { ArtifactDispatcher } from './artifactDispatcher';
import { Dispatcher, existingDispatcher } from './dispatcher';
Expand All @@ -41,6 +42,15 @@ export class TracingDispatcher extends Dispatcher<Tracing, channels.TracingChann
return await this._object.startChunk(params);
}

async tracingGroup(params: channels.TracingTracingGroupParams, metadata: CallMetadata): Promise<channels.TracingTracingGroupResult> {
const { name, location } = params;
await this._object.group(name, location, metadata);
}

async tracingGroupEnd(params: channels.TracingTracingGroupEndParams): Promise<channels.TracingTracingGroupEndResult> {
await this._object.groupEnd();
}

async tracingStopChunk(params: channels.TracingTracingStopChunkParams): Promise<channels.TracingTracingStopChunkResult> {
const { artifact, entries } = await this._object.stopChunk(params);
return { artifact: artifact ? ArtifactDispatcher.from(this, artifact) : undefined, entries };
Expand Down
68 changes: 64 additions & 4 deletions packages/playwright-core/src/server/trace/recorder/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
import type { NameValue } from '../../../common/types';
import type { TracingTracingStopChunkParams } from '@protocol/channels';
import type { TracingTracingStopChunkParams, StackFrame } from '@protocol/channels';
import { commandsWithTracingSnapshots } from '../../../protocol/debug';
import { assert, createGuid, monotonicTime, SerializedFS, removeFolders, eventsHelper, type RegisteredListener } from '../../../utils';
import { Artifact } from '../../artifact';
Expand Down Expand Up @@ -61,6 +61,7 @@ type RecordingState = {
traceSha1s: Set<string>,
recording: boolean;
callIds: Set<string>;
groupStack: string[];
};

const kScreencastOptions = { width: 800, height: 600, quality: 90 };
Expand Down Expand Up @@ -148,6 +149,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
networkSha1s: new Set(),
recording: false,
callIds: new Set(),
groupStack: [],
};
this._fs.mkdir(this._state.resourcesDir);
this._fs.writeFile(this._state.networkFile, '');
Expand Down Expand Up @@ -194,6 +196,53 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
return { traceName: this._state.traceName };
}

private _currentGroupId(): string | undefined {
return this._state?.groupStack.length ? this._state.groupStack[this._state.groupStack.length - 1] : undefined;
}

async group(name: string, location: { file: string, line?: number, column?: number } | undefined, metadata: CallMetadata): Promise<void> {
if (!this._state)
return;
const stackFrames: StackFrame[] = [];
const { file, line, column } = location ?? metadata.location ?? {};
if (file) {
stackFrames.push({
file,
line: line ?? 0,
column: column ?? 0,
});
}
const event: trace.BeforeActionTraceEvent = {
type: 'before',
callId: metadata.id,
startTime: metadata.startTime,
apiName: name,
class: 'Tracing',
method: 'tracingGroup',
params: { },
stepId: metadata.stepId,
stack: stackFrames,
};
if (this._currentGroupId())
event.parentId = this._currentGroupId();
this._state.groupStack.push(event.callId);
this._appendTraceEvent(event);
}

async groupEnd(): Promise<void> {
if (!this._state)
return;
const callId = this._state.groupStack.pop();
if (!callId)
return;
const event: trace.AfterActionTraceEvent = {
type: 'after',
callId,
endTime: monotonicTime(),
};
this._appendTraceEvent(event);
}

private _startScreencast() {
if (!(this._context instanceof BrowserContext))
return;
Expand Down Expand Up @@ -236,6 +285,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
throw new Error(`Tracing is already stopping`);
if (this._state.recording)
throw new Error(`Must stop trace file before stopping tracing`);
await this._closeAllGroups();
this._harTracer.stop();
this.flushHarEntries();
await this._fs.syncAndGetError();
Expand Down Expand Up @@ -264,6 +314,11 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
await this._fs.syncAndGetError();
}

async _closeAllGroups() {
while (this._currentGroupId())
await this.groupEnd();
}

async stopChunk(params: TracingTracingStopChunkParams): Promise<{ artifact?: Artifact, entries?: NameValue[] }> {
if (this._isStopping)
throw new Error(`Tracing is already stopping`);
Expand All @@ -276,6 +331,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)
Expand Down Expand Up @@ -354,7 +411,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps

onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
// IMPORTANT: no awaits before this._appendTraceEvent in this method.
const event = createBeforeActionTraceEvent(metadata);
const event = createBeforeActionTraceEvent(metadata, this._currentGroupId());
if (!event)
return Promise.resolve();
sdkObject.attribution.page?.temporarilyDisableTracingScreencastThrottling();
Expand Down Expand Up @@ -571,10 +628,10 @@ export function shouldCaptureSnapshot(metadata: CallMetadata): boolean {
return commandsWithTracingSnapshots.has(metadata.type + '.' + metadata.method);
}

function createBeforeActionTraceEvent(metadata: CallMetadata): trace.BeforeActionTraceEvent | null {
function createBeforeActionTraceEvent(metadata: CallMetadata, parentId?: string): trace.BeforeActionTraceEvent | null {
if (metadata.internal || metadata.method.startsWith('tracing'))
return null;
return {
const event: trace.BeforeActionTraceEvent = {
type: 'before',
callId: metadata.id,
startTime: metadata.startTime,
Expand All @@ -585,6 +642,9 @@ function createBeforeActionTraceEvent(metadata: CallMetadata): trace.BeforeActio
stepId: metadata.stepId,
pageId: metadata.pageId,
};
if (parentId)
event.parentId = parentId;
return event;
}

function createInputActionTraceEvent(metadata: CallMetadata): trace.InputActionTraceEvent | null {
Expand Down
56 changes: 56 additions & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21058,6 +21058,62 @@ export interface Touchscreen {
*
*/
export interface Tracing {
/**
* Creates a new inline group within the trace, assigning any subsequent calls to this group until
* [method: Tracing.groupEnd] is invoked.
*
* Groups can be nested and are similar to `test.step` in trace. However, groups are only visualized in the trace
* viewer and, unlike test.step, have no effect on the test reports.
*
* **NOTE** This API is intended for Playwright API users that can not use `test.step`.
*
* **Usage**
*
* ```js
* await context.tracing.start({ screenshots: true, snapshots: true });
* await context.tracing.group('Open Playwright.dev');
* // All actions between group and groupEnd will be shown in the trace viewer as a group.
* const page = await context.newPage();
* await page.goto('https://playwright.dev/');
* await context.tracing.groupEnd();
* await context.tracing.group('Open API Docs of Tracing');
* await page.getByRole('link', { name: 'API' }).click();
* await page.getByRole('link', { name: 'Tracing' }).click();
* await context.tracing.groupEnd();
* // This Trace will have two groups: 'Open Playwright.dev' and 'Open API Docs of Tracing'.
* ```
*
* @param name Group name shown in the actions tree in trace viewer.
* @param options
*/
group(name: string, options?: {
/**
* Specifies a custom location for the group start to be shown in source tab in trace viewer. By default, location of
* the tracing.group() call is shown.
*/
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<void>;

/**
* Closes the currently open inline group in the trace.
*/
groupEnd(): Promise<void>;

/**
* Start tracing.
*
Expand Down
12 changes: 10 additions & 2 deletions packages/playwright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type { APIRequestContext, BrowserContext, Browser, BrowserContextOptions,
import * as playwrightLibrary from 'playwright-core';
import { createGuid, debugMode, addInternalStackPrefix, isString, asLocator, jsonStringifyForceASCII } from 'playwright-core/lib/utils';
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
import type { TestInfoImpl } from './worker/testInfo';
import type { TestInfoImpl, TestStepInternal } from './worker/testInfo';
import { rootTestType } from './common/testType';
import type { ContextReuseMode } from './common/config';
import type { ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
Expand Down Expand Up @@ -255,20 +255,28 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({

const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);

const tracingGroupSteps: TestStepInternal[] = [];
const csiListener: ClientInstrumentationListener = {
onApiCallBegin: (apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }) => {
const testInfo = currentTestInfo();
if (!testInfo || apiName.includes('setTestIdAttribute'))
return { userObject: null };
if (apiName === 'tracing.groupEnd') {
tracingGroupSteps.pop();
return { userObject: null };
}
const step = testInfo._addStep({
location: frames[0] as any,
category: 'pw:api',
title: renderApiCall(apiName, params),
apiName,
params,
});
}, tracingGroupSteps[tracingGroupSteps.length - 1]);
userData.userObject = step;
out.stepId = step.stepId;
if (apiName === 'tracing.group')
tracingGroupSteps.push(step);
},
onApiCallEnd: (userData: any, error?: Error) => {
const step = userData.userObject;
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright/src/worker/testInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,15 +237,15 @@ export class TestInfoImpl implements TestInfo {
}
}

_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps'>): TestStepInternal {
_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps'>, parentStep?: TestStepInternal): TestStepInternal {
const stepId = `${data.category}@${++this._lastStepId}`;

let parentStep: TestStepInternal | undefined;
if (data.isStage) {
// Predefined stages form a fixed hierarchy - use the current one as parent.
parentStep = this._findLastStageStep(this._steps);
} else {
parentStep = zones.zoneData<TestStepInternal>('stepZone');
if (!parentStep)
parentStep = zones.zoneData<TestStepInternal>('stepZone');
if (!parentStep) {
// If no parent step on stack, assume the current stage as parent.
parentStep = this._findLastStageStep(this._steps);
Expand Down
Loading