chore(ui): show output on demand (#21592)

This commit is contained in:
Pavel Feldman 2023-03-11 11:43:33 -08:00 committed by GitHub
parent 428ea66578
commit b85d670491
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 71 additions and 38 deletions

View File

@ -24,7 +24,7 @@ export const MetadataView: React.FunctionComponent<{
}> = ({ model }) => {
if (!model)
return <></>;
return <div className='vbox'>
return <div className='metadata-view vbox'>
<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>}
<div className='call-line'>duration:<span className='call-value number' title={msToString(model.endTime - model.startTime)}>{msToString(model.endTime - model.startTime)}</span></div>

View File

@ -44,7 +44,7 @@
margin: 0;
}
.watch-mode-sidebar .section-title {
.watch-mode .section-title {
font-size: 11px;
text-transform: uppercase;
font-weight: bold;

View File

@ -62,6 +62,7 @@ export const WatchModeView: React.FC<{}> = ({
const [visibleTestIds, setVisibleTestIds] = React.useState<string[]>([]);
const [filterText, setFilterText] = React.useState<string>('');
const [filterExpanded, setFilterExpanded] = React.useState<boolean>(false);
const [isShowingOutput, setIsShowingOutput] = React.useState<boolean>(false);
const inputRef = React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
@ -125,9 +126,24 @@ export const WatchModeView: React.FC<{}> = ({
};
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}>
{(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'>
<Toolbar>
<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>
<div className='spacer'></div>
<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>
{!settingsVisible && <Expandable
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);
}, [stepsProgress, testResult]);
return <TraceView model={model} />;
return <Workbench model={model} hideTimelineBars={true} hideStackFrames={true} showSourcesFirst={true} />;
};
export const FinishedTraceView: React.FC<{
@ -337,16 +354,7 @@ export const FinishedTraceView: React.FC<{
loadSingleTraceFile(attachment.path).then(setModel);
}, [testResult]);
return <TraceView model={model} />;
};
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} />;
return <Workbench key='workbench' model={model} hideTimelineBars={true} hideStackFrames={true} showSourcesFirst={true} />;
};
declare global {

View File

@ -33,42 +33,65 @@ import { MetadataView } from './metadataView';
export const Workbench: React.FunctionComponent<{
model?: MultiTraceModel,
output?: React.ReactElement,
rightToolbar?: React.ReactElement[],
hideTimelineBars?: boolean,
hideStackFrames?: boolean,
}> = ({ model, output, rightToolbar, hideTimelineBars, hideStackFrames }) => {
showSourcesFirst?: boolean,
}> = ({ model, hideTimelineBars, hideStackFrames, showSourcesFirst }) => {
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>(undefined);
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
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;
React.useEffect(() => {
if (selectedAction)
if (selectedAction && model?.actions.includes(selectedAction))
return;
const failedAction = model?.actions.find(a => a.error);
if (failedAction)
setSelectedAction(failedAction);
// In the UI mode, selecting the first error should reveal source.
if (failedAction && output)
setSelectedPropertiesTab('source');
}, [model, output, selectedAction, setSelectedAction, setSelectedPropertiesTab]);
else if (model?.actions.length)
setSelectedAction(model.actions[model.actions.length - 1]);
}, [model, selectedAction, setSelectedAction, setSelectedPropertiesTab]);
const { errors, warnings } = activeAction ? modelUtil.stats(activeAction) : { errors: 0, warnings: 0 };
const consoleCount = errors + warnings;
const networkCount = activeAction ? modelUtil.resourcesForAction(activeAction).length : 0;
const sdkLanguage = model?.sdkLanguage || 'javascript';
const tabs: TabbedPaneTabModel[] = [
{ id: 'call', title: 'Call', render: () => <CallTab action={activeAction} sdkLanguage={sdkLanguage} /> },
{ id: 'source', title: 'Source', count: 0, render: () => <SourceTab action={activeAction} hideStackFrames={hideStackFrames}/> },
{ id: 'console', title: 'Console', count: consoleCount, render: () => <ConsoleTab action={activeAction} /> },
{ id: 'network', title: 'Network', count: networkCount, render: () => <NetworkTab action={activeAction} /> },
];
const callTab: TabbedPaneTabModel = {
id: 'call',
title: showSourcesFirst ? 'Log' : 'Call',
render: () => <CallTab action={activeAction} sdkLanguage={sdkLanguage} />
};
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)
tabs.unshift({ id: 'output', title: 'Output', component: output });
const tabs: TabbedPaneTabModel[] = showSourcesFirst ? [
sourceTab,
consoleTab,
networkTab,
callTab,
] : [
callTab,
consoleTab,
networkTab,
sourceTab,
];
return <div className='vbox'>
<Timeline
@ -77,7 +100,7 @@ export const Workbench: React.FunctionComponent<{
onSelected={action => setSelectedAction(action)}
hideTimelineBars={hideTimelineBars}
/>
<SplitView sidebarSize={output ? 250 : 350} orientation={output ? 'vertical' : 'horizontal'}>
<SplitView sidebarSize={250} orientation='vertical'>
<SplitView sidebarSize={250} orientation='horizontal' sidebarIsFirst={true}>
<SnapshotTab action={activeAction} sdkLanguage={sdkLanguage} testIdAttributeName={model?.testIdAttributeName || 'data-testid'} />
<TabbedPane tabs={
@ -108,7 +131,7 @@ export const Workbench: React.FunctionComponent<{
]
} selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/>
</SplitView>
<TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab} rightToolbar={rightToolbar}/>
<TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab} />
</SplitView>
</div>;
};

View File

@ -72,7 +72,8 @@ body {
min-height: 0;
}
*[hidden] {
*[hidden],
.hidden {
display: none !important;
}

View File

@ -146,7 +146,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
}
codemirrorRef.current!.widgets = widgets;
if (revealLine)
if (revealLine && codemirrorRef.current!.cm.lineCount() >= revealLine)
codemirror.scrollIntoView({ line: revealLine - 1, ch: 0 }, 50);
}, [codemirror, text, highlight, revealLine, focusOnChange, onChange]);

View File

@ -633,7 +633,7 @@ test('should follow redirects', async ({ page, runAndTrace, server, asset }) =>
test('should include metainfo', async ({ showTraceViewer, browserName }) => {
const traceViewer = await showTraceViewer([traceFile]);
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('duration')).toHaveText(/duration:[\dms]+/);
await expect(callLine.getByText('engine')).toHaveText(/engine:[\w]+/);

View File

@ -37,6 +37,8 @@ const test = baseTest.extend<{ showReport: (reportFolder?: string) => Promise<vo
test.use({ channel: 'chrome' });
test.describe.configure({ mode: 'parallel' });
test('should generate report', async ({ runInlineTest, showReport, page }) => {
await runInlineTest({
'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 showReport();
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();
});