diff --git a/src/test/cli.ts b/src/test/cli.ts index 4d43f1df24..8f814c9d0a 100644 --- a/src/test/cli.ts +++ b/src/test/cli.ts @@ -100,7 +100,7 @@ export function addGenerateHtmlCommand(program: commander.CommanderStatic) { for (const file of files) reportFiles.add(path.join(reportFolder, file)); } - new HtmlBuilder([...reportFiles], output); + new HtmlBuilder([...reportFiles], output, loader.fullConfig().rootDir); }).on('--help', () => { console.log(''); console.log('Examples:'); diff --git a/src/test/html/htmlBuilder.ts b/src/test/html/htmlBuilder.ts index 73ba8c35ae..0cf967621b 100644 --- a/src/test/html/htmlBuilder.ts +++ b/src/test/html/htmlBuilder.ts @@ -16,15 +16,18 @@ import fs from 'fs'; import path from 'path'; -import { ProjectTreeItem, SuiteTreeItem, TestTreeItem, TestCase, TestResult, TestStep, TestFile } from './types'; +import { ProjectTreeItem, SuiteTreeItem, TestTreeItem, TestCase, TestResult, TestStep, TestFile, Location } from './types'; import { JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep } from '../reporters/raw'; import { calculateSha1 } from '../../utils/utils'; +import { toPosixPath } from '../reporters/json'; export class HtmlBuilder { private _reportFolder: string; private _tests = new Map(); + private _rootDir: string; - constructor(rawReports: string[], outputDir: string) { + constructor(rawReports: string[], outputDir: string, rootDir: string) { + this._rootDir = rootDir; this._reportFolder = path.resolve(process.cwd(), outputDir); const dataFolder = path.join(this._reportFolder, 'data'); fs.mkdirSync(dataFolder, { recursive: true }); @@ -37,12 +40,13 @@ export class HtmlBuilder { const projectJson = JSON.parse(fs.readFileSync(projectFile, 'utf-8')) as JsonReport; const suites: SuiteTreeItem[] = []; for (const file of projectJson.suites) { - const fileId = calculateSha1(projectFile + ':' + file.location!.file); + const relativeFileName = this._relativeLocation(file.location).file; + const fileId = calculateSha1(projectFile + ':' + relativeFileName); const tests: JsonTestCase[] = []; suites.push(this._createSuiteTreeItem(file, fileId, tests)); const testFile: TestFile = { fileId, - path: file.location!.file, + path: relativeFileName, tests: tests.map(t => this._createTestCase(t)) }; fs.writeFileSync(path.join(dataFolder, fileId + '.json'), JSON.stringify(testFile, undefined, 2)); @@ -60,7 +64,7 @@ export class HtmlBuilder { return { testId: test.testId, title: test.title, - location: test.location, + location: this._relativeLocation(test.location), results: test.results.map(r => this._createTestResult(r)) }; } @@ -71,7 +75,7 @@ export class HtmlBuilder { testCollector.push(...suite.tests); return { title: suite.title, - location: suite.location, + location: this._relativeLocation(suite.location), duration: suites.reduce((a, s) => a + s.duration, 0) + tests.reduce((a, t) => a + t.duration, 0), failedTests: suites.reduce((a, s) => a + s.failedTests, 0) + tests.reduce((a, t) => t.outcome === 'unexpected' || t.outcome === 'flaky' ? a + 1 : a, 0), suites, @@ -85,7 +89,7 @@ export class HtmlBuilder { return { testId: test.testId, fileId: fileId, - location: test.location, + location: this._relativeLocation(test.location), title: test.title, duration, outcome: test.outcome @@ -98,7 +102,7 @@ export class HtmlBuilder { startTime: result.startTime, retry: result.retry, steps: result.steps.map(s => this._createTestStep(s)), - error: result.error, + error: result.error?.message, status: result.status, }; } @@ -110,7 +114,17 @@ export class HtmlBuilder { duration: step.duration, steps: step.steps.map(s => this._createTestStep(s)), log: step.log, - error: step.error + error: step.error?.message + }; + } + + private _relativeLocation(location: Location | undefined): Location { + if (!location) + return { file: '', line: 0, column: 0 }; + return { + file: toPosixPath(path.relative(this._rootDir, location.file)), + line: location.line, + column: location.column, }; } } diff --git a/src/test/html/types.ts b/src/test/html/types.ts index 410e8403ea..dacadd48f4 100644 --- a/src/test/html/types.ts +++ b/src/test/html/types.ts @@ -57,18 +57,12 @@ export type TestCase = { results: TestResult[]; }; -export interface TestError { - message?: string; - stack?: string; - value?: string; -} - export type TestResult = { retry: number; startTime: string; duration: number; steps: TestStep[]; - error?: TestError; + error?: string; status: 'passed' | 'failed' | 'timedOut' | 'skipped'; }; @@ -77,6 +71,6 @@ export type TestStep = { startTime: string; duration: number; log?: string[]; - error?: TestError; + error?: string; steps: TestStep[]; }; diff --git a/src/test/reporters/raw.ts b/src/test/reporters/raw.ts index a5455693cf..7b5f3cd9dc 100644 --- a/src/test/reporters/raw.ts +++ b/src/test/reporters/raw.ts @@ -22,8 +22,8 @@ import { assert, calculateSha1 } from '../../utils/utils'; import { sanitizeForFilePath } from '../util'; import { serializePatterns } from './json'; -export type JsonStats = { expected: number, unexpected: number, flaky: number, skipped: number }; export type JsonLocation = Location; +export type JsonError = TestError; export type JsonStackFrame = { file: string, line: number, column: number }; export type JsonReport = { @@ -86,7 +86,7 @@ export type JsonTestResult = { startTime: string; duration: number; status: TestStatus; - error?: TestError; + error?: JsonError; attachments: JsonAttachment[]; steps: JsonTestStep[]; }; @@ -96,7 +96,7 @@ export type JsonTestStep = { category: string, startTime: string; duration: number; - error?: TestError; + error?: JsonError; steps: JsonTestStep[]; log?: string[]; }; diff --git a/src/web/htmlReport2/htmlReport.css b/src/web/htmlReport2/htmlReport.css index 46c1c4a653..3afea0ad45 100644 --- a/src/web/htmlReport2/htmlReport.css +++ b/src/web/htmlReport2/htmlReport.css @@ -52,7 +52,7 @@ padding: 5px; overflow: auto; margin: 20px 0; - flex: auto; + flex: none; } .status-icon { diff --git a/src/web/htmlReport2/htmlReport.tsx b/src/web/htmlReport2/htmlReport.tsx index 8568d24f98..38faa0b815 100644 --- a/src/web/htmlReport2/htmlReport.tsx +++ b/src/web/htmlReport2/htmlReport.tsx @@ -16,6 +16,7 @@ import './htmlReport.css'; import * as React from 'react'; +import ansi2html from 'ansi-to-html'; import { SplitView } from '../components/splitView'; import { TreeItem } from '../components/treeItem'; import { TabbedPane } from '../traceViewer/ui/tabbedPane'; @@ -166,6 +167,7 @@ const TestResultView: React.FC<{ result: TestResult, }> = ({ test, result }) => { return
+ {result.error && } {result.steps.map((step, i) => )}
; }; @@ -179,10 +181,13 @@ const StepTreeItem: React.FC<{ {step.title}
{msToString(step.duration)}
- } loadChildren={step.steps.length + (step.log || []).length ? () => { + } loadChildren={step.steps.length + (step.log || []).length + (step.error ? 1 : 0) ? () => { const stepChildren = step.steps.map((s, i) => ); const logChildren = (step.log || []).map((l, i) => ); - return [...stepChildren, ...logChildren]; + const children = [...stepChildren, ...logChildren]; + if (step.error) + children.unshift(); + return children; } : undefined} depth={depth}>; }; @@ -225,3 +230,35 @@ function retryLabel(index: number) { return 'Run'; return `Retry #${index}`; } + +const ErrorMessage: React.FC<{ + error: string; +}> = ({ error }) => { + const html = React.useMemo(() => { + return new ansi2html({ colors: ansiColors }).toHtml(escapeHTML(error)); + }, [error]); + return
; +}; + +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' +}; + +function escapeHTML(text: string): string { + return text.replace(/[&"<>]/g, c => ({ '&': '&', '"': '"', '<': '<', '>': '>' }[c]!)); +}