2021-08-05 22:36:47 +02:00
|
|
|
|
/*
|
|
|
|
|
|
Copyright (c) Microsoft Corporation.
|
|
|
|
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
|
|
limitations under the License.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import './htmlReport.css';
|
|
|
|
|
|
import * as React from 'react';
|
|
|
|
|
|
import { SplitView } from '../components/splitView';
|
|
|
|
|
|
import { TreeItem } from '../components/treeItem';
|
|
|
|
|
|
import { TabbedPane } from '../traceViewer/ui/tabbedPane';
|
|
|
|
|
|
import ansi2html from 'ansi-to-html';
|
2021-08-08 00:47:03 +02:00
|
|
|
|
import type { JsonAttachment, JsonLocation, JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep } from '../../test/reporters/html';
|
2021-08-05 22:36:47 +02:00
|
|
|
|
import { msToString } from '../uiUtils';
|
2021-09-01 01:34:52 +02:00
|
|
|
|
import { Source, SourceProps } from '../components/source';
|
2021-08-05 22:36:47 +02:00
|
|
|
|
|
|
|
|
|
|
type Filter = 'Failing' | 'All';
|
|
|
|
|
|
|
|
|
|
|
|
export const Report: React.FC = () => {
|
|
|
|
|
|
const [report, setReport] = React.useState<JsonReport | undefined>();
|
|
|
|
|
|
const [selectedTest, setSelectedTest] = React.useState<JsonTestCase | undefined>();
|
|
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
|
(async () => {
|
|
|
|
|
|
const result = await fetch('report.json');
|
2021-09-01 01:34:52 +02:00
|
|
|
|
const json = (await result.json()) as JsonReport;
|
2021-08-05 22:36:47 +02:00
|
|
|
|
setReport(json);
|
|
|
|
|
|
})();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
const [filter, setFilter] = React.useState<Filter>('Failing');
|
|
|
|
|
|
|
2021-08-08 00:47:03 +02:00
|
|
|
|
const { unexpectedTests, unexpectedTestCount } = React.useMemo(() => {
|
|
|
|
|
|
const unexpectedTests = new Map<JsonSuite, JsonTestCase[]>();
|
|
|
|
|
|
let unexpectedTestCount = 0;
|
|
|
|
|
|
for (const project of report?.suites || []) {
|
|
|
|
|
|
const unexpected = computeUnexpectedTests(project);
|
|
|
|
|
|
unexpectedTestCount += unexpected.length;
|
|
|
|
|
|
unexpectedTests.set(project, unexpected);
|
|
|
|
|
|
}
|
|
|
|
|
|
return { unexpectedTests, unexpectedTestCount };
|
2021-08-05 22:36:47 +02:00
|
|
|
|
}, [report]);
|
|
|
|
|
|
|
|
|
|
|
|
return <div className='hbox'>
|
2021-09-01 01:34:52 +02:00
|
|
|
|
<SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
|
2021-08-05 22:36:47 +02:00
|
|
|
|
<TestCaseView test={selectedTest}></TestCaseView>
|
|
|
|
|
|
<div className='suite-tree'>
|
2021-09-01 01:34:52 +02:00
|
|
|
|
<div className='tab-strip'>{
|
|
|
|
|
|
(['Failing', 'All'] as Filter[]).map(item => {
|
|
|
|
|
|
const selected = item === filter;
|
|
|
|
|
|
return <div key={item} className={'tab-element' + (selected ? ' selected' : '')} onClick={e => {
|
|
|
|
|
|
setFilter(item);
|
|
|
|
|
|
}}>{item}</div>;
|
|
|
|
|
|
})
|
|
|
|
|
|
}</div>
|
2021-08-05 22:36:47 +02:00
|
|
|
|
{filter === 'All' && report?.suites.map((s, i) => <ProjectTreeItem key={i} suite={s} setSelectedTest={setSelectedTest} selectedTest={selectedTest}></ProjectTreeItem>)}
|
2021-08-08 00:47:03 +02:00
|
|
|
|
{filter === 'Failing' && !!unexpectedTestCount && report?.suites.map((s, i) => {
|
|
|
|
|
|
const hasUnexpectedOutcomes = !!unexpectedTests.get(s)?.length;
|
|
|
|
|
|
return hasUnexpectedOutcomes && <ProjectFlatTreeItem key={i} suite={s} setSelectedTest={setSelectedTest} selectedTest={selectedTest} unexpectedTests={unexpectedTests.get(s)!}></ProjectFlatTreeItem>;
|
2021-08-05 22:36:47 +02:00
|
|
|
|
})}
|
2021-08-08 00:47:03 +02:00
|
|
|
|
{filter === 'Failing' && !unexpectedTestCount && <div className='awesome'>You are awesome!</div>}
|
2021-08-05 22:36:47 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</SplitView>
|
|
|
|
|
|
</div>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const ProjectTreeItem: React.FC<{
|
|
|
|
|
|
suite?: JsonSuite;
|
|
|
|
|
|
selectedTest?: JsonTestCase,
|
|
|
|
|
|
setSelectedTest: (test: JsonTestCase) => void;
|
|
|
|
|
|
}> = ({ suite, setSelectedTest, selectedTest }) => {
|
2021-08-08 00:47:03 +02:00
|
|
|
|
const location = renderLocation(suite?.location, true);
|
2021-08-05 22:36:47 +02:00
|
|
|
|
|
|
|
|
|
|
return <TreeItem title={<div className='hbox'>
|
2021-08-11 02:06:25 +02:00
|
|
|
|
<div style={{ flex: 'auto', alignItems: 'center', display: 'flex' }}>{testSuiteErrorStatusIcon(suite) || statusIcon('passed')}<div style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{suite?.title || 'Project'}</div></div>
|
2021-08-05 22:36:47 +02:00
|
|
|
|
{!!suite?.location?.line && location && <div style={{ flex: 'none', padding: '0 4px', color: '#666' }}>{location}</div>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
} loadChildren={() => {
|
2021-08-08 00:47:03 +02:00
|
|
|
|
return suite?.suites.map((s, i) => <SuiteTreeItem key={i} suite={s} setSelectedTest={setSelectedTest} selectedTest={selectedTest} depth={1} showFileName={true}></SuiteTreeItem>) || [];
|
2021-08-05 22:36:47 +02:00
|
|
|
|
}} depth={0} expandByDefault={true}></TreeItem>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const ProjectFlatTreeItem: React.FC<{
|
|
|
|
|
|
suite?: JsonSuite;
|
2021-08-08 00:47:03 +02:00
|
|
|
|
unexpectedTests: JsonTestCase[],
|
2021-08-05 22:36:47 +02:00
|
|
|
|
selectedTest?: JsonTestCase,
|
|
|
|
|
|
setSelectedTest: (test: JsonTestCase) => void;
|
2021-08-08 00:47:03 +02:00
|
|
|
|
}> = ({ suite, setSelectedTest, selectedTest, unexpectedTests }) => {
|
|
|
|
|
|
const location = renderLocation(suite?.location, true);
|
2021-08-05 22:36:47 +02:00
|
|
|
|
|
|
|
|
|
|
return <TreeItem title={<div className='hbox'>
|
2021-08-11 02:06:25 +02:00
|
|
|
|
<div style={{ flex: 'auto', alignItems: 'center', display: 'flex' }}>{testSuiteErrorStatusIcon(suite) || statusIcon('passed')}<div style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{suite?.title || 'Project'}</div></div>
|
2021-08-05 22:36:47 +02:00
|
|
|
|
{!!suite?.location?.line && location && <div style={{ flex: 'none', padding: '0 4px', color: '#666' }}>{location}</div>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
} loadChildren={() => {
|
2021-08-08 00:47:03 +02:00
|
|
|
|
return unexpectedTests.map((t, i) => <TestTreeItem key={i} test={t} setSelectedTest={setSelectedTest} selectedTest={selectedTest} showFileName={false} depth={1}></TestTreeItem>) || [];
|
2021-08-05 22:36:47 +02:00
|
|
|
|
}} depth={0} expandByDefault={true}></TreeItem>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const SuiteTreeItem: React.FC<{
|
|
|
|
|
|
suite?: JsonSuite;
|
|
|
|
|
|
selectedTest?: JsonTestCase,
|
|
|
|
|
|
setSelectedTest: (test: JsonTestCase) => void;
|
|
|
|
|
|
depth: number,
|
2021-08-08 00:47:03 +02:00
|
|
|
|
showFileName: boolean,
|
|
|
|
|
|
}> = ({ suite, setSelectedTest, selectedTest, showFileName, depth }) => {
|
|
|
|
|
|
const location = renderLocation(suite?.location, showFileName);
|
2021-08-05 22:36:47 +02:00
|
|
|
|
return <TreeItem title={<div className='hbox'>
|
|
|
|
|
|
<div style={{ flex: 'auto', alignItems: 'center', display: 'flex' }}>{testSuiteErrorStatusIcon(suite) || statusIcon('passed')}<div style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{suite?.title}</div></div>
|
|
|
|
|
|
{!!suite?.location?.line && location && <div style={{ flex: 'none', padding: '0 4px', color: '#666' }}>{location}</div>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
} loadChildren={() => {
|
2021-08-08 00:47:03 +02:00
|
|
|
|
const suiteChildren = suite?.suites.map((s, i) => <SuiteTreeItem key={i} suite={s} setSelectedTest={setSelectedTest} selectedTest={selectedTest} depth={depth + 1} showFileName={false}></SuiteTreeItem>) || [];
|
2021-08-18 23:30:55 +02:00
|
|
|
|
const suiteCount = suite ? suite.suites.length : 0;
|
|
|
|
|
|
const testChildren = suite?.tests.map((t, i) => <TestTreeItem key={i + suiteCount} test={t} setSelectedTest={setSelectedTest} selectedTest={selectedTest} showFileName={false} depth={depth + 1}></TestTreeItem>) || [];
|
2021-08-05 22:36:47 +02:00
|
|
|
|
return [...suiteChildren, ...testChildren];
|
|
|
|
|
|
}} depth={depth}></TreeItem>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const TestTreeItem: React.FC<{
|
|
|
|
|
|
expandByDefault?: boolean,
|
|
|
|
|
|
test: JsonTestCase;
|
|
|
|
|
|
showFileName: boolean,
|
|
|
|
|
|
selectedTest?: JsonTestCase,
|
|
|
|
|
|
setSelectedTest: (test: JsonTestCase) => void;
|
|
|
|
|
|
depth: number,
|
|
|
|
|
|
}> = ({ test, setSelectedTest, selectedTest, showFileName, expandByDefault, depth }) => {
|
|
|
|
|
|
const fileName = test.location.file;
|
|
|
|
|
|
const name = fileName.substring(fileName.lastIndexOf('/') + 1);
|
|
|
|
|
|
return <TreeItem title={<div className='hbox'>
|
|
|
|
|
|
<div style={{ flex: 'auto', alignItems: 'center', display: 'flex' }}>{testCaseStatusIcon(test)}<div style={{overflow: 'hidden', textOverflow: 'ellipsis'}}>{test.title}</div></div>
|
|
|
|
|
|
{showFileName && <div style={{ flex: 'none', padding: '0 4px', color: '#666' }}>{name}:{test.location.line}</div>}
|
|
|
|
|
|
{!showFileName && <div style={{ flex: 'none', padding: '0 4px', color: '#666' }}>{msToString(test.results.reduce((v, a) => v + a.duration, 0))}</div>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
} selected={test === selectedTest} depth={depth} expandByDefault={expandByDefault} onClick={() => setSelectedTest(test)}></TreeItem>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const TestCaseView: React.FC<{
|
|
|
|
|
|
test?: JsonTestCase,
|
|
|
|
|
|
}> = ({ test }) => {
|
|
|
|
|
|
const [selectedTab, setSelectedTab] = React.useState<string>('0');
|
|
|
|
|
|
return <div className="test-case vbox">
|
2021-09-01 01:34:52 +02:00
|
|
|
|
{ !test && <div className='tab-strip' />}
|
2021-08-05 22:36:47 +02:00
|
|
|
|
{ test && <TabbedPane tabs={
|
|
|
|
|
|
test?.results.map((result, index) => ({
|
|
|
|
|
|
id: String(index),
|
|
|
|
|
|
title: <div style={{ display: 'flex', alignItems: 'center' }}>{statusIcon(result.status)} {retryLabel(index)}</div>,
|
|
|
|
|
|
render: () => <TestOverview test={test} result={result}></TestOverview>
|
|
|
|
|
|
})) || []} selectedTab={selectedTab} setSelectedTab={setSelectedTab} /> }
|
|
|
|
|
|
</div>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const TestOverview: React.FC<{
|
|
|
|
|
|
test: JsonTestCase,
|
|
|
|
|
|
result: JsonTestResult,
|
2021-09-01 01:34:52 +02:00
|
|
|
|
}> = ({ test, result }) => {
|
|
|
|
|
|
const [selectedStep, setSelectedStep] = React.useState<JsonTestStep | undefined>();
|
|
|
|
|
|
return <div className='test-result'>
|
|
|
|
|
|
<SplitView sidebarSize={500} orientation='horizontal' sidebarIsFirst={true}>
|
|
|
|
|
|
{!selectedStep && <TestResultDetails test={test} result={result} />}
|
|
|
|
|
|
{!!selectedStep && <TestStepDetails test={test} result={result} step={selectedStep}/>}
|
|
|
|
|
|
<div className='vbox steps-tree'>
|
|
|
|
|
|
<TreeItem
|
|
|
|
|
|
title={<div className='test-overview-title'>{renderLocation(test.location, true)} › {test?.title} ({msToString(result.duration)})</div>}
|
|
|
|
|
|
depth={0}
|
|
|
|
|
|
key='test'
|
|
|
|
|
|
onClick={() => setSelectedStep(undefined)}>
|
|
|
|
|
|
</TreeItem>
|
|
|
|
|
|
{result.steps.map((step, i) => <StepTreeItem key={i} step={step} depth={0} selectedStep={selectedStep} setSelectedStep={setSelectedStep}></StepTreeItem>)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</SplitView>
|
|
|
|
|
|
</div>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const TestResultDetails: React.FC<{
|
|
|
|
|
|
test: JsonTestCase,
|
|
|
|
|
|
result: JsonTestResult,
|
2021-08-05 22:36:47 +02:00
|
|
|
|
}> = ({ test, result }) => {
|
2021-08-18 23:30:55 +02:00
|
|
|
|
const { screenshots, video, attachmentsMap } = React.useMemo(() => {
|
2021-08-11 02:06:25 +02:00
|
|
|
|
const attachmentsMap = new Map<string, JsonAttachment>();
|
|
|
|
|
|
const screenshots = result.attachments.filter(a => a.name === 'screenshot');
|
2021-08-18 23:30:55 +02:00
|
|
|
|
const video = result.attachments.filter(a => a.name === 'video');
|
2021-08-08 00:47:03 +02:00
|
|
|
|
for (const a of result.attachments)
|
2021-08-11 02:06:25 +02:00
|
|
|
|
attachmentsMap.set(a.name, a);
|
2021-08-18 23:30:55 +02:00
|
|
|
|
return { attachmentsMap, screenshots, video };
|
2021-08-08 00:47:03 +02:00
|
|
|
|
}, [ result ]);
|
2021-09-01 01:34:52 +02:00
|
|
|
|
return <div>
|
|
|
|
|
|
{result.failureSnippet && <div className='test-overview-title'>Test error</div>}
|
|
|
|
|
|
{result.failureSnippet && <div className='error-message' dangerouslySetInnerHTML={{ __html: new ansi2html({ colors: ansiColors }).toHtml(escapeHTML(result.failureSnippet.trim())) }}></div>}
|
2021-08-11 02:06:25 +02:00
|
|
|
|
{attachmentsMap.has('expected') && attachmentsMap.has('actual') && <ImageDiff actual={attachmentsMap.get('actual')!} expected={attachmentsMap.get('expected')!} diff={attachmentsMap.get('diff')}></ImageDiff>}
|
2021-08-08 00:47:03 +02:00
|
|
|
|
{!!screenshots.length && <div className='test-overview-title'>Screenshots</div>}
|
2021-08-11 02:06:25 +02:00
|
|
|
|
{screenshots.map(a => <div className='image-preview'><img src={'resources/' + a.sha1} /></div>)}
|
2021-08-18 23:30:55 +02:00
|
|
|
|
{!!video.length && <div className='test-overview-title'>Video</div>}
|
|
|
|
|
|
{video.map(a => <div className='image-preview'>
|
|
|
|
|
|
<video controls>
|
|
|
|
|
|
<source src={'resources/' + a.sha1} type={a.contentType}/>
|
|
|
|
|
|
</video>
|
|
|
|
|
|
</div>)}
|
2021-08-11 02:06:25 +02:00
|
|
|
|
{!!result.attachments && <div className='test-overview-title'>Attachments</div>}
|
|
|
|
|
|
{result.attachments.map(a => <AttachmentLink attachment={a}></AttachmentLink>)}
|
2021-09-01 01:34:52 +02:00
|
|
|
|
</div>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const TestStepDetails: React.FC<{
|
|
|
|
|
|
test: JsonTestCase,
|
|
|
|
|
|
result: JsonTestResult,
|
|
|
|
|
|
step: JsonTestStep,
|
|
|
|
|
|
}> = ({ test, result, step }) => {
|
|
|
|
|
|
const [source, setSource] = React.useState<SourceProps>({ text: '', language: 'javascript' });
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
|
(async () => {
|
|
|
|
|
|
const frame = step.stack?.[0];
|
|
|
|
|
|
if (!frame || !frame.sha1)
|
|
|
|
|
|
return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('resources/' + frame.sha1);
|
|
|
|
|
|
const text = await response.text();
|
|
|
|
|
|
setSource({ text, language: 'javascript', highlight: [{ line: frame.line, type: 'paused' }], revealLine: frame.line });
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
setSource({ text: '', language: 'javascript' });
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
|
|
|
|
|
}, [step]);
|
|
|
|
|
|
return <div className='vbox'>
|
|
|
|
|
|
{step.failureSnippet && <div className='test-overview-title'>Step error</div>}
|
|
|
|
|
|
{step.failureSnippet && <div className='error-message' dangerouslySetInnerHTML={{ __html: new ansi2html({ colors: ansiColors }).toHtml(escapeHTML(step.failureSnippet.trim())) }}></div>}
|
|
|
|
|
|
<Source text={source.text} language={source.language} highlight={source.highlight} revealLine={source.revealLine}></Source>
|
2021-08-05 22:36:47 +02:00
|
|
|
|
</div>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const StepTreeItem: React.FC<{
|
|
|
|
|
|
step: JsonTestStep;
|
|
|
|
|
|
depth: number,
|
2021-09-01 01:34:52 +02:00
|
|
|
|
selectedStep?: JsonTestStep,
|
|
|
|
|
|
setSelectedStep: (step: JsonTestStep | undefined) => void;
|
|
|
|
|
|
}> = ({ step, depth, selectedStep, setSelectedStep }) => {
|
2021-08-05 22:36:47 +02:00
|
|
|
|
return <TreeItem title={<div style={{ display: 'flex', alignItems: 'center', flex: 'auto', maxWidth: 430 }}>
|
|
|
|
|
|
{testStepStatusIcon(step)}
|
2021-09-01 01:34:52 +02:00
|
|
|
|
<span style={{ whiteSpace: 'pre' }}>{step.preview || step.title}</span>
|
2021-08-05 22:36:47 +02:00
|
|
|
|
<div style={{ flex: 'auto' }}></div>
|
|
|
|
|
|
<div>{msToString(step.duration)}</div>
|
|
|
|
|
|
</div>} loadChildren={step.steps.length ? () => {
|
2021-09-01 01:34:52 +02:00
|
|
|
|
return step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} selectedStep={selectedStep} setSelectedStep={setSelectedStep}></StepTreeItem>);
|
|
|
|
|
|
} : undefined} depth={depth} selected={step === selectedStep} onClick={() => setSelectedStep(step)}></TreeItem>;
|
2021-08-05 22:36:47 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
2021-08-08 00:47:03 +02:00
|
|
|
|
export const ImageDiff: React.FunctionComponent<{
|
|
|
|
|
|
actual: JsonAttachment,
|
|
|
|
|
|
expected: JsonAttachment,
|
|
|
|
|
|
diff?: JsonAttachment,
|
|
|
|
|
|
}> = ({ actual, expected, diff }) => {
|
|
|
|
|
|
const [selectedTab, setSelectedTab] = React.useState<string>('actual');
|
|
|
|
|
|
const tabs = [];
|
|
|
|
|
|
tabs.push({
|
|
|
|
|
|
id: 'actual',
|
|
|
|
|
|
title: 'Actual',
|
2021-08-11 02:06:25 +02:00
|
|
|
|
render: () => <div className='image-preview'><img src={'resources/' + actual.sha1}/></div>
|
2021-08-08 00:47:03 +02:00
|
|
|
|
});
|
|
|
|
|
|
tabs.push({
|
|
|
|
|
|
id: 'expected',
|
|
|
|
|
|
title: 'Expected',
|
2021-08-11 02:06:25 +02:00
|
|
|
|
render: () => <div className='image-preview'><img src={'resources/' + expected.sha1}/></div>
|
2021-08-08 00:47:03 +02:00
|
|
|
|
});
|
|
|
|
|
|
if (diff) {
|
|
|
|
|
|
tabs.push({
|
|
|
|
|
|
id: 'diff',
|
|
|
|
|
|
title: 'Diff',
|
2021-08-11 02:06:25 +02:00
|
|
|
|
render: () => <div className='image-preview'><img src={'resources/' + diff.sha1}/></div>,
|
2021-08-08 00:47:03 +02:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return <div className='vbox test-image-mismatch'>
|
|
|
|
|
|
<div className='test-overview-title'>Image mismatch</div>
|
|
|
|
|
|
<TabbedPane tabs={tabs} selectedTab={selectedTab} setSelectedTab={setSelectedTab} />
|
|
|
|
|
|
</div>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2021-08-11 02:06:25 +02:00
|
|
|
|
export const AttachmentLink: React.FunctionComponent<{
|
|
|
|
|
|
attachment: JsonAttachment,
|
|
|
|
|
|
}> = ({ attachment }) => {
|
|
|
|
|
|
return <TreeItem title={<div style={{ display: 'flex', alignItems: 'center', flex: 'auto', maxWidth: 430 }}>
|
|
|
|
|
|
<span className={'codicon codicon-cloud-download'}></span>
|
|
|
|
|
|
{attachment.sha1 && <a href={'resources/' + attachment.sha1} target='_blank'>{attachment.name}</a>}
|
|
|
|
|
|
{attachment.body && <span>{attachment.name}</span>}
|
|
|
|
|
|
</div>} loadChildren={attachment.body ? () => {
|
|
|
|
|
|
return [<div className='attachment-body'>${attachment.body}</div>];
|
|
|
|
|
|
} : undefined} depth={0}></TreeItem>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2021-08-05 22:36:47 +02:00
|
|
|
|
function testSuiteErrorStatusIcon(suite?: JsonSuite): JSX.Element | undefined {
|
|
|
|
|
|
if (!suite)
|
|
|
|
|
|
return;
|
|
|
|
|
|
for (const child of suite.suites) {
|
|
|
|
|
|
const icon = testSuiteErrorStatusIcon(child);
|
|
|
|
|
|
if (icon)
|
|
|
|
|
|
return icon;
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const test of suite.tests) {
|
|
|
|
|
|
if (test.outcome !== 'expected' && test.outcome !== 'skipped')
|
|
|
|
|
|
return testCaseStatusIcon(test);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function testCaseStatusIcon(test?: JsonTestCase): JSX.Element {
|
|
|
|
|
|
if (!test)
|
|
|
|
|
|
return statusIcon('passed');
|
|
|
|
|
|
return statusIcon(test.outcome);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function testStepStatusIcon(step: JsonTestStep): JSX.Element {
|
|
|
|
|
|
if (step.category === 'internal')
|
|
|
|
|
|
return <span></span>;
|
|
|
|
|
|
return statusIcon(step.error ? 'failed' : 'passed');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function statusIcon(status: 'failed' | 'timedOut' | 'skipped' | 'passed' | 'expected' | 'unexpected' | 'flaky'): JSX.Element {
|
|
|
|
|
|
switch (status) {
|
|
|
|
|
|
case 'failed':
|
|
|
|
|
|
case 'unexpected':
|
|
|
|
|
|
return <span className={'codicon codicon-error status-icon'}></span>;
|
|
|
|
|
|
case 'passed':
|
|
|
|
|
|
case 'expected':
|
|
|
|
|
|
return <span className={'codicon codicon-circle-filled status-icon'}></span>;
|
|
|
|
|
|
case 'timedOut':
|
|
|
|
|
|
return <span className={'codicon codicon-clock status-icon'}></span>;
|
|
|
|
|
|
case 'flaky':
|
|
|
|
|
|
return <span className={'codicon codicon-alert status-icon'}></span>;
|
|
|
|
|
|
case 'skipped':
|
|
|
|
|
|
return <span className={'codicon codicon-tag status-icon'}></span>;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-08-08 00:47:03 +02:00
|
|
|
|
function computeUnexpectedTests(suite: JsonSuite): JsonTestCase[] {
|
2021-08-05 22:36:47 +02:00
|
|
|
|
const failedTests: JsonTestCase[] = [];
|
|
|
|
|
|
const visit = (suite: JsonSuite) => {
|
|
|
|
|
|
for (const child of suite.suites)
|
|
|
|
|
|
visit(child);
|
|
|
|
|
|
for (const test of suite.tests) {
|
2021-08-08 00:47:03 +02:00
|
|
|
|
if (test.outcome !== 'expected' && test.outcome !== 'skipped')
|
2021-08-05 22:36:47 +02:00
|
|
|
|
failedTests.push(test);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
visit(suite);
|
|
|
|
|
|
return failedTests;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-08-08 00:47:03 +02:00
|
|
|
|
function renderLocation(location: JsonLocation | undefined, showFileName: boolean) {
|
2021-08-05 22:36:47 +02:00
|
|
|
|
if (!location)
|
|
|
|
|
|
return '';
|
2021-08-11 02:06:25 +02:00
|
|
|
|
return (showFileName ? location.file : '') + ':' + location.line;
|
2021-08-05 22:36:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function retryLabel(index: number) {
|
|
|
|
|
|
if (!index)
|
|
|
|
|
|
return 'Run';
|
|
|
|
|
|
return `Retry #${index}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ansiColors = {
|
|
|
|
|
|
0: '#000',
|
|
|
|
|
|
1: '#C00',
|
|
|
|
|
|
2: '#0C0',
|
|
|
|
|
|
3: '#C50',
|
|
|
|
|
|
4: '#00C',
|
|
|
|
|
|
5: '#C0C',
|
|
|
|
|
|
6: '#0CC',
|
|
|
|
|
|
7: '#CCC',
|
|
|
|
|
|
8: '#555',
|
|
|
|
|
|
9: '#F55',
|
|
|
|
|
|
10: '#5F5',
|
|
|
|
|
|
11: '#FF5',
|
|
|
|
|
|
12: '#55F',
|
|
|
|
|
|
13: '#F5F',
|
|
|
|
|
|
14: '#5FF',
|
|
|
|
|
|
15: '#FFF'
|
|
|
|
|
|
};
|
2021-09-01 01:34:52 +02:00
|
|
|
|
|
|
|
|
|
|
function escapeHTML(text: string): string {
|
|
|
|
|
|
return text.replace(/[&"<>]/g, c => ({ '&': '&', '"': '"', '<': '<', '>': '>' }[c]!));
|
|
|
|
|
|
}
|