mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore(html): render steps and errors (#8826)
This commit is contained in:
parent
09afd50ab3
commit
ccff6e3036
@ -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:');
|
||||
|
||||
@ -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<string, JsonTestCase>();
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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[];
|
||||
};
|
||||
|
||||
@ -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[];
|
||||
};
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
padding: 5px;
|
||||
overflow: auto;
|
||||
margin: 20px 0;
|
||||
flex: auto;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
|
||||
@ -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 <div className='test-result'>
|
||||
{result.error && <ErrorMessage error={result.error}></ErrorMessage>}
|
||||
{result.steps.map((step, i) => <StepTreeItem key={i} step={step} depth={0}></StepTreeItem>)}
|
||||
</div>;
|
||||
};
|
||||
@ -179,10 +181,13 @@ const StepTreeItem: React.FC<{
|
||||
<span style={{ whiteSpace: 'pre' }}>{step.title}</span>
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
<div>{msToString(step.duration)}</div>
|
||||
</div>} loadChildren={step.steps.length + (step.log || []).length ? () => {
|
||||
</div>} loadChildren={step.steps.length + (step.log || []).length + (step.error ? 1 : 0) ? () => {
|
||||
const stepChildren = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1}></StepTreeItem>);
|
||||
const logChildren = (step.log || []).map((l, i) => <LogTreeItem key={step.steps.length + i} log={l} depth={depth + 1}></LogTreeItem>);
|
||||
return [...stepChildren, ...logChildren];
|
||||
const children = [...stepChildren, ...logChildren];
|
||||
if (step.error)
|
||||
children.unshift(<ErrorMessage error={step.error}></ErrorMessage>);
|
||||
return children;
|
||||
} : undefined} depth={depth}></TreeItem>;
|
||||
};
|
||||
|
||||
@ -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 <div className='error-message' dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
||||
};
|
||||
|
||||
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]!));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user