feat(ui-mode): show step breadcrumbs in Trace Viewer
This commit is contained in:
parent
d0c840f639
commit
8663a6dade
|
|
@ -24,6 +24,7 @@ import type { StackFrame } from '@protocol/channels';
|
||||||
const contextSymbol = Symbol('context');
|
const contextSymbol = Symbol('context');
|
||||||
const nextInContextSymbol = Symbol('next');
|
const nextInContextSymbol = Symbol('next');
|
||||||
const prevInListSymbol = Symbol('prev');
|
const prevInListSymbol = Symbol('prev');
|
||||||
|
const parentActionSymbol = Symbol('parent');
|
||||||
const eventsSymbol = Symbol('events');
|
const eventsSymbol = Symbol('events');
|
||||||
|
|
||||||
export type SourceLocation = {
|
export type SourceLocation = {
|
||||||
|
|
@ -195,8 +196,11 @@ function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) {
|
||||||
return a1.startTime - a2.startTime;
|
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];
|
(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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -356,6 +360,10 @@ export function prevInList(action: ActionTraceEvent): ActionTraceEvent {
|
||||||
return (action as any)[prevInListSymbol];
|
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 } {
|
export function stats(action: ActionTraceEvent): { errors: number, warnings: number } {
|
||||||
let errors = 0;
|
let errors = 0;
|
||||||
let warnings = 0;
|
let warnings = 0;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
import './snapshotTab.css';
|
import './snapshotTab.css';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { ActionTraceEvent } from '@trace/trace';
|
import type { ActionTraceEvent } from '@trace/trace';
|
||||||
import { context, prevInList } from './modelUtil';
|
import { context, parentAction, prevInList } from './modelUtil';
|
||||||
import { Toolbar } from '@web/components/toolbar';
|
import { Toolbar } from '@web/components/toolbar';
|
||||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
import { clsx, useMeasure } from '@web/uiUtils';
|
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.
|
// if the action has no beforeSnapshot, use the last available afterSnapshot.
|
||||||
let beforeSnapshot: Snapshot | undefined = action.beforeSnapshot ? { action, snapshotName: action.beforeSnapshot } : undefined;
|
let beforeSnapshot: Snapshot | undefined = action.beforeSnapshot ? { action, snapshotName: action.beforeSnapshot } : undefined;
|
||||||
let a = action;
|
let a: ActionTraceEvent | undefined = action;
|
||||||
while (!beforeSnapshot && a) {
|
while (!beforeSnapshot && a) {
|
||||||
a = prevInList(a);
|
a = prevInList(a);
|
||||||
beforeSnapshot = a?.afterSnapshot ? { action: a, snapshotName: a?.afterSnapshot } : undefined;
|
beforeSnapshot = a?.afterSnapshot ? { action: a, snapshotName: a?.afterSnapshot } : undefined;
|
||||||
|
|
@ -180,27 +180,51 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
setHighlightedLocator={setHighlightedLocator}
|
setHighlightedLocator={setHighlightedLocator}
|
||||||
iframe={iframeRef1.current}
|
iframe={iframeRef1.current}
|
||||||
iteration={loadingRef.current.iteration} />
|
iteration={loadingRef.current.iteration} />
|
||||||
<Toolbar>
|
<div className='hbox' style={{ flex: '0 0 auto' }}>
|
||||||
<ToolbarButton className='pick-locator' title='Pick locator' icon='target' toggled={isInspecting} onClick={() => setIsInspecting(!isInspecting)} />
|
<Toolbar style={{
|
||||||
{['action', 'before', 'after'].map(tab => {
|
flex: '0 1 0',
|
||||||
return <TabbedPaneTab
|
minWidth: 'min-content',
|
||||||
id={tab}
|
flexGrow: 1
|
||||||
title={renderTitle(tab)}
|
}}>
|
||||||
selected={snapshotTab === tab}
|
<ToolbarButton className='pick-locator' title='Pick locator' icon='target' toggled={isInspecting} onClick={() => setIsInspecting(!isInspecting)} />
|
||||||
onSelect={() => setSnapshotTab(tab as 'action' | 'before' | 'after')}
|
{['action', 'before', 'after'].map(tab => {
|
||||||
></TabbedPaneTab>;
|
return <TabbedPaneTab
|
||||||
})}
|
id={tab}
|
||||||
<div style={{ flex: 'auto' }}></div>
|
title={renderTitle(tab)}
|
||||||
<ToolbarButton icon='link-external' title='Open snapshot in a new tab' disabled={!popoutUrl} onClick={() => {
|
selected={snapshotTab === tab}
|
||||||
if (!openPage)
|
onSelect={() => setSnapshotTab(tab as 'action' | 'before' | 'after')}
|
||||||
openPage = window.open;
|
></TabbedPaneTab>;
|
||||||
const win = openPage(popoutUrl || '', '_blank');
|
})}
|
||||||
win?.addEventListener('DOMContentLoaded', () => {
|
</Toolbar>
|
||||||
const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []);
|
<Toolbar
|
||||||
new ConsoleAPI(injectedScript);
|
style={{
|
||||||
});
|
whiteSpace: 'nowrap',
|
||||||
}}></ToolbarButton>
|
overflow: 'hidden',
|
||||||
</Toolbar>
|
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;
|
||||||
|
const win = openPage(popoutUrl || '', '_blank');
|
||||||
|
win?.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []);
|
||||||
|
new ConsoleAPI(injectedScript);
|
||||||
|
});
|
||||||
|
}}></ToolbarButton>
|
||||||
|
</Toolbar>
|
||||||
|
</div>
|
||||||
<div ref={ref} className='snapshot-wrapper'>
|
<div ref={ref} className='snapshot-wrapper'>
|
||||||
<div className='snapshot-container' style={{
|
<div className='snapshot-container' style={{
|
||||||
width: snapshotContainerSize.width + 'px',
|
width: snapshotContainerSize.width + 'px',
|
||||||
|
|
@ -217,6 +241,17 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
</div>;
|
</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 {
|
function renderTitle(snapshotTitle: string): string {
|
||||||
if (snapshotTitle === 'before')
|
if (snapshotTitle === 'before')
|
||||||
return 'Before';
|
return 'Before';
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,14 @@ type ToolbarProps = {
|
||||||
noShadow?: boolean;
|
noShadow?: boolean;
|
||||||
noMinHeight?: boolean;
|
noMinHeight?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: (e: React.MouseEvent) => void;
|
} & React.HTMLAttributes<HTMLDivElement>;
|
||||||
};
|
|
||||||
|
|
||||||
export const Toolbar: React.FC<React.PropsWithChildren<ToolbarProps>> = ({
|
export const Toolbar: React.FC<React.PropsWithChildren<ToolbarProps>> = ({
|
||||||
noShadow,
|
noShadow,
|
||||||
children,
|
children,
|
||||||
noMinHeight,
|
noMinHeight,
|
||||||
className,
|
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>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -289,6 +289,14 @@ test('should show snapshot URL', async ({ page, runAndTrace, server }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should popup snapshot', 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 () => {
|
const traceViewer = await runAndTrace(async () => {
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
await page.setContent('hello');
|
await page.setContent('hello');
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ test('should contain string attachment', async ({ runUITest }) => {
|
||||||
await page.getByTitle('Run all').click();
|
await page.getByTitle('Run all').click();
|
||||||
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
||||||
await page.getByText('Attachments').click();
|
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');
|
const downloadPromise = page.waitForEvent('download');
|
||||||
await page.locator('.expandable-title', { hasText: 'note' }).getByRole('link').click();
|
await page.locator('.expandable-title', { hasText: 'note' }).getByRole('link').click();
|
||||||
const download = await downloadPromise;
|
const download = await downloadPromise;
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ test('should locate sync assertions in source', async ({ runUITest }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByText('trace test').dblclick();
|
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(
|
await expect(
|
||||||
page.locator('.CodeMirror .source-line-running'),
|
page.locator('.CodeMirror .source-line-running'),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue