feat(tracing) Adding groups to trace via pw-api (#33081)
Signed-off-by: René <snooz@posteo.de> Signed-off-by: René <41592183+Snooz82@users.noreply.github.com> Co-authored-by: Dmitry Gozman <dgozman@gmail.com>
This commit is contained in:
parent
da4614ea7c
commit
fa10bcd5a3
|
@ -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
|
To specify the final trace zip file name, you need to pass `path` option to
|
||||||
[`method: Tracing.stopChunk`] instead.
|
[`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
|
## async method: Tracing.stop
|
||||||
* since: v1.12
|
* since: v1.12
|
||||||
|
|
||||||
|
|
|
@ -168,7 +168,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
||||||
return 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 logger = this._logger;
|
||||||
const apiZone = zones.zoneData<ApiZone>('apiZone');
|
const apiZone = zones.zoneData<ApiZone>('apiZone');
|
||||||
if (apiZone)
|
if (apiZone)
|
||||||
|
@ -178,7 +178,8 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
||||||
let apiName: string | undefined = stackTrace.apiName;
|
let apiName: string | undefined = stackTrace.apiName;
|
||||||
const frames: channels.StackFrame[] = stackTrace.frames;
|
const frames: channels.StackFrame[] = stackTrace.frames;
|
||||||
|
|
||||||
isInternal = isInternal || this._isInternalType;
|
if (isInternal === undefined)
|
||||||
|
isInternal = this._isInternalType;
|
||||||
if (isInternal)
|
if (isInternal)
|
||||||
apiName = undefined;
|
apiName = undefined;
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,18 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
||||||
await this._startCollectingStacks(traceName);
|
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) {
|
private async _startCollectingStacks(traceName: string) {
|
||||||
if (!this._isTracing) {
|
if (!this._isTracing) {
|
||||||
this._isTracing = true;
|
this._isTracing = true;
|
||||||
|
|
|
@ -2297,6 +2297,17 @@ scheme.TracingTracingStartChunkParams = tObject({
|
||||||
scheme.TracingTracingStartChunkResult = tObject({
|
scheme.TracingTracingStartChunkResult = tObject({
|
||||||
traceName: tString,
|
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({
|
scheme.TracingTracingStopChunkParams = tObject({
|
||||||
mode: tEnum(['archive', 'discard', 'entries']),
|
mode: tEnum(['archive', 'discard', 'entries']),
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
|
import type { CallMetadata } from '@protocol/callMetadata';
|
||||||
import type { Tracing } from '../trace/recorder/tracing';
|
import type { Tracing } from '../trace/recorder/tracing';
|
||||||
import { ArtifactDispatcher } from './artifactDispatcher';
|
import { ArtifactDispatcher } from './artifactDispatcher';
|
||||||
import { Dispatcher, existingDispatcher } from './dispatcher';
|
import { Dispatcher, existingDispatcher } from './dispatcher';
|
||||||
|
@ -41,6 +42,15 @@ export class TracingDispatcher extends Dispatcher<Tracing, channels.TracingChann
|
||||||
return await this._object.startChunk(params);
|
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> {
|
async tracingStopChunk(params: channels.TracingTracingStopChunkParams): Promise<channels.TracingTracingStopChunkResult> {
|
||||||
const { artifact, entries } = await this._object.stopChunk(params);
|
const { artifact, entries } = await this._object.stopChunk(params);
|
||||||
return { artifact: artifact ? ArtifactDispatcher.from(this, artifact) : undefined, entries };
|
return { artifact: artifact ? ArtifactDispatcher.from(this, artifact) : undefined, entries };
|
||||||
|
|
|
@ -18,7 +18,7 @@ import fs from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { NameValue } from '../../../common/types';
|
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 { commandsWithTracingSnapshots } from '../../../protocol/debug';
|
||||||
import { assert, createGuid, monotonicTime, SerializedFS, removeFolders, eventsHelper, type RegisteredListener } from '../../../utils';
|
import { assert, createGuid, monotonicTime, SerializedFS, removeFolders, eventsHelper, type RegisteredListener } from '../../../utils';
|
||||||
import { Artifact } from '../../artifact';
|
import { Artifact } from '../../artifact';
|
||||||
|
@ -61,6 +61,7 @@ type RecordingState = {
|
||||||
traceSha1s: Set<string>,
|
traceSha1s: Set<string>,
|
||||||
recording: boolean;
|
recording: boolean;
|
||||||
callIds: Set<string>;
|
callIds: Set<string>;
|
||||||
|
groupStack: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const kScreencastOptions = { width: 800, height: 600, quality: 90 };
|
const kScreencastOptions = { width: 800, height: 600, quality: 90 };
|
||||||
|
@ -148,6 +149,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
networkSha1s: new Set(),
|
networkSha1s: new Set(),
|
||||||
recording: false,
|
recording: false,
|
||||||
callIds: new Set(),
|
callIds: new Set(),
|
||||||
|
groupStack: [],
|
||||||
};
|
};
|
||||||
this._fs.mkdir(this._state.resourcesDir);
|
this._fs.mkdir(this._state.resourcesDir);
|
||||||
this._fs.writeFile(this._state.networkFile, '');
|
this._fs.writeFile(this._state.networkFile, '');
|
||||||
|
@ -194,6 +196,53 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
return { traceName: this._state.traceName };
|
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() {
|
private _startScreencast() {
|
||||||
if (!(this._context instanceof BrowserContext))
|
if (!(this._context instanceof BrowserContext))
|
||||||
return;
|
return;
|
||||||
|
@ -236,6 +285,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
throw new Error(`Tracing is already stopping`);
|
throw new Error(`Tracing is already stopping`);
|
||||||
if (this._state.recording)
|
if (this._state.recording)
|
||||||
throw new Error(`Must stop trace file before stopping tracing`);
|
throw new Error(`Must stop trace file before stopping tracing`);
|
||||||
|
await this._closeAllGroups();
|
||||||
this._harTracer.stop();
|
this._harTracer.stop();
|
||||||
this.flushHarEntries();
|
this.flushHarEntries();
|
||||||
await this._fs.syncAndGetError();
|
await this._fs.syncAndGetError();
|
||||||
|
@ -264,6 +314,11 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
await this._fs.syncAndGetError();
|
await this._fs.syncAndGetError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _closeAllGroups() {
|
||||||
|
while (this._currentGroupId())
|
||||||
|
await this.groupEnd();
|
||||||
|
}
|
||||||
|
|
||||||
async stopChunk(params: TracingTracingStopChunkParams): Promise<{ artifact?: Artifact, entries?: NameValue[] }> {
|
async stopChunk(params: TracingTracingStopChunkParams): Promise<{ artifact?: Artifact, entries?: NameValue[] }> {
|
||||||
if (this._isStopping)
|
if (this._isStopping)
|
||||||
throw new Error(`Tracing is already stopping`);
|
throw new Error(`Tracing is already stopping`);
|
||||||
|
@ -276,6 +331,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this._closeAllGroups();
|
||||||
|
|
||||||
this._context.instrumentation.removeListener(this);
|
this._context.instrumentation.removeListener(this);
|
||||||
eventsHelper.removeEventListeners(this._eventListeners);
|
eventsHelper.removeEventListeners(this._eventListeners);
|
||||||
if (this._state.options.screenshots)
|
if (this._state.options.screenshots)
|
||||||
|
@ -354,7 +411,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
|
|
||||||
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||||
// IMPORTANT: no awaits before this._appendTraceEvent in this method.
|
// IMPORTANT: no awaits before this._appendTraceEvent in this method.
|
||||||
const event = createBeforeActionTraceEvent(metadata);
|
const event = createBeforeActionTraceEvent(metadata, this._currentGroupId());
|
||||||
if (!event)
|
if (!event)
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
sdkObject.attribution.page?.temporarilyDisableTracingScreencastThrottling();
|
sdkObject.attribution.page?.temporarilyDisableTracingScreencastThrottling();
|
||||||
|
@ -571,10 +628,10 @@ export function shouldCaptureSnapshot(metadata: CallMetadata): boolean {
|
||||||
return commandsWithTracingSnapshots.has(metadata.type + '.' + metadata.method);
|
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'))
|
if (metadata.internal || metadata.method.startsWith('tracing'))
|
||||||
return null;
|
return null;
|
||||||
return {
|
const event: trace.BeforeActionTraceEvent = {
|
||||||
type: 'before',
|
type: 'before',
|
||||||
callId: metadata.id,
|
callId: metadata.id,
|
||||||
startTime: metadata.startTime,
|
startTime: metadata.startTime,
|
||||||
|
@ -585,6 +642,9 @@ function createBeforeActionTraceEvent(metadata: CallMetadata): trace.BeforeActio
|
||||||
stepId: metadata.stepId,
|
stepId: metadata.stepId,
|
||||||
pageId: metadata.pageId,
|
pageId: metadata.pageId,
|
||||||
};
|
};
|
||||||
|
if (parentId)
|
||||||
|
event.parentId = parentId;
|
||||||
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createInputActionTraceEvent(metadata: CallMetadata): trace.InputActionTraceEvent | null {
|
function createInputActionTraceEvent(metadata: CallMetadata): trace.InputActionTraceEvent | null {
|
||||||
|
|
|
@ -21055,6 +21055,62 @@ export interface Touchscreen {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export interface Tracing {
|
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.
|
* Start tracing.
|
||||||
*
|
*
|
||||||
|
|
|
@ -20,7 +20,7 @@ import type { APIRequestContext, BrowserContext, Browser, BrowserContextOptions,
|
||||||
import * as playwrightLibrary from 'playwright-core';
|
import * as playwrightLibrary from 'playwright-core';
|
||||||
import { createGuid, debugMode, addInternalStackPrefix, isString, asLocator, jsonStringifyForceASCII } from 'playwright-core/lib/utils';
|
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 { 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 { rootTestType } from './common/testType';
|
||||||
import type { ContextReuseMode } from './common/config';
|
import type { ContextReuseMode } from './common/config';
|
||||||
import type { ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
|
import type { ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
|
||||||
|
@ -255,20 +255,28 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
|
|
||||||
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
|
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
|
||||||
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);
|
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);
|
||||||
|
|
||||||
|
const tracingGroupSteps: TestStepInternal[] = [];
|
||||||
const csiListener: ClientInstrumentationListener = {
|
const csiListener: ClientInstrumentationListener = {
|
||||||
onApiCallBegin: (apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }) => {
|
onApiCallBegin: (apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }) => {
|
||||||
const testInfo = currentTestInfo();
|
const testInfo = currentTestInfo();
|
||||||
if (!testInfo || apiName.includes('setTestIdAttribute'))
|
if (!testInfo || apiName.includes('setTestIdAttribute'))
|
||||||
return { userObject: null };
|
return { userObject: null };
|
||||||
|
if (apiName === 'tracing.groupEnd') {
|
||||||
|
tracingGroupSteps.pop();
|
||||||
|
return { userObject: null };
|
||||||
|
}
|
||||||
const step = testInfo._addStep({
|
const step = testInfo._addStep({
|
||||||
location: frames[0] as any,
|
location: frames[0] as any,
|
||||||
category: 'pw:api',
|
category: 'pw:api',
|
||||||
title: renderApiCall(apiName, params),
|
title: renderApiCall(apiName, params),
|
||||||
apiName,
|
apiName,
|
||||||
params,
|
params,
|
||||||
});
|
}, tracingGroupSteps[tracingGroupSteps.length - 1]);
|
||||||
userData.userObject = step;
|
userData.userObject = step;
|
||||||
out.stepId = step.stepId;
|
out.stepId = step.stepId;
|
||||||
|
if (apiName === 'tracing.group')
|
||||||
|
tracingGroupSteps.push(step);
|
||||||
},
|
},
|
||||||
onApiCallEnd: (userData: any, error?: Error) => {
|
onApiCallEnd: (userData: any, error?: Error) => {
|
||||||
const step = userData.userObject;
|
const step = userData.userObject;
|
||||||
|
|
|
@ -238,15 +238,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}`;
|
const stepId = `${data.category}@${++this._lastStepId}`;
|
||||||
|
|
||||||
let parentStep: TestStepInternal | undefined;
|
|
||||||
if (data.isStage) {
|
if (data.isStage) {
|
||||||
// Predefined stages form a fixed hierarchy - use the current one as parent.
|
// Predefined stages form a fixed hierarchy - use the current one as parent.
|
||||||
parentStep = this._findLastStageStep(this._steps);
|
parentStep = this._findLastStageStep(this._steps);
|
||||||
} else {
|
} else {
|
||||||
parentStep = zones.zoneData<TestStepInternal>('stepZone');
|
if (!parentStep)
|
||||||
|
parentStep = zones.zoneData<TestStepInternal>('stepZone');
|
||||||
if (!parentStep) {
|
if (!parentStep) {
|
||||||
// If no parent step on stack, assume the current stage as parent.
|
// If no parent step on stack, assume the current stage as parent.
|
||||||
parentStep = this._findLastStageStep(this._steps);
|
parentStep = this._findLastStageStep(this._steps);
|
||||||
|
|
|
@ -4086,6 +4086,8 @@ export interface TracingChannel extends TracingEventTarget, Channel {
|
||||||
_type_Tracing: boolean;
|
_type_Tracing: boolean;
|
||||||
tracingStart(params: TracingTracingStartParams, metadata?: CallMetadata): Promise<TracingTracingStartResult>;
|
tracingStart(params: TracingTracingStartParams, metadata?: CallMetadata): Promise<TracingTracingStartResult>;
|
||||||
tracingStartChunk(params: TracingTracingStartChunkParams, metadata?: CallMetadata): Promise<TracingTracingStartChunkResult>;
|
tracingStartChunk(params: TracingTracingStartChunkParams, metadata?: CallMetadata): Promise<TracingTracingStartChunkResult>;
|
||||||
|
tracingGroup(params: TracingTracingGroupParams, metadata?: CallMetadata): Promise<TracingTracingGroupResult>;
|
||||||
|
tracingGroupEnd(params?: TracingTracingGroupEndParams, metadata?: CallMetadata): Promise<TracingTracingGroupEndResult>;
|
||||||
tracingStopChunk(params: TracingTracingStopChunkParams, metadata?: CallMetadata): Promise<TracingTracingStopChunkResult>;
|
tracingStopChunk(params: TracingTracingStopChunkParams, metadata?: CallMetadata): Promise<TracingTracingStopChunkResult>;
|
||||||
tracingStop(params?: TracingTracingStopParams, metadata?: CallMetadata): Promise<TracingTracingStopResult>;
|
tracingStop(params?: TracingTracingStopParams, metadata?: CallMetadata): Promise<TracingTracingStopResult>;
|
||||||
}
|
}
|
||||||
|
@ -4113,6 +4115,25 @@ export type TracingTracingStartChunkOptions = {
|
||||||
export type TracingTracingStartChunkResult = {
|
export type TracingTracingStartChunkResult = {
|
||||||
traceName: string,
|
traceName: string,
|
||||||
};
|
};
|
||||||
|
export type TracingTracingGroupParams = {
|
||||||
|
name: string,
|
||||||
|
location?: {
|
||||||
|
file: string,
|
||||||
|
line?: number,
|
||||||
|
column?: number,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export type TracingTracingGroupOptions = {
|
||||||
|
location?: {
|
||||||
|
file: string,
|
||||||
|
line?: number,
|
||||||
|
column?: number,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export type TracingTracingGroupResult = void;
|
||||||
|
export type TracingTracingGroupEndParams = {};
|
||||||
|
export type TracingTracingGroupEndOptions = {};
|
||||||
|
export type TracingTracingGroupEndResult = void;
|
||||||
export type TracingTracingStopChunkParams = {
|
export type TracingTracingStopChunkParams = {
|
||||||
mode: 'archive' | 'discard' | 'entries',
|
mode: 'archive' | 'discard' | 'entries',
|
||||||
};
|
};
|
||||||
|
|
|
@ -3198,6 +3198,18 @@ Tracing:
|
||||||
returns:
|
returns:
|
||||||
traceName: string
|
traceName: string
|
||||||
|
|
||||||
|
tracingGroup:
|
||||||
|
parameters:
|
||||||
|
name: string
|
||||||
|
location:
|
||||||
|
type: object?
|
||||||
|
properties:
|
||||||
|
file: string
|
||||||
|
line: number?
|
||||||
|
column: number?
|
||||||
|
|
||||||
|
tracingGroupEnd:
|
||||||
|
|
||||||
tracingStopChunk:
|
tracingStopChunk:
|
||||||
parameters:
|
parameters:
|
||||||
mode:
|
mode:
|
||||||
|
|
|
@ -36,6 +36,7 @@ export type TraceViewerFixtures = {
|
||||||
|
|
||||||
class TraceViewerPage {
|
class TraceViewerPage {
|
||||||
actionTitles: Locator;
|
actionTitles: Locator;
|
||||||
|
actionsTree: Locator;
|
||||||
callLines: Locator;
|
callLines: Locator;
|
||||||
consoleLines: Locator;
|
consoleLines: Locator;
|
||||||
logLines: Locator;
|
logLines: Locator;
|
||||||
|
@ -46,9 +47,11 @@ class TraceViewerPage {
|
||||||
networkRequests: Locator;
|
networkRequests: Locator;
|
||||||
metadataTab: Locator;
|
metadataTab: Locator;
|
||||||
snapshotContainer: Locator;
|
snapshotContainer: Locator;
|
||||||
|
sourceCodeTab: Locator;
|
||||||
|
|
||||||
constructor(public page: Page) {
|
constructor(public page: Page) {
|
||||||
this.actionTitles = page.locator('.action-title');
|
this.actionTitles = page.locator('.action-title');
|
||||||
|
this.actionsTree = page.getByTestId('actions-tree');
|
||||||
this.callLines = page.locator('.call-tab .call-line');
|
this.callLines = page.locator('.call-tab .call-line');
|
||||||
this.logLines = page.getByTestId('log-list').locator('.list-view-entry');
|
this.logLines = page.getByTestId('log-list').locator('.list-view-entry');
|
||||||
this.consoleLines = page.locator('.console-line');
|
this.consoleLines = page.locator('.console-line');
|
||||||
|
@ -59,6 +62,7 @@ class TraceViewerPage {
|
||||||
this.networkRequests = page.getByTestId('network-list').locator('.list-view-entry');
|
this.networkRequests = page.getByTestId('network-list').locator('.list-view-entry');
|
||||||
this.snapshotContainer = page.locator('.snapshot-container iframe.snapshot-visible[name=snapshot]');
|
this.snapshotContainer = page.locator('.snapshot-container iframe.snapshot-visible[name=snapshot]');
|
||||||
this.metadataTab = page.getByTestId('metadata-view');
|
this.metadataTab = page.getByTestId('metadata-view');
|
||||||
|
this.sourceCodeTab = page.getByTestId('source-code');
|
||||||
}
|
}
|
||||||
|
|
||||||
async actionIconsText(action: string) {
|
async actionIconsText(action: string) {
|
||||||
|
|
|
@ -103,6 +103,106 @@ test('should open trace viewer on specific host', async ({ showTraceViewer }, te
|
||||||
await expect(traceViewer.page).toHaveURL(/127.0.0.1/);
|
await expect(traceViewer.page).toHaveURL(/127.0.0.1/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should show groups as tree in trace viewer', async ({ runAndTrace, page, context }) => {
|
||||||
|
const outerGroup = 'Outer Group';
|
||||||
|
const outerGroupContent = 'locator.clickgetByText(\'Click\')';
|
||||||
|
const firstInnerGroup = 'First Inner Group';
|
||||||
|
const firstInnerGroupContent = 'locator.clicklocator(\'button\').first()';
|
||||||
|
const secondInnerGroup = 'Second Inner Group';
|
||||||
|
const secondInnerGroupContent = 'expect.toBeVisiblegetByText(\'Click\')';
|
||||||
|
const expandedFailure = 'Expanded Failure';
|
||||||
|
|
||||||
|
const traceViewer = await test.step('create trace with groups', async () => {
|
||||||
|
return await runAndTrace(async () => {
|
||||||
|
try {
|
||||||
|
await page.goto(`data:text/html,<!DOCTYPE html><html>Hello world</html>`);
|
||||||
|
await page.setContent('<!DOCTYPE html><button>Click</button>');
|
||||||
|
async function doClick() {
|
||||||
|
await page.getByText('Click').click();
|
||||||
|
}
|
||||||
|
await context.tracing.group(outerGroup); // Outer group
|
||||||
|
await doClick();
|
||||||
|
await context.tracing.group(firstInnerGroup, { location: { file: `${__dirname}/tracing.spec.ts`, line: 100, column: 10 } });
|
||||||
|
await page.locator('button >> nth=0').click();
|
||||||
|
await context.tracing.groupEnd();
|
||||||
|
await context.tracing.group(secondInnerGroup, { location: { file: __filename } });
|
||||||
|
await expect(page.getByText('Click')).toBeVisible();
|
||||||
|
await context.tracing.groupEnd();
|
||||||
|
await context.tracing.groupEnd();
|
||||||
|
await context.tracing.group(expandedFailure);
|
||||||
|
try {
|
||||||
|
await expect(page.getByText('Click')).toBeHidden({ timeout: 1 });
|
||||||
|
} catch (e) {}
|
||||||
|
await context.tracing.groupEnd();
|
||||||
|
await page.evaluate(() => console.log('ungrouped'), null);
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
}, { box: true });
|
||||||
|
const treeViewEntries = traceViewer.actionsTree.locator('.tree-view-entry');
|
||||||
|
|
||||||
|
await test.step('check automatic expansion of groups on failure', async () => {
|
||||||
|
await expect(traceViewer.actionTitles).toHaveText([
|
||||||
|
/page.gotodata:text\/html,<!DOCTYPE html><html>Hello world<\/html>/,
|
||||||
|
/page.setContent/,
|
||||||
|
outerGroup,
|
||||||
|
expandedFailure,
|
||||||
|
/expect.toBeHiddengetByText\('Click'\)/,
|
||||||
|
/page.evaluate/,
|
||||||
|
]);
|
||||||
|
await expect(traceViewer.actionsTree.locator('.tree-view-entry.selected > .tree-view-indent')).toHaveCount(1);
|
||||||
|
await expect(traceViewer.actionsTree.locator('.tree-view-entry.selected')).toHaveText(/expect.toBeHiddengetByText\('Click'\)/);
|
||||||
|
await treeViewEntries.filter({ hasText: expandedFailure }).locator('.codicon-chevron-down').click();
|
||||||
|
});
|
||||||
|
await test.step('check outer group', async () => {
|
||||||
|
await treeViewEntries.filter({ hasText: outerGroup }).locator('.codicon-chevron-right').click();
|
||||||
|
await expect(traceViewer.actionTitles).toHaveText([
|
||||||
|
/page.gotodata:text\/html,<!DOCTYPE html><html>Hello world<\/html>/,
|
||||||
|
/page.setContent/,
|
||||||
|
outerGroup,
|
||||||
|
outerGroupContent,
|
||||||
|
firstInnerGroup,
|
||||||
|
secondInnerGroup,
|
||||||
|
expandedFailure,
|
||||||
|
/page.evaluate/,
|
||||||
|
]);
|
||||||
|
await expect(treeViewEntries.filter({ hasText: firstInnerGroup }).locator(' > .tree-view-indent')).toHaveCount(1);
|
||||||
|
await expect(treeViewEntries.filter({ hasText: secondInnerGroup }).locator(' > .tree-view-indent')).toHaveCount(1);
|
||||||
|
await test.step('check automatic location of groups', async () => {
|
||||||
|
await traceViewer.showSourceTab();
|
||||||
|
await traceViewer.selectAction(outerGroup);
|
||||||
|
await expect(traceViewer.sourceCodeTab.locator('.source-tab-file-name')).toHaveAttribute('title', __filename);
|
||||||
|
await expect(traceViewer.sourceCodeTab.locator('.source-line-running')).toHaveText(/\d+\s+await context.tracing.group\(outerGroup\); \/\/ Outer group/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await test.step('check inner groups', async () => {
|
||||||
|
await treeViewEntries.filter({ hasText: firstInnerGroup }).locator('.codicon-chevron-right').click();
|
||||||
|
await treeViewEntries.filter({ hasText: secondInnerGroup }).locator('.codicon-chevron-right').click();
|
||||||
|
await expect(traceViewer.actionTitles).toHaveText([
|
||||||
|
/page.gotodata:text\/html,<!DOCTYPE html><html>Hello world<\/html>/,
|
||||||
|
/page.setContent/,
|
||||||
|
outerGroup,
|
||||||
|
outerGroupContent,
|
||||||
|
firstInnerGroup,
|
||||||
|
firstInnerGroupContent,
|
||||||
|
secondInnerGroup,
|
||||||
|
secondInnerGroupContent,
|
||||||
|
expandedFailure,
|
||||||
|
/page.evaluate/,
|
||||||
|
]);
|
||||||
|
await expect(treeViewEntries.filter({ hasText: firstInnerGroupContent }).locator(' > .tree-view-indent')).toHaveCount(2);
|
||||||
|
await expect(treeViewEntries.filter({ hasText: secondInnerGroupContent }).locator(' > .tree-view-indent')).toHaveCount(2);
|
||||||
|
await test.step('check location with file, line, column', async () => {
|
||||||
|
await traceViewer.selectAction(firstInnerGroup);
|
||||||
|
await expect(traceViewer.sourceCodeTab.locator('.source-tab-file-name')).toHaveAttribute('title', `${__dirname}/tracing.spec.ts`);
|
||||||
|
});
|
||||||
|
await test.step('check location with file', async () => {
|
||||||
|
await traceViewer.selectAction(secondInnerGroup);
|
||||||
|
await expect(traceViewer.sourceCodeTab.getByText(/Licensed under the Apache License/)).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer([traceFile]);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await expect(traceViewer.actionTitles).toHaveText([
|
await expect(traceViewer.actionTitles).toHaveText([
|
||||||
|
|
Loading…
Reference in New Issue