import * as fs from 'fs'; import * as path from 'path'; import { TestResult, TestModule, TestSuite, TestCase, TestStatus, TDDIteration } from '../models/test-result'; import { logger } from '../utils/logger'; export interface ReportConfig { outputDir: string; formats: ('html' | 'json' | 'junit' | 'markdown')[]; includeScreenshots: boolean; includeVideos: boolean; } export class ReportGenerator { private config: ReportConfig; constructor(config?: Partial) { this.config = { outputDir: config?.outputDir ?? 'test-results/reports', formats: config?.formats ?? ['html', 'json', 'markdown'], includeScreenshots: config?.includeScreenshots ?? true, includeVideos: config?.includeVideos ?? true }; this.ensureOutputDir(); } private ensureOutputDir(): void { try { if (!fs.existsSync(this.config.outputDir)) { fs.mkdirSync(this.config.outputDir, { recursive: true }); } } catch (error) { logger.error('创建报告目录失败', error); } } async generate(result: TestResult, iterations?: TDDIteration[]): Promise { logger.section('生成测试报告'); this.ensureOutputDir(); const generatedFiles: string[] = []; for (const format of this.config.formats) { try { const filePath = await this.generateFormat(result, format, iterations); generatedFiles.push(filePath); logger.info(`生成 ${format.toUpperCase()} 报告: ${filePath}`); } catch (error) { logger.error(`生成 ${format.toUpperCase()} 报告失败`, error); } } return generatedFiles; } private async generateFormat( result: TestResult, format: 'html' | 'json' | 'junit' | 'markdown', iterations?: TDDIteration[] ): Promise { switch (format) { case 'html': return this.generateHTMLReport(result, iterations); case 'json': return this.generateJSONReport(result, iterations); case 'junit': return this.generateJUnitReport(result); case 'markdown': return this.generateMarkdownReport(result, iterations); default: throw new Error(`不支持的报告格式: ${format}`); } } private generateHTMLReport(result: TestResult, iterations?: TDDIteration[]): string { const filePath = path.join(this.config.outputDir, 'test-report.html'); const html = ` E2E 测试报告

E2E 自动化测试报告

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

总测试数

${result.totalTests}

通过

${result.passedTests}

失败

${result.failedTests}

跳过

${result.skippedTests}

通过率

${result.passRate.toFixed(1)}%
${result.modules.map(module => this.renderModule(module)).join('')}
${iterations && iterations.length > 0 ? this.renderIterations(iterations) : ''}
`; fs.writeFileSync(filePath, html, 'utf-8'); return filePath; } private renderModule(module: TestModule): string { return `
${module.name.toUpperCase()} 模块 ${module.passedTests}/${module.totalTests} 通过 (${module.passRate.toFixed(1)}%)
${module.suites.map(suite => this.renderSuite(suite)).join('')}
`; } private renderSuite(suite: TestSuite): string { return `
${suite.name} ${suite.status}
${suite.tests.map(test => this.renderTestCase(test)).join('')}
`; } private renderTestCase(test: TestCase): string { return `
${test.name}
${test.status} ${(test.duration / 1000).toFixed(2)}s
${test.error ? `
${this.escapeHtml(test.error.message)}
` : ''} `; } private renderIterations(iterations: TDDIteration[]): string { return `

TDD 迭代记录

${iterations.map((iter, index) => `

迭代 ${index + 1}

修复数量: ${iter.fixes.length}

通过率变化: ${iter.previousResult.passRate.toFixed(1)}% → ${iter.currentResult.passRate.toFixed(1)}%

`).join('')}
`; } private escapeHtml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } private generateJSONReport(result: TestResult, iterations?: TDDIteration[]): string { const filePath = path.join(this.config.outputDir, 'test-report.json'); const report = { summary: { totalTests: result.totalTests, passedTests: result.passedTests, failedTests: result.failedTests, skippedTests: result.skippedTests, passRate: result.passRate, duration: result.duration, startTime: result.startTime, endTime: result.endTime, status: result.status }, environment: result.environment, modules: result.modules.map(module => ({ name: module.name, totalTests: module.totalTests, passedTests: module.passedTests, failedTests: module.failedTests, skippedTests: module.skippedTests, passRate: module.passRate, duration: module.duration, suites: module.suites.map(suite => ({ name: suite.name, status: suite.status, duration: suite.duration, passRate: suite.passRate, tests: suite.tests.map(test => ({ id: test.id, name: test.name, status: test.status, duration: test.duration, error: test.error ? { type: test.error.type, message: test.error.message } : null })) })) })), iterations: iterations?.map((iter, index) => ({ iteration: index + 1, fixesCount: iter.fixes.length, previousPassRate: iter.previousResult.passRate, currentPassRate: iter.currentResult.passRate, timestamp: iter.timestamp })) }; fs.writeFileSync(filePath, JSON.stringify(report, null, 2), 'utf-8'); return filePath; } private generateJUnitReport(result: TestResult): string { const filePath = path.join(this.config.outputDir, 'junit-report.xml'); let xml = '\n'; xml += `\n`; for (const module of result.modules) { for (const suite of module.suites) { xml += ` \n`; for (const test of suite.tests) { xml += ` \n`; xml += ` ${this.escapeXml(test.error.stack || test.error.message)}\n`; xml += ' \n'; xml += ' \n'; } else { xml += '/>\n'; } } xml += ' \n'; } } xml += '\n'; fs.writeFileSync(filePath, xml, 'utf-8'); return filePath; } private generateMarkdownReport(result: TestResult, iterations?: TDDIteration[]): string { const filePath = path.join(this.config.outputDir, 'test-report.md'); let md = '# E2E 自动化测试报告\n\n'; md += `**生成时间**: ${result.startTime.toLocaleString('zh-CN')}\n\n`; md += '## 测试概览\n\n'; md += '| 指标 | 数值 |\n'; md += '|------|------|\n'; md += `| 总测试数 | ${result.totalTests} |\n`; md += `| 通过数 | ${result.passedTests} |\n`; md += `| 失败数 | ${result.failedTests} |\n`; md += `| 跳过数 | ${result.skippedTests} |\n`; md += `| 通过率 | ${result.passRate.toFixed(2)}% |\n`; md += `| 执行时间 | ${(result.duration / 1000).toFixed(2)}s |\n\n`; md += '## 模块详情\n\n'; for (const module of result.modules) { md += `### ${module.name.toUpperCase()} 模块\n\n`; md += `- 通过率: ${module.passRate.toFixed(2)}%\n`; md += `- 测试数: ${module.totalTests}\n`; md += `- 通过: ${module.passedTests}\n`; md += `- 失败: ${module.failedTests}\n\n`; if (module.suites.length > 0) { md += '| 测试用例 | 状态 | 耗时 |\n'; md += '|----------|------|------|\n'; for (const suite of module.suites) { for (const test of suite.tests) { const statusEmoji = test.status === TestStatus.PASSED ? '✅' : test.status === TestStatus.FAILED ? '❌' : '⏭️'; md += `| ${test.name} | ${statusEmoji} ${test.status} | ${(test.duration / 1000).toFixed(2)}s |\n`; } } md += '\n'; } } if (iterations && iterations.length > 0) { md += '## TDD 迭代记录\n\n'; for (let i = 0; i < iterations.length; i++) { const iter = iterations[i]; md += `### 迭代 ${i + 1}\n\n`; md += `- 修复数量: ${iter.fixes.length}\n`; md += `- 通过率变化: ${iter.previousResult.passRate.toFixed(2)}% → ${iter.currentResult.passRate.toFixed(2)}%\n\n`; } } md += '## 环境信息\n\n'; md += `- Node.js: ${result.environment.nodeVersion}\n`; md += `- 操作系统: ${result.environment.os}\n`; md += `- API 地址: ${result.environment.apiBaseUrl}\n`; md += `- Admin 地址: ${result.environment.adminBaseUrl}\n`; md += `- Uniapp 地址: ${result.environment.uniappBaseUrl}\n`; fs.writeFileSync(filePath, md, 'utf-8'); return filePath; } private escapeXml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } }