diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 3eba797853..6e6ab4660c 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -322,7 +322,7 @@ export class TestInfoImpl implements TestInfo { location: data.location, }; this._onStepBegin(payload); - this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.apiName || data.title, data.params, data.location ? [data.location] : []); + this._tracing.appendBeforeActionForStep(stepId, parentStep?.stepId, data.category, data.apiName || data.title, data.params, data.location ? [data.location] : []); return step; } @@ -421,7 +421,7 @@ export class TestInfoImpl implements TestInfo { } else { // trace viewer has no means of representing attachments outside of a step, so we create an artificial action const callId = `attach@${++this._lastStepId}`; - this._tracing.appendBeforeActionForStep(callId, this._findLastStageStep(this._steps)?.stepId, `attach "${attachment.name}"`, undefined, []); + this._tracing.appendBeforeActionForStep(callId, this._findLastStageStep(this._steps)?.stepId, 'attach', `attach "${attachment.name}"`, undefined, []); this._tracing.appendAfterActionForStep(callId, undefined, [attachment]); } diff --git a/packages/playwright/src/worker/testTracing.ts b/packages/playwright/src/worker/testTracing.ts index eb0ce9d807..fde5ea5f3f 100644 --- a/packages/playwright/src/worker/testTracing.ts +++ b/packages/playwright/src/worker/testTracing.ts @@ -245,14 +245,14 @@ export class TestTracing { }); } - appendBeforeActionForStep(callId: string, parentId: string | undefined, apiName: string, params: Record | undefined, stack: StackFrame[]) { + appendBeforeActionForStep(callId: string, parentId: string | undefined, category: string, apiName: string, params: Record | undefined, stack: StackFrame[]) { this._appendTraceEvent({ type: 'before', callId, parentId, startTime: monotonicTime(), class: 'Test', - method: 'step', + method: category, apiName, params: Object.fromEntries(Object.entries(params || {}).map(([name, value]) => [name, generatePreview(value)])), stack, diff --git a/packages/trace-viewer/src/ui/actionList.css b/packages/trace-viewer/src/ui/actionList.css index 0cb3cb5f54..f167352593 100644 --- a/packages/trace-viewer/src/ui/actionList.css +++ b/packages/trace-viewer/src/ui/actionList.css @@ -41,6 +41,10 @@ color: var(--vscode-editorCodeLens-foreground); } +.action-skipped { + margin-right: 4px; +} + .action-icon { flex: none; display: flex; diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx index 87e78416b0..2c6932bd45 100644 --- a/packages/trace-viewer/src/ui/actionList.tsx +++ b/packages/trace-viewer/src/ui/actionList.tsx @@ -15,7 +15,7 @@ */ import type { ActionTraceEvent, AfterActionTraceEventAttachment } from '@trace/trace'; -import { msToString } from '@web/uiUtils'; +import { clsx, msToString } from '@web/uiUtils'; import * as React from 'react'; import './actionList.css'; import * as modelUtil from './modelUtil'; @@ -25,6 +25,7 @@ import { TreeView } from '@web/components/treeView'; import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil'; import type { Boundaries } from './geometry'; import { ToolbarButton } from '@web/components/toolbarButton'; +import { testStatusIcon } from './testUtils'; export interface ActionListProps { actions: ActionTraceEventInContext[], @@ -119,6 +120,7 @@ export const renderAction = ( const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript'); + const isSkipped = action.class === 'Test' && action.method === 'test.step.skip'; let time: string = ''; if (action.endTime) time = msToString(action.endTime - action.startTime); @@ -149,9 +151,10 @@ export const renderAction = ( {action.method === 'goto' && action.params.url &&
{action.params.url}
} {action.class === 'APIRequestContext' && action.params.url &&
{excludeOrigin(action.params.url)}
} - {(showDuration || showBadges || showAttachments) &&
} + {(showDuration || showBadges || showAttachments || isSkipped) &&
} {showAttachments && revealAttachment(action.attachments![0])} />} - {showDuration &&
{time || }
} + {showDuration && !isSkipped &&
{time || }
} + {isSkipped && } {showBadges &&
revealConsole?.()}> {!!errors &&
{errors}
} {!!warnings &&
{warnings}
} diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts index 23a321338b..8001623721 100644 --- a/tests/playwright-test/ui-mode-trace.spec.ts +++ b/tests/playwright-test/ui-mode-trace.spec.ts @@ -398,7 +398,7 @@ test('should show custom fixture titles in actions tree', async ({ runUITest }) const { page } = await runUITest({ 'a.test.ts': ` import { test as base, expect } from '@playwright/test'; - + const test = base.extend({ fixture1: [async ({}, use) => { await use(); @@ -457,7 +457,7 @@ test('attachments tab shows all but top-level .push attachments', async ({ runUI - tree: - treeitem /step/: - group: - - treeitem /attach \\"foo-attach\\"/ + - treeitem /attach \\"foo-attach\\"/ - treeitem /attach \\"bar-push\\"/ - treeitem /attach \\"bar-attach\\"/ `); @@ -470,3 +470,32 @@ test('attachments tab shows all but top-level .push attachments', async ({ runUI - button /bar-attach/ `); }); + +test('skipped steps should have an indicator', async ({ runUITest }) => { + const { page } = await runUITest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('test with steps', async ({}) => { + await test.step('outer', async () => { + await test.step.skip('skipped1', () => {}); + }); + await test.step.skip('skipped2', () => {}); + }); + `, + }); + + await page.getByRole('treeitem', { name: 'test with steps' }).dblclick(); + const actionsTree = page.getByTestId('actions-tree'); + await actionsTree.getByRole('treeitem', { name: 'outer' }).click(); + await page.keyboard.press('ArrowRight'); + await expect(actionsTree).toMatchAriaSnapshot(` + - tree: + - treeitem /outer/ [expanded]: + - group: + - treeitem /skipped1/ + - treeitem /skipped2/ + `); + const skippedMarker = actionsTree.getByRole('treeitem', { name: 'skipped1' }).locator('.action-skipped'); + await expect(skippedMarker).toBeVisible(); + await expect(skippedMarker).toHaveAccessibleName('skipped'); +});