feat(ui-mode): show step breadcrumbs in Trace Viewer

This commit is contained in:
Max Schmitt 2024-08-04 12:11:40 +02:00
parent d0c840f639
commit 8663a6dade
6 changed files with 80 additions and 30 deletions

View file

@ -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;

View file

@ -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,7 +180,12 @@ export const SnapshotTab: React.FunctionComponent<{
setHighlightedLocator={setHighlightedLocator}
iframe={iframeRef1.current}
iteration={loadingRef.current.iteration} />
<Toolbar>
<div className='hbox' style={{ flex: '0 0 auto' }}>
<Toolbar style={{
flex: '0 1 0',
minWidth: 'min-content',
flexGrow: 1
}}>
<ToolbarButton className='pick-locator' title='Pick locator' icon='target' toggled={isInspecting} onClick={() => setIsInspecting(!isInspecting)} />
{['action', 'before', 'after'].map(tab => {
return <TabbedPaneTab
@ -190,7 +195,25 @@ export const SnapshotTab: React.FunctionComponent<{
onSelect={() => setSnapshotTab(tab as 'action' | 'before' | 'after')}
></TabbedPaneTab>;
})}
<div style={{ flex: 'auto' }}></div>
</Toolbar>
<Toolbar
style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
direction: 'rtl',
flexGrow: 0,
flexShrink: 1,
}}
data-testid='snapshot-breadcrumb'>
{actionToBreadcrumb(action)}
</Toolbar>
<Toolbar style={{
flex: '0 1 0',
minWidth: 'min-content',
flexGrow: 1,
justifyContent: 'flex-end',
}}>
<ToolbarButton icon='link-external' title='Open snapshot in a new tab' disabled={!popoutUrl} onClick={() => {
if (!openPage)
openPage = window.open;
@ -201,6 +224,7 @@ export const SnapshotTab: React.FunctionComponent<{
});
}}></ToolbarButton>
</Toolbar>
</div>
<div ref={ref} className='snapshot-wrapper'>
<div className='snapshot-container' style={{
width: snapshotContainerSize.width + 'px',
@ -217,6 +241,17 @@ export const SnapshotTab: React.FunctionComponent<{
</div>;
};
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';

View file

@ -22,15 +22,14 @@ type ToolbarProps = {
noShadow?: boolean;
noMinHeight?: boolean;
className?: string;
onClick?: (e: React.MouseEvent) => void;
};
} & React.HTMLAttributes<HTMLDivElement>;
export const Toolbar: React.FC<React.PropsWithChildren<ToolbarProps>> = ({
noShadow,
children,
noMinHeight,
className,
onClick,
...props
}) => {
return <div className={clsx('toolbar', noShadow && 'no-shadow', noMinHeight && 'no-min-height', className)} onClick={onClick}>{children}</div>;
return <div className={clsx('toolbar', noShadow && 'no-shadow', noMinHeight && 'no-min-height', className)} {...props}>{children}</div>;
};

View file

@ -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');

View file

@ -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;

View file

@ -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'),