/* 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'; import { JsonLocation, JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep } from '../../test/reporters/html'; import { msToString } from '../uiUtils'; type Filter = 'Failing' | 'All'; export const Report: React.FC = () => { const [report, setReport] = React.useState(); const [selectedTest, setSelectedTest] = React.useState(); React.useEffect(() => { (async () => { const result = await fetch('report.json'); const json = await result.json(); setReport(json); })(); }, []); const [filter, setFilter] = React.useState('Failing'); const failingTests = React.useMemo(() => { const map = new Map(); for (const project of report?.suites || []) map.set(project, computeFailingTests(project)); return map; }, [report]); return
{filter === 'All' && report?.suites.map((s, i) => )} {filter === 'Failing' && report?.suites.map((s, i) => { const hasFailingTests = !!failingTests.get(s)?.length; return hasFailingTests && ; })}
; }; const FilterView: React.FC<{ filter: Filter, setFilter: (filter: Filter) => void }> = ({ filter, setFilter }) => { return
{ (['Failing', 'All'] as Filter[]).map(item => { const selected = item === filter; return
{ setFilter(item); }}>{item}
; }) }
; }; const ProjectTreeItem: React.FC<{ suite?: JsonSuite; selectedTest?: JsonTestCase, setSelectedTest: (test: JsonTestCase) => void; }> = ({ suite, setSelectedTest, selectedTest }) => { const location = renderLocation(suite?.location); return
{testSuiteErrorStatusIcon(suite) || statusIcon('passed')}
{suite?.title}
{!!suite?.location?.line && location &&
{location}
} } loadChildren={() => { return suite?.suites.map((s, i) => ) || []; }} depth={0} expandByDefault={true}>
; }; const ProjectFlatTreeItem: React.FC<{ suite?: JsonSuite; failingTests: JsonTestCase[], selectedTest?: JsonTestCase, setSelectedTest: (test: JsonTestCase) => void; }> = ({ suite, setSelectedTest, selectedTest, failingTests }) => { const location = renderLocation(suite?.location); return
{testSuiteErrorStatusIcon(suite) || statusIcon('passed')}
{suite?.title}
{!!suite?.location?.line && location &&
{location}
} } loadChildren={() => { return failingTests.map((t, i) => ) || []; }} depth={0} expandByDefault={true}>
; }; const SuiteTreeItem: React.FC<{ suite?: JsonSuite; selectedTest?: JsonTestCase, setSelectedTest: (test: JsonTestCase) => void; depth: number, }> = ({ suite, setSelectedTest, selectedTest, depth }) => { const location = renderLocation(suite?.location); return
{testSuiteErrorStatusIcon(suite) || statusIcon('passed')}
{suite?.title}
{!!suite?.location?.line && location &&
{location}
} } loadChildren={() => { const suiteChildren = suite?.suites.map((s, i) => ) || []; const testChildren = suite?.tests.map((t, i) => ) || []; return [...suiteChildren, ...testChildren]; }} depth={depth}>
; }; 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
{testCaseStatusIcon(test)}
{test.title}
{showFileName &&
{name}:{test.location.line}
} {!showFileName &&
{msToString(test.results.reduce((v, a) => v + a.duration, 0))}
} } selected={test === selectedTest} depth={depth} expandByDefault={expandByDefault} onClick={() => setSelectedTest(test)}>
; }; const TestCaseView: React.FC<{ test?: JsonTestCase, }> = ({ test }) => { const [selectedTab, setSelectedTab] = React.useState('0'); return
{ test && ({ id: String(index), title:
{statusIcon(result.status)} {retryLabel(index)}
, render: () => })) || []} selectedTab={selectedTab} setSelectedTab={setSelectedTab} /> }
; }; const TestOverview: React.FC<{ test: JsonTestCase, result: JsonTestResult, }> = ({ test, result }) => { return
{test?.title}
{renderLocation(test.location)}
{msToString(result.duration)}
{ result.failureSnippet &&
} { result.steps.map((step, i) => ) } {/*
{ JSON.stringify(result.steps, undefined, 2) }
*/}
; }; const StepTreeItem: React.FC<{ step: JsonTestStep; depth: number, }> = ({ step, depth }) => { return {testStepStatusIcon(step)} {step.title}
{msToString(step.duration)}
} loadChildren={step.steps.length ? () => { return step.steps.map((s, i) => ); } : undefined} depth={depth}>
; }; 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 ; 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 ; case 'passed': case 'expected': return ; case 'timedOut': return ; case 'flaky': return ; case 'skipped': return ; } } function computeFailingTests(suite: JsonSuite): JsonTestCase[] { const failedTests: JsonTestCase[] = []; const visit = (suite: JsonSuite) => { for (const child of suite.suites) visit(child); for (const test of suite.tests) { if (test.results.find(r => r.status === 'failed' || r.status === 'timedOut')) failedTests.push(test); } }; visit(suite); return failedTests; } function renderLocation(location?: JsonLocation) { if (!location) return ''; return location.file + ':' + location.column; } 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' };