mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
fix(trace viewer): reveal error location when it comes from the test (#29268)
Errors coming from the test runner do not have an associated `action`, but have a `stack` that we can reveal.
This commit is contained in:
parent
80189c9daf
commit
cf6549687c
@ -45,7 +45,7 @@ export function useErrorsTabModel(model: modelUtil.MultiTraceModel | undefined):
|
|||||||
export const ErrorsTab: React.FunctionComponent<{
|
export const ErrorsTab: React.FunctionComponent<{
|
||||||
errorsModel: ErrorsTabModel,
|
errorsModel: ErrorsTabModel,
|
||||||
sdkLanguage: Language,
|
sdkLanguage: Language,
|
||||||
revealInSource: (action: modelUtil.ActionTraceEventInContext) => void,
|
revealInSource: (error: ErrorDescription) => void,
|
||||||
}> = ({ errorsModel, sdkLanguage, revealInSource }) => {
|
}> = ({ errorsModel, sdkLanguage, revealInSource }) => {
|
||||||
if (!errorsModel.errors.size)
|
if (!errorsModel.errors.size)
|
||||||
return <PlaceholderPanel text='No errors' />;
|
return <PlaceholderPanel text='No errors' />;
|
||||||
@ -70,7 +70,7 @@ export const ErrorsTab: React.FunctionComponent<{
|
|||||||
}}>
|
}}>
|
||||||
{error.action && renderAction(error.action, { sdkLanguage })}
|
{error.action && renderAction(error.action, { sdkLanguage })}
|
||||||
{location && <div className='action-location'>
|
{location && <div className='action-location'>
|
||||||
@ <span title={longLocation} onClick={() => error.action && revealInSource(error.action)}>{location}</span>
|
@ <span title={longLocation} onClick={() => revealInSource(error)}>{location}</span>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
<ErrorMessage error={message} />
|
<ErrorMessage error={message} />
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ActionTraceEvent } from '@trace/trace';
|
|
||||||
import { SplitView } from '@web/components/splitView';
|
import { SplitView } from '@web/components/splitView';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useAsyncMemo } from '@web/uiUtils';
|
import { useAsyncMemo } from '@web/uiUtils';
|
||||||
@ -23,26 +22,27 @@ import { StackTraceView } from './stackTrace';
|
|||||||
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||||
import type { SourceHighlight } from '@web/components/codeMirrorWrapper';
|
import type { SourceHighlight } from '@web/components/codeMirrorWrapper';
|
||||||
import type { SourceLocation, SourceModel } from './modelUtil';
|
import type { SourceLocation, SourceModel } from './modelUtil';
|
||||||
|
import type { StackFrame } from '@protocol/channels';
|
||||||
|
|
||||||
export const SourceTab: React.FunctionComponent<{
|
export const SourceTab: React.FunctionComponent<{
|
||||||
action: ActionTraceEvent | undefined,
|
stack: StackFrame[] | undefined,
|
||||||
sources: Map<string, SourceModel>,
|
sources: Map<string, SourceModel>,
|
||||||
hideStackFrames?: boolean,
|
hideStackFrames?: boolean,
|
||||||
rootDir?: string,
|
rootDir?: string,
|
||||||
fallbackLocation?: SourceLocation,
|
fallbackLocation?: SourceLocation,
|
||||||
}> = ({ action, sources, hideStackFrames, rootDir, fallbackLocation }) => {
|
}> = ({ stack, sources, hideStackFrames, rootDir, fallbackLocation }) => {
|
||||||
const [lastAction, setLastAction] = React.useState<ActionTraceEvent | undefined>();
|
const [lastStack, setLastStack] = React.useState<StackFrame[] | undefined>();
|
||||||
const [selectedFrame, setSelectedFrame] = React.useState<number>(0);
|
const [selectedFrame, setSelectedFrame] = React.useState<number>(0);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (lastAction !== action) {
|
if (lastStack !== stack) {
|
||||||
setLastAction(action);
|
setLastStack(stack);
|
||||||
setSelectedFrame(0);
|
setSelectedFrame(0);
|
||||||
}
|
}
|
||||||
}, [action, lastAction, setLastAction, setSelectedFrame]);
|
}, [stack, lastStack, setLastStack, setSelectedFrame]);
|
||||||
|
|
||||||
const { source, highlight, targetLine, fileName } = useAsyncMemo<{ source: SourceModel, targetLine?: number, fileName?: string, highlight: SourceHighlight[] }>(async () => {
|
const { source, highlight, targetLine, fileName } = useAsyncMemo<{ source: SourceModel, targetLine?: number, fileName?: string, highlight: SourceHighlight[] }>(async () => {
|
||||||
const actionLocation = action?.stack?.[selectedFrame];
|
const actionLocation = stack?.[selectedFrame];
|
||||||
const shouldUseFallback = !actionLocation?.file;
|
const shouldUseFallback = !actionLocation?.file;
|
||||||
if (shouldUseFallback && !fallbackLocation)
|
if (shouldUseFallback && !fallbackLocation)
|
||||||
return { source: { file: '', errors: [], content: undefined }, targetLine: 0, highlight: [] };
|
return { source: { file: '', errors: [], content: undefined }, targetLine: 0, highlight: [] };
|
||||||
@ -76,14 +76,14 @@ export const SourceTab: React.FunctionComponent<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { source, highlight, targetLine, fileName };
|
return { source, highlight, targetLine, fileName };
|
||||||
}, [action, selectedFrame, rootDir, fallbackLocation], { source: { errors: [], content: 'Loading\u2026' }, highlight: [] });
|
}, [stack, selectedFrame, rootDir, fallbackLocation], { source: { errors: [], content: 'Loading\u2026' }, highlight: [] });
|
||||||
|
|
||||||
return <SplitView sidebarSize={200} orientation='horizontal' sidebarHidden={hideStackFrames}>
|
return <SplitView sidebarSize={200} orientation='horizontal' sidebarHidden={hideStackFrames}>
|
||||||
<div className='vbox' data-testid='source-code'>
|
<div className='vbox' data-testid='source-code'>
|
||||||
{fileName && <div className='source-tab-file-name'>{fileName}</div>}
|
{fileName && <div className='source-tab-file-name'>{fileName}</div>}
|
||||||
<CodeMirrorWrapper text={source.content || ''} language='javascript' highlight={highlight} revealLine={targetLine} readOnly={true} lineNumbers={true} />
|
<CodeMirrorWrapper text={source.content || ''} language='javascript' highlight={highlight} revealLine={targetLine} readOnly={true} lineNumbers={true} />
|
||||||
</div>
|
</div>
|
||||||
<StackTraceView action={action} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame} />
|
<StackTraceView stack={stack} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame} />
|
||||||
</SplitView>;
|
</SplitView>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -16,18 +16,17 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './stackTrace.css';
|
import './stackTrace.css';
|
||||||
import type { ActionTraceEvent } from '@trace/trace';
|
|
||||||
import { ListView } from '@web/components/listView';
|
import { ListView } from '@web/components/listView';
|
||||||
import type { StackFrame } from '@protocol/channels';
|
import type { StackFrame } from '@protocol/channels';
|
||||||
|
|
||||||
const StackFrameListView = ListView<StackFrame>;
|
const StackFrameListView = ListView<StackFrame>;
|
||||||
|
|
||||||
export const StackTraceView: React.FunctionComponent<{
|
export const StackTraceView: React.FunctionComponent<{
|
||||||
action: ActionTraceEvent | undefined,
|
stack: StackFrame[] | undefined,
|
||||||
selectedFrame: number,
|
selectedFrame: number,
|
||||||
setSelectedFrame: (index: number) => void
|
setSelectedFrame: (index: number) => void
|
||||||
}> = ({ action, setSelectedFrame, selectedFrame }) => {
|
}> = ({ stack, setSelectedFrame, selectedFrame }) => {
|
||||||
const frames = action?.stack || [];
|
const frames = stack || [];
|
||||||
return <StackFrameListView
|
return <StackFrameListView
|
||||||
name='stack-trace'
|
name='stack-trace'
|
||||||
items={frames}
|
items={frames}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { ErrorsTab, useErrorsTabModel } from './errorsTab';
|
|||||||
import { ConsoleTab, useConsoleTabModel } from './consoleTab';
|
import { ConsoleTab, useConsoleTabModel } from './consoleTab';
|
||||||
import type * as modelUtil from './modelUtil';
|
import type * as modelUtil from './modelUtil';
|
||||||
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
||||||
|
import type { StackFrame } from '@protocol/channels';
|
||||||
import { NetworkTab, useNetworkTabModel } from './networkTab';
|
import { NetworkTab, useNetworkTabModel } from './networkTab';
|
||||||
import { SnapshotTab } from './snapshotTab';
|
import { SnapshotTab } from './snapshotTab';
|
||||||
import { SourceTab } from './sourceTab';
|
import { SourceTab } from './sourceTab';
|
||||||
@ -51,7 +52,8 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
isLive?: boolean,
|
isLive?: boolean,
|
||||||
status?: UITestStatus,
|
status?: UITestStatus,
|
||||||
}> = ({ model, hideStackFrames, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status }) => {
|
}> = ({ model, hideStackFrames, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status }) => {
|
||||||
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEventInContext | undefined>(undefined);
|
const [selectedAction, setSelectedActionImpl] = React.useState<ActionTraceEventInContext | undefined>(undefined);
|
||||||
|
const [revealedStack, setRevealedStack] = React.useState<StackFrame[] | undefined>(undefined);
|
||||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
||||||
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
||||||
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
||||||
@ -62,6 +64,11 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>();
|
const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>();
|
||||||
const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom');
|
const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom');
|
||||||
|
|
||||||
|
const setSelectedAction = React.useCallback((action: ActionTraceEventInContext | undefined) => {
|
||||||
|
setSelectedActionImpl(action);
|
||||||
|
setRevealedStack(action?.stack);
|
||||||
|
}, [setSelectedActionImpl, setRevealedStack]);
|
||||||
|
|
||||||
const sources = React.useMemo(() => model?.sources || new Map(), [model]);
|
const sources = React.useMemo(() => model?.sources || new Map(), [model]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -137,8 +144,11 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
id: 'errors',
|
id: 'errors',
|
||||||
title: 'Errors',
|
title: 'Errors',
|
||||||
errorCount: errorsModel.errors.size,
|
errorCount: errorsModel.errors.size,
|
||||||
render: () => <ErrorsTab errorsModel={errorsModel} sdkLanguage={sdkLanguage} revealInSource={action => {
|
render: () => <ErrorsTab errorsModel={errorsModel} sdkLanguage={sdkLanguage} revealInSource={error => {
|
||||||
setSelectedAction(action);
|
if (error.action)
|
||||||
|
setSelectedAction(error.action);
|
||||||
|
else
|
||||||
|
setRevealedStack(error.stack);
|
||||||
selectPropertiesTab('source');
|
selectPropertiesTab('source');
|
||||||
}} />
|
}} />
|
||||||
};
|
};
|
||||||
@ -146,7 +156,7 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
id: 'source',
|
id: 'source',
|
||||||
title: 'Source',
|
title: 'Source',
|
||||||
render: () => <SourceTab
|
render: () => <SourceTab
|
||||||
action={activeAction}
|
stack={revealedStack}
|
||||||
sources={sources}
|
sources={sources}
|
||||||
hideStackFrames={hideStackFrames}
|
hideStackFrames={hideStackFrames}
|
||||||
rootDir={rootDir}
|
rootDir={rootDir}
|
||||||
|
|||||||
@ -258,3 +258,29 @@ test('should not show caught errors in the errors tab', async ({ runUITest }, te
|
|||||||
await page.getByText('Errors', { exact: true }).click();
|
await page.getByText('Errors', { exact: true }).click();
|
||||||
await expect(page.locator('.tab-errors')).toHaveText('No errors');
|
await expect(page.locator('.tab-errors')).toHaveText('No errors');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should reveal errors in the sourcetab', async ({ runUITest }) => {
|
||||||
|
const { page } = await runUITest({
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('pass', async ({ page }) => {
|
||||||
|
throw new Error('Oh my');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByText('pass').dblclick();
|
||||||
|
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
listItem,
|
||||||
|
'action list'
|
||||||
|
).toContainText([
|
||||||
|
/Before Hooks/,
|
||||||
|
/After Hooks/,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await page.getByText('Errors', { exact: true }).click();
|
||||||
|
await page.getByText('a.spec.ts:4', { exact: true }).click();
|
||||||
|
await expect(page.locator('.source-line-running')).toContainText(`throw new Error('Oh my');`);
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user