/** * 测试报告生成器 * 生成多种格式的测试报告:HTML、JSON、JUnit XML、Markdown */ import * as fs from 'fs'; import * as path from 'path'; import { TestResult, TestStep } from './test-logger'; export interface TestSuite { name: string; tests: TestResult[]; startTime?: string; endTime?: string; } export interface ReportSummary { totalTests: number; passed: number; failed: number; skipped: number; totalDuration: number; passRate: number; startTime: string; endTime: string; } class TestReporter { private suites: TestSuite[] = []; private startTime: string = ''; private endTime: string = ''; startReport(): void { this.startTime = new Date().toISOString(); this.suites = []; } endReport(): void { this.endTime = new Date().toISOString(); } addTestSuite(suite: TestSuite): void { this.suites.push(suite); } recordTestResult(test: TestResult): void { // 查找或创建测试套件 let suite = this.suites.find(s => s.name === 'Default Suite'); if (!suite) { suite = { name: 'Default Suite', tests: [] }; this.suites.push(suite); } suite.tests.push(test); } generateSummary(): ReportSummary { const allTests = this.suites.flatMap(s => s.tests); const passed = allTests.filter(t => t.status === 'passed').length; const failed = allTests.filter(t => t.status === 'failed').length; const skipped = allTests.filter(t => t.status === 'skipped').length; const totalDuration = allTests.reduce((sum, t) => sum + t.duration, 0); return { totalTests: allTests.length, passed, failed, skipped, totalDuration, passRate: allTests.length > 0 ? (passed / allTests.length) * 100 : 0, startTime: this.startTime, endTime: this.endTime, }; } /** * 生成JSON格式报告 */ generateJSONReport(outputPath: string): void { const report = { summary: this.generateSummary(), suites: this.suites, generatedAt: new Date().toISOString(), }; this.ensureDirectoryExists(path.dirname(outputPath)); fs.writeFileSync(outputPath, JSON.stringify(report, null, 2), 'utf-8'); console.log(`JSON报告已生成: ${outputPath}`); } /** * 生成HTML格式报告 */ generateHTMLReport(outputPath: string): void { const summary = this.generateSummary(); const html = this.buildHTMLReport(summary); this.ensureDirectoryExists(path.dirname(outputPath)); fs.writeFileSync(outputPath, html, 'utf-8'); console.log(`HTML报告已生成: ${outputPath}`); } /** * 生成JUnit XML格式报告 */ generateJUnitReport(outputPath: string): void { const summary = this.generateSummary(); const xml = this.buildJUnitReport(summary); this.ensureDirectoryExists(path.dirname(outputPath)); fs.writeFileSync(outputPath, xml, 'utf-8'); console.log(`JUnit报告已生成: ${outputPath}`); } /** * 生成Markdown格式报告 */ generateMarkdownReport(outputPath: string): void { const summary = this.generateSummary(); const markdown = this.buildMarkdownReport(summary); this.ensureDirectoryExists(path.dirname(outputPath)); fs.writeFileSync(outputPath, markdown, 'utf-8'); console.log(`Markdown报告已生成: ${outputPath}`); } /** * 生成所有报告 */ generateAllReports(outputDir: string): void { this.endReport(); this.generateJSONReport(path.join(outputDir, 'e2e-report.json')); this.generateHTMLReport(path.join(outputDir, 'e2e-report.html')); this.generateJUnitReport(path.join(outputDir, 'junit-report.xml')); this.generateMarkdownReport(path.join(outputDir, 'e2e-report.md')); // 打印摘要 this.printSummary(); } private printSummary(): void { const summary = this.generateSummary(); console.log('\n========== 测试执行摘要 =========='); console.log(`总测试数: ${summary.totalTests}`); console.log(`通过: ${summary.passed} ✅`); console.log(`失败: ${summary.failed} ❌`); console.log(`跳过: ${summary.skipped} ⏭️`); console.log(`通过率: ${summary.passRate.toFixed(2)}%`); console.log(`总耗时: ${(summary.totalDuration / 1000).toFixed(2)}s`); console.log('===================================\n'); } private ensureDirectoryExists(dirPath: string): void { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } private buildHTMLReport(summary: ReportSummary): string { const allTests = this.suites.flatMap(s => s.tests); const statusColor = { passed: '#28a745', failed: '#dc3545', skipped: '#ffc107', }; return ` E2E测试报告

E2E测试报告

生成时间: ${new Date().toLocaleString('zh-CN')}

总测试数

${summary.totalTests}

通过

${summary.passed}

失败

${summary.failed}

跳过

${summary.skipped}

通过率: ${summary.passRate.toFixed(2)}%

总耗时: ${(summary.totalDuration / 1000).toFixed(2)}秒

测试详情
${allTests.map(test => `
${test.testName} ${(test.duration / 1000).toFixed(2)}s
${test.status}
`).join('')}
`; } private buildJUnitReport(summary: ReportSummary): string { const allTests = this.suites.flatMap(s => s.tests); const failures = allTests.filter(t => t.status === 'failed').length; let xml = `\n`; xml += `\n`; this.suites.forEach(suite => { xml += ` \n`; suite.tests.forEach(test => { xml += ` \n`; if (test.status === 'failed' && test.error) { xml += ` \n`; xml += ` ${this.escapeXml(test.error.stack || '')}\n`; xml += ` \n`; } else if (test.status === 'skipped') { xml += ` \n`; } xml += ` \n`; }); xml += ` \n`; }); xml += ``; return xml; } private buildMarkdownReport(summary: ReportSummary): string { const allTests = this.suites.flatMap(s => s.tests); let md = `# E2E测试报告\n\n`; md += `**生成时间**: ${new Date().toLocaleString('zh-CN')}\n\n`; md += `## 执行摘要\n\n`; md += `| 指标 | 数值 |\n`; md += `|------|------|\n`; md += `| 总测试数 | ${summary.totalTests} |\n`; md += `| 通过 | ${summary.passed} ✅ |\n`; md += `| 失败 | ${summary.failed} ❌ |\n`; md += `| 跳过 | ${summary.skipped} ⏭️ |\n`; md += `| 通过率 | ${summary.passRate.toFixed(2)}% |\n`; md += `| 总耗时 | ${(summary.totalDuration / 1000).toFixed(2)}秒 |\n\n`; md += `## 测试详情\n\n`; md += `| 测试名称 | 状态 | 耗时 |\n`; md += `|----------|------|------|\n`; allTests.forEach(test => { const statusIcon = test.status === 'passed' ? '✅' : test.status === 'failed' ? '❌' : '⏭️'; md += `| ${test.testName} | ${statusIcon} ${test.status} | ${(test.duration / 1000).toFixed(2)}s |\n`; }); md += `\n---\n\n`; md += `*由 Playwright E2E 测试框架生成*\n`; return md; } private escapeXml(str: string): string { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } } export const testReporter = new TestReporter();