mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-07 19:59:11 +03:00
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
|
||||
[`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
|
||||
|
||||
|
@ -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)
|
||||
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -2297,6 +2297,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']),
|
||||
});
|
||||
|
@ -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';
|
||||
@ -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 };
|
||||
|
@ -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';
|
||||
@ -61,6 +61,7 @@ type RecordingState = {
|
||||
traceSha1s: Set<string>,
|
||||
recording: boolean;
|
||||
callIds: Set<string>;
|
||||
groupStack: string[];
|
||||
};
|
||||
|
||||
const kScreencastOptions = { width: 800, height: 600, quality: 90 };
|
||||
@ -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, '');
|
||||
@ -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;
|
||||
@ -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();
|
||||
@ -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`);
|
||||
@ -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)
|
||||
@ -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();
|
||||
@ -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,
|
||||
@ -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 {
|
||||
|
56
packages/playwright-core/types/types.d.ts
vendored
56
packages/playwright-core/types/types.d.ts
vendored
@ -21055,6 +21055,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.
|
||||
*
|
||||
|
@ -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';
|
||||
@ -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;
|
||||
|
@ -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}`;
|
||||
|
||||
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);
|
||||
|
@ -4086,6 +4086,8 @@ export interface TracingChannel extends TracingEventTarget, Channel {
|
||||
_type_Tracing: boolean;
|
||||
tracingStart(params: TracingTracingStartParams, metadata?: CallMetadata): Promise<TracingTracingStartResult>;
|
||||
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>;
|
||||
tracingStop(params?: TracingTracingStopParams, metadata?: CallMetadata): Promise<TracingTracingStopResult>;
|
||||
}
|
||||
@ -4113,6 +4115,25 @@ export type TracingTracingStartChunkOptions = {
|
||||
export type TracingTracingStartChunkResult = {
|
||||
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 = {
|
||||
mode: 'archive' | 'discard' | 'entries',
|
||||
};
|
||||
|
@ -3198,6 +3198,18 @@ Tracing:
|
||||
returns:
|
||||
traceName: string
|
||||
|
||||
tracingGroup:
|
||||
parameters:
|
||||
name: string
|
||||
location:
|
||||
type: object?
|
||||
properties:
|
||||
file: string
|
||||
line: number?
|
||||
column: number?
|
||||
|
||||
tracingGroupEnd:
|
||||
|
||||
tracingStopChunk:
|
||||
parameters:
|
||||
mode:
|
||||
|
@ -36,6 +36,7 @@ export type TraceViewerFixtures = {
|
||||
|
||||
class TraceViewerPage {
|
||||
actionTitles: Locator;
|
||||
actionsTree: Locator;
|
||||
callLines: Locator;
|
||||
consoleLines: Locator;
|
||||
logLines: Locator;
|
||||
@ -46,9 +47,11 @@ class TraceViewerPage {
|
||||
networkRequests: Locator;
|
||||
metadataTab: Locator;
|
||||
snapshotContainer: Locator;
|
||||
sourceCodeTab: Locator;
|
||||
|
||||
constructor(public page: Page) {
|
||||
this.actionTitles = page.locator('.action-title');
|
||||
this.actionsTree = page.getByTestId('actions-tree');
|
||||
this.callLines = page.locator('.call-tab .call-line');
|
||||
this.logLines = page.getByTestId('log-list').locator('.list-view-entry');
|
||||
this.consoleLines = page.locator('.console-line');
|
||||
@ -59,6 +62,7 @@ class TraceViewerPage {
|
||||
this.networkRequests = page.getByTestId('network-list').locator('.list-view-entry');
|
||||
this.snapshotContainer = page.locator('.snapshot-container iframe.snapshot-visible[name=snapshot]');
|
||||
this.metadataTab = page.getByTestId('metadata-view');
|
||||
this.sourceCodeTab = page.getByTestId('source-code');
|
||||
}
|
||||
|
||||
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/);
|
||||
});
|
||||
|
||||
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 }) => {
|
||||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
await expect(traceViewer.actionTitles).toHaveText([
|
||||
|
Loading…
Reference in New Issue
Block a user