chore: trace viewer UX (auto scroll to action + timeline duration) (#20001)

Fixes https://github.com/microsoft/playwright/issues/19916
This commit is contained in:
Max Schmitt 2023-01-10 18:33:20 +01:00 committed by GitHub
parent 0fe327c21b
commit 3918e33c91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 68 additions and 31 deletions

View File

@ -72,42 +72,75 @@ export const ActionList: React.FC<ActionListProps> = ({
newIndex = Math.max(index - 1, 0);
}
const element = actionListRef.current?.children.item(newIndex);
if ((element as any)?.scrollIntoViewIfNeeded)
(element as any).scrollIntoViewIfNeeded(false);
else
element?.scrollIntoView();
scrollIntoViewIfNeeded(element);
onSelected(actions[newIndex]);
}}
ref={actionListRef}
>
{actions.length === 0 && <div className='no-actions-entry'>No actions recorded</div>}
{actions.map(action => {
const { metadata } = action;
const selectedSuffix = action === selectedAction ? ' selected' : '';
const highlightedSuffix = action === highlightedAction ? ' highlighted' : '';
const error = metadata.error?.error?.message;
const { errors, warnings } = modelUtil.stats(action);
const locator = metadata.params.selector ? asLocator(sdkLanguage || 'javascript', metadata.params.selector) : undefined;
return <div
className={'action-entry' + selectedSuffix + highlightedSuffix}
key={metadata.id}
onClick={() => onSelected(action)}
onMouseEnter={() => onHighlighted(action)}
onMouseLeave={() => (highlightedAction === action) && onHighlighted(undefined)}
>
<div className='action-title'>
<span>{metadata.apiName}</span>
{locator && <div className='action-selector' title={locator}>{locator}</div>}
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
</div>
<div className='action-duration' style={{ flex: 'none' }}>{metadata.endTime ? msToString(metadata.endTime - metadata.startTime) : 'Timed Out'}</div>
<div className='action-icons' onClick={() => setSelectedTab('console')}>
{!!errors && <div className='action-icon'><span className={'codicon codicon-error'}></span><span className="action-icon-value">{errors}</span></div>}
{!!warnings && <div className='action-icon'><span className={'codicon codicon-warning'}></span><span className="action-icon-value">{warnings}</span></div>}
</div>
{error && <div className='codicon codicon-issues' title={error} />}
</div>;
})}
{actions.map(action => <ActionListItem
action={action}
highlightedAction={highlightedAction}
onSelected={onSelected}
onHighlighted={onHighlighted}
selectedAction={selectedAction}
sdkLanguage={sdkLanguage}
setSelectedTab={setSelectedTab}
/>)}
</div>
</div>;
};
const ActionListItem: React.FC<{
action: ActionTraceEvent,
highlightedAction: ActionTraceEvent | undefined,
onSelected: (action: ActionTraceEvent) => void,
onHighlighted: (action: ActionTraceEvent | undefined) => void,
selectedAction: ActionTraceEvent | undefined,
sdkLanguage: Language | undefined,
setSelectedTab: (tab: string) => void,
}> = ({ action, onSelected, onHighlighted, highlightedAction, selectedAction, sdkLanguage, setSelectedTab }) => {
const { metadata } = action;
const selectedSuffix = action === selectedAction ? ' selected' : '';
const highlightedSuffix = action === highlightedAction ? ' highlighted' : '';
const error = metadata.error?.error?.message;
const { errors, warnings } = modelUtil.stats(action);
const locator = metadata.params.selector ? asLocator(sdkLanguage || 'javascript', metadata.params.selector) : undefined;
const divRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (divRef.current && selectedAction === action)
scrollIntoViewIfNeeded(divRef.current);
}, [selectedAction, action]);
return <div
className={'action-entry' + selectedSuffix + highlightedSuffix}
key={metadata.id}
onClick={() => onSelected(action)}
onMouseEnter={() => onHighlighted(action)}
onMouseLeave={() => (highlightedAction === action) && onHighlighted(undefined)}
ref={divRef}
>
<div className='action-title'>
<span>{metadata.apiName}</span>
{locator && <div className='action-selector' title={locator}>{locator}</div>}
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
</div>
<div className='action-duration' style={{ flex: 'none' }}>{metadata.endTime ? msToString(metadata.endTime - metadata.startTime) : 'Timed Out'}</div>
<div className='action-icons' onClick={() => setSelectedTab('console')}>
{!!errors && <div className='action-icon'><span className={'codicon codicon-error'}></span><span className="action-icon-value">{errors}</span></div>}
{!!warnings && <div className='action-icon'><span className={'codicon codicon-warning'}></span><span className="action-icon-value">{warnings}</span></div>}
</div>
{error && <div className='codicon codicon-issues' title={error} />}
</div>;
};
function scrollIntoViewIfNeeded(element?: Element | null) {
if (!element)
return;
if ((element as any)?.scrollIntoViewIfNeeded)
(element as any).scrollIntoViewIfNeeded(false);
else
element?.scrollIntoView();
}

View File

@ -33,6 +33,7 @@ type TimelineBar = {
rightTime: number;
type: string;
label: string;
title: string;
className: string;
};
@ -67,6 +68,7 @@ export const Timeline: React.FunctionComponent<{
leftPosition: timeToPosition(measure.width, boundaries, entry.metadata.startTime),
rightPosition: timeToPosition(measure.width, boundaries, entry.metadata.endTime),
label: entry.metadata.apiName + ' ' + detail,
title: entry.metadata.endTime ? msToString(entry.metadata.endTime - entry.metadata.startTime) : 'Timed Out',
type: entry.metadata.type + '.' + entry.metadata.method,
className: `${entry.metadata.type}_${entry.metadata.method}`.toLowerCase()
});
@ -81,6 +83,7 @@ export const Timeline: React.FunctionComponent<{
leftPosition: timeToPosition(measure.width, boundaries, startTime),
rightPosition: timeToPosition(measure.width, boundaries, startTime),
label: event.metadata.method,
title: event.metadata.endTime ? msToString(event.metadata.endTime - event.metadata.startTime) : 'Timed Out',
type: event.metadata.type + '.' + event.metadata.method,
className: `${event.metadata.type}_${event.metadata.method}`.toLowerCase()
});
@ -183,6 +186,7 @@ export const Timeline: React.FunctionComponent<{
width: Math.max(1, bar.rightPosition - bar.leftPosition) + 'px',
top: barTop(bar) + 'px',
}}
title={bar.title}
></div>;
})
}</div>