From fa10bcd5a30c94c1a966bc34ab33b8b214dbf47a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= <41592183+Snooz82@users.noreply.github.com> Date: Tue, 5 Nov 2024 12:45:37 +0100 Subject: [PATCH] feat(tracing) Adding groups to trace via pw-api (#33081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: René Signed-off-by: René <41592183+Snooz82@users.noreply.github.com> Co-authored-by: Dmitry Gozman --- docs/src/api/class-tracing.md | 50 +++++++++ .../src/client/channelOwner.ts | 5 +- .../playwright-core/src/client/tracing.ts | 12 +++ .../playwright-core/src/protocol/validator.ts | 11 ++ .../server/dispatchers/tracingDispatcher.ts | 10 ++ .../src/server/trace/recorder/tracing.ts | 68 +++++++++++- packages/playwright-core/types/types.d.ts | 56 ++++++++++ packages/playwright/src/index.ts | 12 ++- packages/playwright/src/worker/testInfo.ts | 6 +- packages/protocol/src/channels.ts | 21 ++++ packages/protocol/src/protocol.yml | 12 +++ tests/config/traceViewerFixtures.ts | 4 + tests/library/trace-viewer.spec.ts | 100 ++++++++++++++++++ 13 files changed, 356 insertions(+), 11 deletions(-) diff --git a/docs/src/api/class-tracing.md b/docs/src/api/class-tracing.md index 065896925fa74..353a220516698 100644 --- a/docs/src/api/class-tracing.md +++ b/docs/src/api/class-tracing.md @@ -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 diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index 89f3edced3229..a5d753507bddf 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -168,7 +168,7 @@ export abstract class ChannelOwner(func: (apiZone: ApiZone) => Promise, isInternal = false): Promise { + async _wrapApiCall(func: (apiZone: ApiZone) => Promise, isInternal?: boolean): Promise { const logger = this._logger; const apiZone = zones.zoneData('apiZone'); if (apiZone) @@ -178,7 +178,8 @@ export abstract class ChannelOwner 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; diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 13dbeaa362e7e..9cfe7149a43d5 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -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']), }); diff --git a/packages/playwright-core/src/server/dispatchers/tracingDispatcher.ts b/packages/playwright-core/src/server/dispatchers/tracingDispatcher.ts index b8214fbe31b5f..5555de15d1e1b 100644 --- a/packages/playwright-core/src/server/dispatchers/tracingDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/tracingDispatcher.ts @@ -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 { + const { name, location } = params; + await this._object.group(name, location, metadata); + } + + async tracingGroupEnd(params: channels.TracingTracingGroupEndParams): Promise { + await this._object.groupEnd(); + } + async tracingStopChunk(params: channels.TracingTracingStopChunkParams): Promise { const { artifact, entries } = await this._object.stopChunk(params); return { artifact: artifact ? ArtifactDispatcher.from(this, artifact) : undefined, entries }; diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 97476d4b3122a..3f278008e2742 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -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, recording: boolean; callIds: Set; + 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 { + 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 { + 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 { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index d963203643a03..d67c2ca8abc6d 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -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; + + /** + * Closes the currently open inline group in the trace. + */ + groupEnd(): Promise; + /** * Start tracing. * diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index c2c9596b6e844..efade476a8dae 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -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 = ({ 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, 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; diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index b5b1010ff255c..8b965e0a14577 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -238,15 +238,15 @@ export class TestInfoImpl implements TestInfo { } } - _addStep(data: Omit): TestStepInternal { + _addStep(data: Omit, 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('stepZone'); + if (!parentStep) + parentStep = zones.zoneData('stepZone'); if (!parentStep) { // If no parent step on stack, assume the current stage as parent. parentStep = this._findLastStageStep(this._steps); diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 5b37b33bcf86d..9311e969af5cb 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -4086,6 +4086,8 @@ export interface TracingChannel extends TracingEventTarget, Channel { _type_Tracing: boolean; tracingStart(params: TracingTracingStartParams, metadata?: CallMetadata): Promise; tracingStartChunk(params: TracingTracingStartChunkParams, metadata?: CallMetadata): Promise; + tracingGroup(params: TracingTracingGroupParams, metadata?: CallMetadata): Promise; + tracingGroupEnd(params?: TracingTracingGroupEndParams, metadata?: CallMetadata): Promise; tracingStopChunk(params: TracingTracingStopChunkParams, metadata?: CallMetadata): Promise; tracingStop(params?: TracingTracingStopParams, metadata?: CallMetadata): Promise; } @@ -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', }; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 82de53be24fc8..7893bb10930b5 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -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: diff --git a/tests/config/traceViewerFixtures.ts b/tests/config/traceViewerFixtures.ts index 3eb3b11a153c7..837b953ef4f28 100644 --- a/tests/config/traceViewerFixtures.ts +++ b/tests/config/traceViewerFixtures.ts @@ -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) { diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index c13eb778d1068..b8a8602313f3f 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -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,Hello world`); + await page.setContent(''); + 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,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,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,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([