mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 13:50:25 +03:00
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 }) => {
|
||||
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>
|
||||
|
@ -44,7 +44,7 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.watch-mode-sidebar .section-title {
|
||||
.watch-mode .section-title {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
|
@ -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 {
|
||||
|
@ -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>;
|
||||
};
|
||||
|
@ -72,7 +72,8 @@ body {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
*[hidden] {
|
||||
*[hidden],
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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]+/);
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user