mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: use listview to render stack trace (#21197)
This commit is contained in:
parent
3fa19e80ad
commit
ed41fd0643
@ -87,7 +87,7 @@ export class TraceModel {
|
|||||||
await stacksEntry.getData!(writer);
|
await stacksEntry.getData!(writer);
|
||||||
const metadataMap = parseClientSideCallMetadata(JSON.parse(await writer.getData()));
|
const metadataMap = parseClientSideCallMetadata(JSON.parse(await writer.getData()));
|
||||||
for (const action of this.contextEntry.actions)
|
for (const action of this.contextEntry.actions)
|
||||||
action.metadata.stack = metadataMap.get(action.metadata.id);
|
action.metadata.stack = action.metadata.stack || metadataMap.get(action.metadata.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._build();
|
this._build();
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export const SourceTab: React.FunctionComponent<{
|
|||||||
}
|
}
|
||||||
}, [needReveal, targetLineRef]);
|
}, [needReveal, targetLineRef]);
|
||||||
|
|
||||||
return <SplitView sidebarSize={100} orientation='vertical'>
|
return <SplitView sidebarSize={200} orientation='horizontal'>
|
||||||
<SourceView text={content} language='javascript' highlight={[{ line: targetLine, type: 'running' }]} revealLine={targetLine}></SourceView>
|
<SourceView text={content} language='javascript' highlight={[{ line: targetLine, type: 'running' }]} revealLine={targetLine}></SourceView>
|
||||||
<StackTraceView action={action} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame}></StackTraceView>
|
<StackTraceView action={action} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame}></StackTraceView>
|
||||||
</SplitView>;
|
</SplitView>;
|
||||||
|
|||||||
@ -14,32 +14,6 @@
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.stack-trace {
|
|
||||||
flex: 1 1 120px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stack-trace-frame {
|
|
||||||
flex: 0 0 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stack-trace-frame:hover {
|
|
||||||
background-color: var(--vscode-list-inactiveSelectionBackground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stack-trace-frame:selected {
|
|
||||||
background-color: var(--vscode-list-activeSelectionBackground);
|
|
||||||
color: var(--vscode-list-activeSelectionForeground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stack-trace-frame-function {
|
.stack-trace-frame-function {
|
||||||
flex: 1 1 100px;
|
flex: 1 1 100px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './stackTrace.css';
|
import './stackTrace.css';
|
||||||
import type { ActionTraceEvent } from '@trace/trace';
|
import type { ActionTraceEvent } from '@trace/trace';
|
||||||
|
import { ListView } from '@web/components/listView';
|
||||||
|
|
||||||
export const StackTraceView: React.FunctionComponent<{
|
export const StackTraceView: React.FunctionComponent<{
|
||||||
action: ActionTraceEvent | undefined,
|
action: ActionTraceEvent | undefined,
|
||||||
@ -24,17 +25,13 @@ export const StackTraceView: React.FunctionComponent<{
|
|||||||
setSelectedFrame: (index: number) => void
|
setSelectedFrame: (index: number) => void
|
||||||
}> = ({ action, setSelectedFrame, selectedFrame }) => {
|
}> = ({ action, setSelectedFrame, selectedFrame }) => {
|
||||||
const frames = action?.metadata.stack || [];
|
const frames = action?.metadata.stack || [];
|
||||||
return <div className='stack-trace'>{
|
return <ListView
|
||||||
frames.map((frame, index) => {
|
dataTestId='stack-trace'
|
||||||
// Windows frames are E:\path\to\file
|
items={frames}
|
||||||
|
selectedItem={frames[selectedFrame]}
|
||||||
|
itemRender={frame => {
|
||||||
const pathSep = frame.file[1] === ':' ? '\\' : '/';
|
const pathSep = frame.file[1] === ':' ? '\\' : '/';
|
||||||
return <div
|
return <>
|
||||||
key={index}
|
|
||||||
className={'stack-trace-frame' + (selectedFrame === index ? ' selected' : '')}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedFrame(index);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className='stack-trace-frame-function'>
|
<span className='stack-trace-frame-function'>
|
||||||
{frame.function || '(anonymous)'}
|
{frame.function || '(anonymous)'}
|
||||||
</span>
|
</span>
|
||||||
@ -44,8 +41,7 @@ export const StackTraceView: React.FunctionComponent<{
|
|||||||
<span className='stack-trace-frame-line'>
|
<span className='stack-trace-frame-line'>
|
||||||
{':' + frame.line}
|
{':' + frame.line}
|
||||||
</span>
|
</span>
|
||||||
</div>;
|
</>;
|
||||||
})
|
}}
|
||||||
}
|
onSelected={frame => setSelectedFrame(frames.indexOf(frame))} />;
|
||||||
</div>;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -207,7 +207,7 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
|
<SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
|
||||||
<SplitView sidebarSize={300} orientation={view === 'embedded' ? 'vertical' : 'horizontal'}>
|
<SplitView sidebarSize={300} orientation='vertical'>
|
||||||
<SnapshotTab action={activeAction} sdkLanguage={model.sdkLanguage || 'javascript'} testIdAttributeName={model.testIdAttributeName || 'data-testid'} />
|
<SnapshotTab action={activeAction} sdkLanguage={model.sdkLanguage || 'javascript'} testIdAttributeName={model.testIdAttributeName || 'data-testid'} />
|
||||||
<TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab}/>
|
<TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab}/>
|
||||||
</SplitView>
|
</SplitView>
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
outline: none;
|
outline: 1 px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-view-entry {
|
.list-view-entry {
|
||||||
@ -39,18 +39,22 @@
|
|||||||
background-color: var(--vscode-list-inactiveSelectionBackground);
|
background-color: var(--vscode-list-inactiveSelectionBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-view-entry.selected {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
.list-view-entry.highlighted {
|
.list-view-entry.highlighted {
|
||||||
background-color: var(--vscode-list-inactiveSelectionBackground);
|
background-color: var(--vscode-list-inactiveSelectionBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-view-content:focus .list-view-entry.selected {
|
.list-view-content:focus .list-view-entry.selected:not(.error) {
|
||||||
background-color: var(--vscode-list-activeSelectionBackground);
|
background-color: var(--vscode-list-activeSelectionBackground);
|
||||||
color: var(--vscode-list-activeSelectionForeground);
|
color: var(--vscode-list-activeSelectionForeground);
|
||||||
outline: 1px solid var(--vscode-focusBorder);
|
outline: 1px solid var(--vscode-focusBorder);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-view-content:focus .list-view-entry.selected * {
|
.list-view-content:focus .list-view-entry.error.selected {
|
||||||
color: var(--vscode-list-activeSelectionForeground);
|
outline: 1px solid var(--vscode-inputValidation-errorBorder);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-view-empty {
|
.list-view-empty {
|
||||||
@ -60,8 +64,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-view-entry.error:not(.selected) {
|
.list-view-entry.error {
|
||||||
color: var(--vscode-list-errorForeground);
|
color: var(--vscode-list-errorForeground);
|
||||||
background-color: var(--vscode-inputValidation-errorBackground);
|
background-color: var(--vscode-inputValidation-errorBackground);
|
||||||
outline: 1px solid var(--vscode-inputValidation-errorBorder);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,16 +19,17 @@ import './listView.css';
|
|||||||
|
|
||||||
export type ListViewProps = {
|
export type ListViewProps = {
|
||||||
items: any[],
|
items: any[],
|
||||||
itemKey: (item: any) => string,
|
|
||||||
itemRender: (item: any) => React.ReactNode,
|
itemRender: (item: any) => React.ReactNode,
|
||||||
|
itemKey?: (item: any) => string,
|
||||||
itemIcon?: (item: any) => string | undefined,
|
itemIcon?: (item: any) => string | undefined,
|
||||||
itemIndent?: (item: any) => number | undefined,
|
itemIndent?: (item: any) => number | undefined,
|
||||||
itemType: (item: any) => 'error' | undefined,
|
itemType?: (item: any) => 'error' | undefined,
|
||||||
selectedItem?: any,
|
selectedItem?: any,
|
||||||
onAccepted?: (item: any) => void,
|
onAccepted?: (item: any) => void,
|
||||||
onSelected?: (item: any) => void,
|
onSelected?: (item: any) => void,
|
||||||
onHighlighted?: (item: any | undefined) => void,
|
onHighlighted?: (item: any | undefined) => void,
|
||||||
showNoItemsMessage?: boolean,
|
showNoItemsMessage?: boolean,
|
||||||
|
dataTestId?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ListView: React.FC<ListViewProps> = ({
|
export const ListView: React.FC<ListViewProps> = ({
|
||||||
@ -43,11 +44,12 @@ export const ListView: React.FC<ListViewProps> = ({
|
|||||||
onSelected,
|
onSelected,
|
||||||
onHighlighted,
|
onHighlighted,
|
||||||
showNoItemsMessage,
|
showNoItemsMessage,
|
||||||
|
dataTestId,
|
||||||
}) => {
|
}) => {
|
||||||
const itemListRef = React.createRef<HTMLDivElement>();
|
const itemListRef = React.createRef<HTMLDivElement>();
|
||||||
const [highlightedItem, setHighlightedItem] = React.useState<any>();
|
const [highlightedItem, setHighlightedItem] = React.useState<any>();
|
||||||
|
|
||||||
return <div className='list-view vbox'>
|
return <div className='list-view vbox' data-testid={dataTestId}>
|
||||||
<div
|
<div
|
||||||
className='list-view-content'
|
className='list-view-content'
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@ -83,8 +85,9 @@ export const ListView: React.FC<ListViewProps> = ({
|
|||||||
ref={itemListRef}
|
ref={itemListRef}
|
||||||
>
|
>
|
||||||
{showNoItemsMessage && items.length === 0 && <div className='list-view-empty'>No items</div>}
|
{showNoItemsMessage && items.length === 0 && <div className='list-view-empty'>No items</div>}
|
||||||
{items.map(item => <ListItemView
|
{items.map((item, index) => <ListItemView
|
||||||
key={itemKey(item)}
|
key={itemKey ? itemKey(item) : String(index)}
|
||||||
|
hasIcons={!!itemIcon}
|
||||||
icon={itemIcon?.(item)}
|
icon={itemIcon?.(item)}
|
||||||
type={itemType?.(item)}
|
type={itemType?.(item)}
|
||||||
indent={itemIndent?.(item)}
|
indent={itemIndent?.(item)}
|
||||||
@ -108,6 +111,7 @@ export const ListView: React.FC<ListViewProps> = ({
|
|||||||
|
|
||||||
const ListItemView: React.FC<{
|
const ListItemView: React.FC<{
|
||||||
key: string,
|
key: string,
|
||||||
|
hasIcons: boolean,
|
||||||
icon: string | undefined,
|
icon: string | undefined,
|
||||||
type: 'error' | undefined,
|
type: 'error' | undefined,
|
||||||
indent: number | undefined,
|
indent: number | undefined,
|
||||||
@ -117,7 +121,7 @@ const ListItemView: React.FC<{
|
|||||||
onMouseEnter: () => void,
|
onMouseEnter: () => void,
|
||||||
onMouseLeave: () => void,
|
onMouseLeave: () => void,
|
||||||
children: React.ReactNode | React.ReactNode[],
|
children: React.ReactNode | React.ReactNode[],
|
||||||
}> = ({ key, icon, type, indent, onSelected, onMouseEnter, onMouseLeave, isHighlighted, isSelected, children }) => {
|
}> = ({ key, hasIcons, icon, type, indent, onSelected, onMouseEnter, onMouseLeave, isHighlighted, isSelected, children }) => {
|
||||||
const selectedSuffix = isSelected ? ' selected' : '';
|
const selectedSuffix = isSelected ? ' selected' : '';
|
||||||
const highlightedSuffix = isHighlighted ? ' highlighted' : '';
|
const highlightedSuffix = isHighlighted ? ' highlighted' : '';
|
||||||
const errorSuffix = type === 'error' ? ' error' : '';
|
const errorSuffix = type === 'error' ? ' error' : '';
|
||||||
@ -137,7 +141,7 @@ const ListItemView: React.FC<{
|
|||||||
ref={divRef}
|
ref={divRef}
|
||||||
>
|
>
|
||||||
{indent ? <div style={{ minWidth: indent * 16 }}></div> : undefined}
|
{indent ? <div style={{ minWidth: indent * 16 }}></div> : undefined}
|
||||||
<div className={'codicon ' + (icon || 'blank')} style={{ minWidth: 16, marginRight: 4 }}></div>
|
{hasIcons && <div className={'codicon ' + (icon || 'blank')} style={{ minWidth: 16, marginRight: 4 }}></div>}
|
||||||
{typeof children === 'string' ? <div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{children}</div> : children}
|
{typeof children === 'string' ? <div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{children}</div> : children}
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -49,7 +49,7 @@ class TraceViewerPage {
|
|||||||
this.consoleLines = page.locator('.console-line');
|
this.consoleLines = page.locator('.console-line');
|
||||||
this.consoleLineMessages = page.locator('.console-line-message');
|
this.consoleLineMessages = page.locator('.console-line-message');
|
||||||
this.consoleStacks = page.locator('.console-stack');
|
this.consoleStacks = page.locator('.console-stack');
|
||||||
this.stackFrames = page.locator('.stack-trace-frame');
|
this.stackFrames = page.getByTestId('stack-trace').locator('.list-view-entry');
|
||||||
this.networkRequests = page.locator('.network-request-title');
|
this.networkRequests = page.locator('.network-request-title');
|
||||||
this.snapshotContainer = page.locator('.snapshot-container iframe');
|
this.snapshotContainer = page.locator('.snapshot-container iframe');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -601,7 +601,7 @@ test('should show action source', async ({ showTraceViewer }) => {
|
|||||||
|
|
||||||
await page.click('text=Source');
|
await page.click('text=Source');
|
||||||
await expect(page.locator('.source-line-running')).toContainText('await page.getByText(\'Click\').click()');
|
await expect(page.locator('.source-line-running')).toContainText('await page.getByText(\'Click\').click()');
|
||||||
await expect(page.locator('.stack-trace-frame.selected')).toHaveText(/doClick.*trace-viewer\.spec\.ts:[\d]+/);
|
await expect(page.getByTestId('stack-trace').locator('.list-view-entry.selected')).toHaveText(/doClick.*trace-viewer\.spec\.ts:[\d]+/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should follow redirects', async ({ page, runAndTrace, server, asset }) => {
|
test('should follow redirects', async ({ page, runAndTrace, server, asset }) => {
|
||||||
|
|||||||
@ -394,10 +394,10 @@ test('should show trace source', async ({ runInlineTest, page, showReport }) =>
|
|||||||
]);
|
]);
|
||||||
await expect(page.locator('.source-line-running')).toContainText('page.evaluate');
|
await expect(page.locator('.source-line-running')).toContainText('page.evaluate');
|
||||||
|
|
||||||
await expect(page.locator('.stack-trace-frame')).toContainText([
|
await expect(page.getByTestId('stack-trace')).toContainText([
|
||||||
/a.test.js:[\d]+/,
|
/a.test.js:[\d]+/,
|
||||||
]);
|
]);
|
||||||
await expect(page.locator('.stack-trace-frame.selected')).toContainText('a.test.js');
|
await expect(page.getByTestId('stack-trace').locator('.list-view-entry.selected')).toContainText('a.test.js');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show trace title', async ({ runInlineTest, page, showReport }) => {
|
test('should show trace title', async ({ runInlineTest, page, showReport }) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user