mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-05 19:04:43 +03:00
feat(trace): allow navigating from error to source (#27464)
This commit is contained in:
parent
cba2fc0752
commit
70dbb9d83a
@ -33,7 +33,7 @@ export type ConfigCLIOverrides = {
|
||||
timeout?: number;
|
||||
ignoreSnapshots?: boolean;
|
||||
updateSnapshots?: 'all'|'none'|'missing';
|
||||
workers?: number;
|
||||
workers?: number | string;
|
||||
projects?: { name: string, use?: any }[],
|
||||
use?: any;
|
||||
};
|
||||
|
@ -15,12 +15,24 @@
|
||||
*/
|
||||
|
||||
.action-title {
|
||||
flex: auto;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.action-location {
|
||||
display: flex;
|
||||
flex: none;
|
||||
margin: 0 4px;
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.action-location > span {
|
||||
margin: 0 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.action-duration {
|
||||
display: flex;
|
||||
flex: none;
|
||||
|
@ -72,17 +72,21 @@ export const ActionList: React.FC<ActionListProps> = ({
|
||||
onAccepted={item => setSelectedTime({ minimum: item.action!.startTime, maximum: item.action!.endTime })}
|
||||
isError={item => !!item.action?.error?.message}
|
||||
isVisible={item => !selectedTime || (item.action!.startTime <= selectedTime.maximum && item.action!.endTime >= selectedTime.minimum)}
|
||||
render={item => renderAction(item.action!, sdkLanguage, revealConsole, isLive || false)}
|
||||
render={item => renderAction(item.action!, { sdkLanguage, revealConsole, isLive, showDuration: true, showBadges: true })}
|
||||
/>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const renderAction = (
|
||||
action: ActionTraceEvent,
|
||||
sdkLanguage?: Language,
|
||||
revealConsole?: () => void,
|
||||
isLive?: boolean,
|
||||
) => {
|
||||
options: {
|
||||
sdkLanguage?: Language,
|
||||
revealConsole?: () => void,
|
||||
isLive?: boolean,
|
||||
showDuration?: boolean,
|
||||
showBadges?: boolean,
|
||||
}) => {
|
||||
const { sdkLanguage, revealConsole, isLive, showDuration, showBadges } = options;
|
||||
const { errors, warnings } = modelUtil.stats(action);
|
||||
const locator = action.params.selector ? asLocator(sdkLanguage || 'javascript', action.params.selector, false /* isFrameLocator */, true /* playSafe */) : undefined;
|
||||
|
||||
@ -99,10 +103,11 @@ export const renderAction = (
|
||||
{locator && <div className='action-selector' title={locator}>{locator}</div>}
|
||||
{action.method === 'goto' && action.params.url && <div className='action-url' title={action.params.url}>{action.params.url}</div>}
|
||||
</div>
|
||||
<div className='action-duration' style={{ flex: 'none' }}>{time || <span className='codicon codicon-loading'></span>}</div>
|
||||
<div className='action-icons' onClick={() => revealConsole?.()}>
|
||||
{(showDuration || showBadges) && <div className='spacer'></div>}
|
||||
{showDuration && <div className='action-duration'>{time || <span className='codicon codicon-loading'></span>}</div>}
|
||||
{showBadges && <div className='action-icons' onClick={() => revealConsole?.()}>
|
||||
{!!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>
|
||||
</div>}
|
||||
</>;
|
||||
};
|
||||
|
@ -20,8 +20,6 @@ import type * as modelUtil from './modelUtil';
|
||||
import { PlaceholderPanel } from './placeholderPanel';
|
||||
import { renderAction } from './actionList';
|
||||
import type { Language } from '@isomorphic/locatorGenerators';
|
||||
import type { Boundaries } from '../geometry';
|
||||
import { msToString } from '@web/uiUtils';
|
||||
|
||||
type ErrorsTabModel = {
|
||||
errors: Map<string, modelUtil.ActionTraceEventInContext>;
|
||||
@ -42,17 +40,32 @@ export function useErrorsTabModel(model: modelUtil.MultiTraceModel | undefined):
|
||||
export const ErrorsTab: React.FunctionComponent<{
|
||||
errorsModel: ErrorsTabModel,
|
||||
sdkLanguage: Language,
|
||||
boundaries: Boundaries,
|
||||
}> = ({ errorsModel, sdkLanguage, boundaries }) => {
|
||||
revealInSource: (action: modelUtil.ActionTraceEventInContext) => void,
|
||||
}> = ({ errorsModel, sdkLanguage, revealInSource }) => {
|
||||
if (!errorsModel.errors.size)
|
||||
return <PlaceholderPanel text='No errors' />;
|
||||
|
||||
return <div className='fill' style={{ overflow: 'auto ' }}>
|
||||
return <div className='fill' style={{ overflow: 'auto' }}>
|
||||
{[...errorsModel.errors.entries()].map(([message, action]) => {
|
||||
let location: string | undefined;
|
||||
let longLocation: string | undefined;
|
||||
if (action.stack?.[0]) {
|
||||
const file = action.stack[0].file.replace(/.*\/(.*)/, '$1');
|
||||
location = file + ':' + action.stack[0].line;
|
||||
longLocation = action.stack[0].file + ':' + action.stack[0].line;
|
||||
}
|
||||
return <div key={message}>
|
||||
<div className='hbox' style={{ alignItems: 'center', padding: 5 }}>
|
||||
<div style={{ color: 'var(--vscode-editorCodeLens-foreground)', marginRight: 5 }}>{msToString(action.startTime - boundaries.minimum)}</div>
|
||||
{renderAction(action, sdkLanguage)}
|
||||
<div className='hbox' style={{
|
||||
alignItems: 'center',
|
||||
padding: '5px 10px',
|
||||
minHeight: 36,
|
||||
fontWeight: 'bold',
|
||||
color: 'var(--vscode-errorForeground)',
|
||||
}}>
|
||||
{renderAction(action, { sdkLanguage })}
|
||||
{location && <div className='action-location'>
|
||||
@ <span title={longLocation} onClick={() => revealInSource(action)}>{location}</span>
|
||||
</div>}
|
||||
</div>
|
||||
<ErrorMessage error={message} />
|
||||
</div>;
|
||||
|
@ -55,8 +55,8 @@ export const FilmStrip: React.FunctionComponent<{
|
||||
const previewTime = boundaries.minimum + (boundaries.maximum - boundaries.minimum) * previewPoint.x / measure.width;
|
||||
previewImage = screencastFrames[upperBound(screencastFrames, previewTime, timeComparator) - 1];
|
||||
const fitInto = {
|
||||
width: Math.min(500, (window.innerWidth / 2) | 0),
|
||||
height: Math.min(500, (window.innerHeight / 2) | 0),
|
||||
width: Math.min(800, (window.innerWidth / 2) | 0),
|
||||
height: Math.min(800, (window.innerHeight / 2) | 0),
|
||||
};
|
||||
previewSize = previewImage ? inscribe({ width: previewImage.width, height: previewImage.height }, fitInto) : undefined;
|
||||
}
|
||||
@ -75,7 +75,7 @@ export const FilmStrip: React.FunctionComponent<{
|
||||
top: measure.bottom + 5,
|
||||
left: Math.min(previewPoint!.x, measure.width - (previewSize ? previewSize.width : 0) - 10),
|
||||
}}>
|
||||
{previewPoint.action && <div className='film-strip-hover-title'>{renderAction(previewPoint.action, previewPoint.sdkLanguage)}</div>}
|
||||
{previewPoint.action && <div className='film-strip-hover-title'>{renderAction(previewPoint.action, previewPoint)}</div>}
|
||||
{previewImage && previewSize && <div style={{ width: previewSize.width, height: previewSize.height }}>
|
||||
<img src={`sha1/${previewImage.sha1}`} width={previewSize.width} height={previewSize.height} />
|
||||
</div>}
|
||||
|
@ -124,7 +124,10 @@ export const Workbench: React.FunctionComponent<{
|
||||
id: 'errors',
|
||||
title: 'Errors',
|
||||
errorCount: errorsModel.errors.size,
|
||||
render: () => <ErrorsTab errorsModel={errorsModel} sdkLanguage={sdkLanguage} boundaries={boundaries} />
|
||||
render: () => <ErrorsTab errorsModel={errorsModel} sdkLanguage={sdkLanguage} revealInSource={action => {
|
||||
setSelectedAction(action);
|
||||
selectPropertiesTab('source');
|
||||
}} />
|
||||
};
|
||||
const sourceTab: TabbedPaneTabModel = {
|
||||
id: 'source',
|
||||
|
@ -18,7 +18,6 @@
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-weight: var(--vscode-editor-font-weight);
|
||||
font-size: var(--vscode-editor-font-size);
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
padding: 10px;
|
||||
|
@ -822,16 +822,16 @@ test('should chain expect matchers and expose matcher utils', async ({ runInline
|
||||
|
||||
const log = callLogText(matcherResult?.log);
|
||||
const message = pass
|
||||
? () => this.utils.matcherHint('toBe', locator, expected, expectOptions) +
|
||||
? () => this.utils.matcherHint('toHaveAmount', undefined, undefined, expectOptions) +
|
||||
'\\n\\n' +
|
||||
\`Expected: \${this.isNot ? 'not' : ''}\${this.utils.printExpected(expected)}\\n\` +
|
||||
(matcherResult ? \`Received: \${this.utils.printReceived(matcherResult.actual)}\` : '') +
|
||||
log
|
||||
: () => this.utils.matcherHint('toBe', locator, expected, expectOptions) +
|
||||
'\\n\\n' +log
|
||||
: () => this.utils.matcherHint('toHaveAmount', undefined, undefined, expectOptions) +
|
||||
'\\n\\n' +
|
||||
\`Expected: \${this.utils.printExpected(expected)}\n\` +
|
||||
(matcherResult ? \`Received: \${this.utils.printReceived(matcherResult.actual)}\` : '') +
|
||||
log;
|
||||
'\\n\\n' +log;
|
||||
|
||||
return {
|
||||
name: 'toHaveAmount',
|
||||
|
Loading…
Reference in New Issue
Block a user