diff --git a/packages/trace-viewer/src/ui/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts index c82cbc99a6..5793536da5 100644 --- a/packages/trace-viewer/src/ui/modelUtil.ts +++ b/packages/trace-viewer/src/ui/modelUtil.ts @@ -24,6 +24,7 @@ import type { StackFrame } from '@protocol/channels'; const contextSymbol = Symbol('context'); const nextInContextSymbol = Symbol('next'); const prevInListSymbol = Symbol('prev'); +const parentActionSymbol = Symbol('parent'); const eventsSymbol = Symbol('events'); export type SourceLocation = { @@ -195,8 +196,11 @@ function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) { return a1.startTime - a2.startTime; }); - for (let i = 1; i < result.length; ++i) + for (let i = 1; i < result.length; ++i) { (result[i] as any)[prevInListSymbol] = result[i - 1]; + if (result[i].parentId) + (result[i] as any)[parentActionSymbol] = result.find(a => a.callId === result[i].parentId); + } return result; } @@ -356,6 +360,10 @@ export function prevInList(action: ActionTraceEvent): ActionTraceEvent { return (action as any)[prevInListSymbol]; } +export function parentAction(action: ActionTraceEvent): ActionTraceEvent { + return (action as any)[parentActionSymbol]; +} + export function stats(action: ActionTraceEvent): { errors: number, warnings: number } { let errors = 0; let warnings = 0; diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 798025cbbd..149e554d5b 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -17,7 +17,7 @@ import './snapshotTab.css'; import * as React from 'react'; import type { ActionTraceEvent } from '@trace/trace'; -import { context, prevInList } from './modelUtil'; +import { context, parentAction, prevInList } from './modelUtil'; import { Toolbar } from '@web/components/toolbar'; import { ToolbarButton } from '@web/components/toolbarButton'; import { clsx, useMeasure } from '@web/uiUtils'; @@ -50,7 +50,7 @@ export const SnapshotTab: React.FunctionComponent<{ // if the action has no beforeSnapshot, use the last available afterSnapshot. let beforeSnapshot: Snapshot | undefined = action.beforeSnapshot ? { action, snapshotName: action.beforeSnapshot } : undefined; - let a = action; + let a: ActionTraceEvent | undefined = action; while (!beforeSnapshot && a) { a = prevInList(a); beforeSnapshot = a?.afterSnapshot ? { action: a, snapshotName: a?.afterSnapshot } : undefined; @@ -180,27 +180,51 @@ export const SnapshotTab: React.FunctionComponent<{ setHighlightedLocator={setHighlightedLocator} iframe={iframeRef1.current} iteration={loadingRef.current.iteration} /> - - setIsInspecting(!isInspecting)} /> - {['action', 'before', 'after'].map(tab => { - return setSnapshotTab(tab as 'action' | 'before' | 'after')} - >; - })} -
- { - if (!openPage) - openPage = window.open; - const win = openPage(popoutUrl || '', '_blank'); - win?.addEventListener('DOMContentLoaded', () => { - const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []); - new ConsoleAPI(injectedScript); - }); - }}> -
+
+ + setIsInspecting(!isInspecting)} /> + {['action', 'before', 'after'].map(tab => { + return setSnapshotTab(tab as 'action' | 'before' | 'after')} + >; + })} + + + {actionToBreadcrumb(action)} + + + { + if (!openPage) + openPage = window.open; + const win = openPage(popoutUrl || '', '_blank'); + win?.addEventListener('DOMContentLoaded', () => { + const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []); + new ConsoleAPI(injectedScript); + }); + }}> + +
; }; +function actionToBreadcrumb(action: ActionTraceEvent | undefined): React.ReactNode { + if (!action) + return ''; + const parts = []; + while (action) { + parts.push(action.apiName); + action = parentAction(action); + } + return parts.reverse().join(' › '); +} + function renderTitle(snapshotTitle: string): string { if (snapshotTitle === 'before') return 'Before'; diff --git a/packages/web/src/components/toolbar.tsx b/packages/web/src/components/toolbar.tsx index c7dd6843d9..97212f70a5 100644 --- a/packages/web/src/components/toolbar.tsx +++ b/packages/web/src/components/toolbar.tsx @@ -22,15 +22,14 @@ type ToolbarProps = { noShadow?: boolean; noMinHeight?: boolean; className?: string; - onClick?: (e: React.MouseEvent) => void; -}; +} & React.HTMLAttributes; export const Toolbar: React.FC> = ({ noShadow, children, noMinHeight, className, - onClick, + ...props }) => { - return
{children}
; + return
{children}
; }; diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 43a42512fd..7e2add53bb 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -289,6 +289,14 @@ test('should show snapshot URL', async ({ page, runAndTrace, server }) => { }); test('should popup snapshot', async ({ page, runAndTrace, server }) => { + const traceViewer = await runAndTrace(async () => { + await page.goto(server.EMPTY_PAGE); + }); + await traceViewer.snapshotFrame('page.goto'); + await expect(traceViewer.page.getByTestId('snapshot-breadcrumb')).toHaveText('page.goto'); +}); + +test('should show active action title', async ({ page, runAndTrace, server }) => { const traceViewer = await runAndTrace(async () => { await page.goto(server.EMPTY_PAGE); await page.setContent('hello'); diff --git a/tests/playwright-test/ui-mode-test-attachments.spec.ts b/tests/playwright-test/ui-mode-test-attachments.spec.ts index 7016a36115..0234004228 100644 --- a/tests/playwright-test/ui-mode-test-attachments.spec.ts +++ b/tests/playwright-test/ui-mode-test-attachments.spec.ts @@ -91,7 +91,7 @@ test('should contain string attachment', async ({ runUITest }) => { await page.getByTitle('Run all').click(); await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); await page.getByText('Attachments').click(); - await page.getByText('attach "note"', { exact: true }).click(); + await page.getByTestId('actions-tree').getByRole('listitem').filter({ hasText: 'attach "note"' }).click(); const downloadPromise = page.waitForEvent('download'); await page.locator('.expandable-title', { hasText: 'note' }).getByRole('link').click(); const download = await downloadPromise; diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts index 9f0749893e..d44b1f595a 100644 --- a/tests/playwright-test/ui-mode-trace.spec.ts +++ b/tests/playwright-test/ui-mode-trace.spec.ts @@ -112,7 +112,7 @@ test('should locate sync assertions in source', async ({ runUITest }) => { }); await page.getByText('trace test').dblclick(); - await page.getByText('expect.toBe').click(); + await page.getByTestId('actions-tree').getByRole('listitem').filter({ hasText: 'expect.toBe' }).click(); await expect( page.locator('.CodeMirror .source-line-running'),