08ea5fbe98
添加用户管理视图、API和状态管理文件
209 lines
6.5 KiB
TypeScript
209 lines
6.5 KiB
TypeScript
import { promises as fs } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
export class TestReporter {
|
|
private results: TestResult[] = [];
|
|
private startTime: number = 0;
|
|
|
|
startReport(): void {
|
|
this.startTime = Date.now();
|
|
console.log('📊 开始生成测试报告');
|
|
}
|
|
|
|
recordResult(result: TestResult): void {
|
|
this.results.push(result);
|
|
}
|
|
|
|
async generateAllReports(outputDir: string): Promise<void> {
|
|
await this.generateJSONReport(join(outputDir, 'test-results.json'));
|
|
await this.generateHTMLReport(join(outputDir, 'test-results.html'));
|
|
await this.generateJUnitReport(join(outputDir, 'junit-report.xml'));
|
|
await this.generateSummaryReport(outputDir);
|
|
}
|
|
|
|
async generateJSONReport(outputPath: string): Promise<void> {
|
|
const report = {
|
|
summary: this.getSummary(),
|
|
results: this.results,
|
|
metadata: {
|
|
generatedAt: new Date().toISOString(),
|
|
duration: Date.now() - this.startTime
|
|
}
|
|
};
|
|
|
|
await fs.writeFile(outputPath, JSON.stringify(report, null, 2));
|
|
console.log(`✅ JSON报告已生成: ${outputPath}`);
|
|
}
|
|
|
|
async generateHTMLReport(outputPath: string): Promise<void> {
|
|
const summary = this.getSummary();
|
|
const html = this.generateHTML(summary, this.results);
|
|
|
|
await fs.writeFile(outputPath, html);
|
|
console.log(`✅ HTML报告已生成: ${outputPath}`);
|
|
}
|
|
|
|
async generateJUnitReport(outputPath: string): Promise<void> {
|
|
const summary = this.getSummary();
|
|
const xml = this.generateJUnitXML(summary, this.results);
|
|
|
|
await fs.writeFile(outputPath, xml);
|
|
console.log(`✅ JUnit报告已生成: ${outputPath}`);
|
|
}
|
|
|
|
async generateSummaryReport(outputDir: string): Promise<void> {
|
|
const summary = this.getSummary();
|
|
const summaryPath = join(outputDir, 'summary.txt');
|
|
const summaryText = this.generateSummaryText(summary);
|
|
|
|
await fs.writeFile(summaryPath, summaryText);
|
|
console.log(`✅ 摘要报告已生成: ${summaryPath}`);
|
|
}
|
|
|
|
private getSummary(): TestSummary {
|
|
const passed = this.results.filter(r => r.status === 'passed').length;
|
|
const failed = this.results.filter(r => r.status === 'failed').length;
|
|
const skipped = this.results.filter(r => r.status === 'skipped').length;
|
|
const total = this.results.length;
|
|
const duration = this.results.reduce((sum, r) => sum + r.duration, 0);
|
|
|
|
return {
|
|
total,
|
|
passed,
|
|
failed,
|
|
skipped,
|
|
passRate: total > 0 ? (passed / total * 100).toFixed(2) : '0',
|
|
duration,
|
|
startTime: new Date(this.startTime).toISOString(),
|
|
endTime: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
private generateHTML(summary: TestSummary, results: TestResult[]): string {
|
|
return `
|
|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>E2E测试报告</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
.summary { background: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
|
|
.passed { color: green; }
|
|
.failed { color: red; }
|
|
.skipped { color: orange; }
|
|
.test-result { border: 1px solid #ddd; padding: 10px; margin: 10px 0; border-radius: 5px; }
|
|
.test-result.failed { background: #ffe6e6; }
|
|
.test-result.passed { background: #e6ffe6; }
|
|
.test-result.skipped { background: #fff3cd; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
|
th { background: #f2f2f2; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>E2E测试报告</h1>
|
|
<div class="summary">
|
|
<h2>测试摘要</h2>
|
|
<p>总测试数: ${summary.total}</p>
|
|
<p>通过: <span class="passed">${summary.passed}</span></p>
|
|
<p>失败: <span class="failed">${summary.failed}</span></p>
|
|
<p>跳过: <span class="skipped">${summary.skipped}</span></p>
|
|
<p>通过率: ${summary.passRate}%</p>
|
|
<p>总耗时: ${summary.duration}ms</p>
|
|
</div>
|
|
<h2>测试结果</h2>
|
|
${results.map(result => `
|
|
<div class="test-result ${result.status}">
|
|
<h3>${result.testName}</h3>
|
|
<p>状态: <span class="${result.status}">${result.status}</span></p>
|
|
<p>耗时: ${result.duration}ms</p>
|
|
${result.error ? `<p>错误: ${result.error.message}</p>` : ''}
|
|
${result.steps.length > 0 ? `
|
|
<h4>测试步骤</h4>
|
|
<ul>
|
|
${result.steps.map(step => `
|
|
<li>${step.name} - <span class="${step.status}">${step.status}</span></li>
|
|
`).join('')}
|
|
</ul>
|
|
` : ''}
|
|
</div>
|
|
`).join('')}
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
private generateJUnitXML(summary: TestSummary, results: TestResult[]): string {
|
|
const testCases = results.map(result => {
|
|
const testCase = `
|
|
<testcase name="${result.testName}" time="${result.duration / 1000}">
|
|
${result.status === 'failed' ? `
|
|
<failure message="${result.error?.message || 'Test failed'}">
|
|
${result.error?.stack || ''}
|
|
</failure>
|
|
` : ''}
|
|
${result.status === 'skipped' ? '<skipped/>' : ''}
|
|
</testcase>`;
|
|
return testCase;
|
|
}).join('\n');
|
|
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
<testsuites>
|
|
<testsuite name="E2E Tests" tests="${summary.total}" failures="${summary.failed}" skipped="${summary.skipped}" time="${summary.duration / 1000}">
|
|
${testCases}
|
|
</testsuite>
|
|
</testsuites>`;
|
|
}
|
|
|
|
private generateSummaryText(summary: TestSummary): string {
|
|
return `
|
|
========================================
|
|
E2E测试摘要报告
|
|
========================================
|
|
|
|
测试时间: ${summary.startTime} - ${summary.endTime}
|
|
总耗时: ${summary.duration}ms
|
|
|
|
测试统计:
|
|
----------------------------------------
|
|
总测试数: ${summary.total}
|
|
通过: ${summary.passed}
|
|
失败: ${summary.failed}
|
|
跳过: ${summary.skipped}
|
|
通过率: ${summary.passRate}%
|
|
========================================
|
|
`;
|
|
}
|
|
}
|
|
|
|
export interface TestResult {
|
|
testName: string;
|
|
status: 'passed' | 'failed' | 'skipped';
|
|
duration: number;
|
|
steps: TestStep[];
|
|
logs: string[];
|
|
screenshots: string[];
|
|
error?: Error;
|
|
}
|
|
|
|
export interface TestStep {
|
|
name: string;
|
|
status: 'passed' | 'failed';
|
|
duration: number;
|
|
}
|
|
|
|
export interface TestSummary {
|
|
total: number;
|
|
passed: number;
|
|
failed: number;
|
|
skipped: number;
|
|
passRate: string;
|
|
duration: number;
|
|
startTime: string;
|
|
endTime: string;
|
|
}
|
|
|
|
export const testReporter = new TestReporter();
|