chore(ui): show output on demand (#21592)
This commit is contained in:
parent
428ea66578
commit
b85d670491
|
|
@ -24,7 +24,7 @@ export const MetadataView: React.FunctionComponent<{
|
||||||
}> = ({ model }) => {
|
}> = ({ model }) => {
|
||||||
if (!model)
|
if (!model)
|
||||||
return <></>;
|
return <></>;
|
||||||
return <div className='vbox'>
|
return <div className='metadata-view vbox'>
|
||||||
<div className='call-section' style={{ paddingTop: 2 }}>Time</div>
|
<div className='call-section' style={{ paddingTop: 2 }}>Time</div>
|
||||||
{!!model.wallTime && <div className='call-line'>start time:<span className='call-value datetime' title={new Date(model.wallTime).toLocaleString()}>{new Date(model.wallTime).toLocaleString()}</span></div>}
|
{!!model.wallTime && <div className='call-line'>start time:<span className='call-value datetime' title={new Date(model.wallTime).toLocaleString()}>{new Date(model.wallTime).toLocaleString()}</span></div>}
|
||||||
<div className='call-line'>duration:<span className='call-value number' title={msToString(model.endTime - model.startTime)}>{msToString(model.endTime - model.startTime)}</span></div>
|
<div className='call-line'>duration:<span className='call-value number' title={msToString(model.endTime - model.startTime)}>{msToString(model.endTime - model.startTime)}</span></div>
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.watch-mode-sidebar .section-title {
|
.watch-mode .section-title {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ export const WatchModeView: React.FC<{}> = ({
|
||||||
const [visibleTestIds, setVisibleTestIds] = React.useState<string[]>([]);
|
const [visibleTestIds, setVisibleTestIds] = React.useState<string[]>([]);
|
||||||
const [filterText, setFilterText] = React.useState<string>('');
|
const [filterText, setFilterText] = React.useState<string>('');
|
||||||
const [filterExpanded, setFilterExpanded] = React.useState<boolean>(false);
|
const [filterExpanded, setFilterExpanded] = React.useState<boolean>(false);
|
||||||
|
const [isShowingOutput, setIsShowingOutput] = React.useState<boolean>(false);
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -125,9 +126,24 @@ export const WatchModeView: React.FC<{}> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = selectedTest?.results[0];
|
const result = selectedTest?.results[0];
|
||||||
return <div className='vbox'>
|
const isFinished = result && result.duration >= 0;
|
||||||
|
return <div className='vbox watch-mode'>
|
||||||
<SplitView sidebarSize={250} orientation='horizontal' sidebarIsFirst={true}>
|
<SplitView sidebarSize={250} orientation='horizontal' sidebarIsFirst={true}>
|
||||||
{(result && result.duration >= 0) ? <FinishedTraceView testResult={result} /> : <InProgressTraceView testResult={result} />}
|
<div className='vbox'>
|
||||||
|
<div className={'vbox' + (isShowingOutput ? '' : ' hidden')}>
|
||||||
|
<Toolbar>
|
||||||
|
<div className='section-title'>Output</div>
|
||||||
|
<ToolbarButton icon='circle-slash' title='Clear output' onClick={() => xtermDataSource.clear()}></ToolbarButton>
|
||||||
|
<div className='spacer'></div>
|
||||||
|
<ToolbarButton icon='close' title='Close' onClick={() => setIsShowingOutput(false)}></ToolbarButton>
|
||||||
|
</Toolbar>
|
||||||
|
<XtermWrapper source={xtermDataSource}></XtermWrapper>;
|
||||||
|
</div>
|
||||||
|
<div className={'vbox' + (isShowingOutput ? ' hidden' : '')}>
|
||||||
|
{isFinished && <FinishedTraceView testResult={result} />}
|
||||||
|
{!isFinished && <InProgressTraceView testResult={result} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className='vbox watch-mode-sidebar'>
|
<div className='vbox watch-mode-sidebar'>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<div className='section-title' style={{ cursor: 'pointer' }} onClick={() => setSettingsVisible(false)}>Tests</div>
|
<div className='section-title' style={{ cursor: 'pointer' }} onClick={() => setSettingsVisible(false)}>Tests</div>
|
||||||
|
|
@ -137,6 +153,7 @@ export const WatchModeView: React.FC<{}> = ({
|
||||||
<ToolbarButton icon='eye-watch' title='Watch' toggled={isWatchingFiles} onClick={() => setIsWatchingFiles(!isWatchingFiles)}></ToolbarButton>
|
<ToolbarButton icon='eye-watch' title='Watch' toggled={isWatchingFiles} onClick={() => setIsWatchingFiles(!isWatchingFiles)}></ToolbarButton>
|
||||||
<div className='spacer'></div>
|
<div className='spacer'></div>
|
||||||
<ToolbarButton icon='gear' title='Toggle color mode' toggled={settingsVisible} onClick={() => { setSettingsVisible(!settingsVisible); }}></ToolbarButton>
|
<ToolbarButton icon='gear' title='Toggle color mode' toggled={settingsVisible} onClick={() => { setSettingsVisible(!settingsVisible); }}></ToolbarButton>
|
||||||
|
<ToolbarButton icon='terminal' title='Toggle color mode' toggled={isShowingOutput} onClick={() => { setIsShowingOutput(!isShowingOutput); }}></ToolbarButton>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
{!settingsVisible && <Expandable
|
{!settingsVisible && <Expandable
|
||||||
title={<input ref={inputRef} type='search' placeholder='Filter (e.g. text, @tag)' spellCheck={false} value={filterText}
|
title={<input ref={inputRef} type='search' placeholder='Filter (e.g. text, @tag)' spellCheck={false} value={filterText}
|
||||||
|
|
@ -322,7 +339,7 @@ export const InProgressTraceView: React.FC<{
|
||||||
setModel(testResult ? stepsToModel(testResult) : undefined);
|
setModel(testResult ? stepsToModel(testResult) : undefined);
|
||||||
}, [stepsProgress, testResult]);
|
}, [stepsProgress, testResult]);
|
||||||
|
|
||||||
return <TraceView model={model} />;
|
return <Workbench model={model} hideTimelineBars={true} hideStackFrames={true} showSourcesFirst={true} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FinishedTraceView: React.FC<{
|
export const FinishedTraceView: React.FC<{
|
||||||
|
|
@ -337,16 +354,7 @@ export const FinishedTraceView: React.FC<{
|
||||||
loadSingleTraceFile(attachment.path).then(setModel);
|
loadSingleTraceFile(attachment.path).then(setModel);
|
||||||
}, [testResult]);
|
}, [testResult]);
|
||||||
|
|
||||||
return <TraceView model={model} />;
|
return <Workbench key='workbench' model={model} hideTimelineBars={true} hideStackFrames={true} showSourcesFirst={true} />;
|
||||||
};
|
|
||||||
|
|
||||||
export const TraceView: React.FC<{
|
|
||||||
model: MultiTraceModel | undefined,
|
|
||||||
}> = ({ model }) => {
|
|
||||||
const xterm = <XtermWrapper source={xtermDataSource}></XtermWrapper>;
|
|
||||||
return <Workbench model={model} output={xterm} rightToolbar={[
|
|
||||||
<ToolbarButton icon='trash' title='Clear output' onClick={() => xtermDataSource.clear()}></ToolbarButton>,
|
|
||||||
]} hideTimelineBars={true} hideStackFrames={true} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
|
||||||
|
|
@ -33,42 +33,65 @@ import { MetadataView } from './metadataView';
|
||||||
|
|
||||||
export const Workbench: React.FunctionComponent<{
|
export const Workbench: React.FunctionComponent<{
|
||||||
model?: MultiTraceModel,
|
model?: MultiTraceModel,
|
||||||
output?: React.ReactElement,
|
|
||||||
rightToolbar?: React.ReactElement[],
|
|
||||||
hideTimelineBars?: boolean,
|
hideTimelineBars?: boolean,
|
||||||
hideStackFrames?: boolean,
|
hideStackFrames?: boolean,
|
||||||
}> = ({ model, output, rightToolbar, hideTimelineBars, hideStackFrames }) => {
|
showSourcesFirst?: boolean,
|
||||||
|
}> = ({ model, hideTimelineBars, hideStackFrames, showSourcesFirst }) => {
|
||||||
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>(undefined);
|
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>(undefined);
|
||||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
|
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||||
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
||||||
const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState<string>(output ? 'output' : 'call');
|
const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState<string>(showSourcesFirst ? 'source' : 'call');
|
||||||
const activeAction = model ? highlightedAction || selectedAction : undefined;
|
const activeAction = model ? highlightedAction || selectedAction : undefined;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (selectedAction)
|
if (selectedAction && model?.actions.includes(selectedAction))
|
||||||
return;
|
return;
|
||||||
const failedAction = model?.actions.find(a => a.error);
|
const failedAction = model?.actions.find(a => a.error);
|
||||||
if (failedAction)
|
if (failedAction)
|
||||||
setSelectedAction(failedAction);
|
setSelectedAction(failedAction);
|
||||||
// In the UI mode, selecting the first error should reveal source.
|
else if (model?.actions.length)
|
||||||
if (failedAction && output)
|
setSelectedAction(model.actions[model.actions.length - 1]);
|
||||||
setSelectedPropertiesTab('source');
|
}, [model, selectedAction, setSelectedAction, setSelectedPropertiesTab]);
|
||||||
}, [model, output, selectedAction, setSelectedAction, setSelectedPropertiesTab]);
|
|
||||||
|
|
||||||
const { errors, warnings } = activeAction ? modelUtil.stats(activeAction) : { errors: 0, warnings: 0 };
|
const { errors, warnings } = activeAction ? modelUtil.stats(activeAction) : { errors: 0, warnings: 0 };
|
||||||
const consoleCount = errors + warnings;
|
const consoleCount = errors + warnings;
|
||||||
const networkCount = activeAction ? modelUtil.resourcesForAction(activeAction).length : 0;
|
const networkCount = activeAction ? modelUtil.resourcesForAction(activeAction).length : 0;
|
||||||
const sdkLanguage = model?.sdkLanguage || 'javascript';
|
const sdkLanguage = model?.sdkLanguage || 'javascript';
|
||||||
|
|
||||||
const tabs: TabbedPaneTabModel[] = [
|
const callTab: TabbedPaneTabModel = {
|
||||||
{ id: 'call', title: 'Call', render: () => <CallTab action={activeAction} sdkLanguage={sdkLanguage} /> },
|
id: 'call',
|
||||||
{ id: 'source', title: 'Source', count: 0, render: () => <SourceTab action={activeAction} hideStackFrames={hideStackFrames}/> },
|
title: showSourcesFirst ? 'Log' : 'Call',
|
||||||
{ id: 'console', title: 'Console', count: consoleCount, render: () => <ConsoleTab action={activeAction} /> },
|
render: () => <CallTab action={activeAction} sdkLanguage={sdkLanguage} />
|
||||||
{ id: 'network', title: 'Network', count: networkCount, render: () => <NetworkTab action={activeAction} /> },
|
};
|
||||||
];
|
const sourceTab: TabbedPaneTabModel = {
|
||||||
|
id: 'source',
|
||||||
|
title: 'Source',
|
||||||
|
render: () => <SourceTab action={activeAction} hideStackFrames={hideStackFrames}/>
|
||||||
|
};
|
||||||
|
const consoleTab: TabbedPaneTabModel = {
|
||||||
|
id: 'console',
|
||||||
|
title: 'Console',
|
||||||
|
count: consoleCount,
|
||||||
|
render: () => <ConsoleTab action={activeAction} />
|
||||||
|
};
|
||||||
|
const networkTab: TabbedPaneTabModel = {
|
||||||
|
id: 'network',
|
||||||
|
title: 'Network',
|
||||||
|
count: networkCount,
|
||||||
|
render: () => <NetworkTab action={activeAction} />
|
||||||
|
};
|
||||||
|
|
||||||
if (output)
|
const tabs: TabbedPaneTabModel[] = showSourcesFirst ? [
|
||||||
tabs.unshift({ id: 'output', title: 'Output', component: output });
|
sourceTab,
|
||||||
|
consoleTab,
|
||||||
|
networkTab,
|
||||||
|
callTab,
|
||||||
|
] : [
|
||||||
|
callTab,
|
||||||
|
consoleTab,
|
||||||
|
networkTab,
|
||||||
|
sourceTab,
|
||||||
|
];
|
||||||
|
|
||||||
return <div className='vbox'>
|
return <div className='vbox'>
|
||||||
<Timeline
|
<Timeline
|
||||||
|
|
@ -77,7 +100,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
onSelected={action => setSelectedAction(action)}
|
onSelected={action => setSelectedAction(action)}
|
||||||
hideTimelineBars={hideTimelineBars}
|
hideTimelineBars={hideTimelineBars}
|
||||||
/>
|
/>
|
||||||
<SplitView sidebarSize={output ? 250 : 350} orientation={output ? 'vertical' : 'horizontal'}>
|
<SplitView sidebarSize={250} orientation='vertical'>
|
||||||
<SplitView sidebarSize={250} orientation='horizontal' sidebarIsFirst={true}>
|
<SplitView sidebarSize={250} orientation='horizontal' sidebarIsFirst={true}>
|
||||||
<SnapshotTab action={activeAction} sdkLanguage={sdkLanguage} testIdAttributeName={model?.testIdAttributeName || 'data-testid'} />
|
<SnapshotTab action={activeAction} sdkLanguage={sdkLanguage} testIdAttributeName={model?.testIdAttributeName || 'data-testid'} />
|
||||||
<TabbedPane tabs={
|
<TabbedPane tabs={
|
||||||
|
|
@ -108,7 +131,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
]
|
]
|
||||||
} selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/>
|
} selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/>
|
||||||
</SplitView>
|
</SplitView>
|
||||||
<TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab} rightToolbar={rightToolbar}/>
|
<TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab} />
|
||||||
</SplitView>
|
</SplitView>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,8 @@ body {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
*[hidden] {
|
*[hidden],
|
||||||
|
.hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
||||||
}
|
}
|
||||||
codemirrorRef.current!.widgets = widgets;
|
codemirrorRef.current!.widgets = widgets;
|
||||||
|
|
||||||
if (revealLine)
|
if (revealLine && codemirrorRef.current!.cm.lineCount() >= revealLine)
|
||||||
codemirror.scrollIntoView({ line: revealLine - 1, ch: 0 }, 50);
|
codemirror.scrollIntoView({ line: revealLine - 1, ch: 0 }, 50);
|
||||||
}, [codemirror, text, highlight, revealLine, focusOnChange, onChange]);
|
}, [codemirror, text, highlight, revealLine, focusOnChange, onChange]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -633,7 +633,7 @@ test('should follow redirects', async ({ page, runAndTrace, server, asset }) =>
|
||||||
test('should include metainfo', async ({ showTraceViewer, browserName }) => {
|
test('should include metainfo', async ({ showTraceViewer, browserName }) => {
|
||||||
const traceViewer = await showTraceViewer([traceFile]);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await traceViewer.page.locator('text=Metadata').click();
|
await traceViewer.page.locator('text=Metadata').click();
|
||||||
const callLine = traceViewer.page.locator('.call-line');
|
const callLine = traceViewer.page.locator('.metadata-view .call-line');
|
||||||
await expect(callLine.getByText('start time')).toHaveText(/start time:[\d/,: ]+/);
|
await expect(callLine.getByText('start time')).toHaveText(/start time:[\d/,: ]+/);
|
||||||
await expect(callLine.getByText('duration')).toHaveText(/duration:[\dms]+/);
|
await expect(callLine.getByText('duration')).toHaveText(/duration:[\dms]+/);
|
||||||
await expect(callLine.getByText('engine')).toHaveText(/engine:[\w]+/);
|
await expect(callLine.getByText('engine')).toHaveText(/engine:[\w]+/);
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ const test = baseTest.extend<{ showReport: (reportFolder?: string) => Promise<vo
|
||||||
|
|
||||||
test.use({ channel: 'chrome' });
|
test.use({ channel: 'chrome' });
|
||||||
|
|
||||||
|
test.describe.configure({ mode: 'parallel' });
|
||||||
|
|
||||||
test('should generate report', async ({ runInlineTest, showReport, page }) => {
|
test('should generate report', async ({ runInlineTest, showReport, page }) => {
|
||||||
await runInlineTest({
|
await runInlineTest({
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
|
|
@ -473,7 +475,6 @@ test('should warn user when viewing via file:// protocol', async ({ runInlineTes
|
||||||
await test.step('view via server', async () => {
|
await test.step('view via server', async () => {
|
||||||
await showReport();
|
await showReport();
|
||||||
await page.locator('[title="View trace"]').click();
|
await page.locator('[title="View trace"]').click();
|
||||||
await expect(page.locator('body')).toContainText('Action does not have snapshots', { useInnerText: true });
|
|
||||||
await expect(page.locator('dialog')).toBeHidden();
|
await expect(page.locator('dialog')).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue