import { test } from '@playwright/test'; import { testLogger } from '../shared/utils/test-logger'; import * as fs from 'fs'; import * as path from 'path'; export interface TestCoverage { totalTests: number; passedTests: number; failedTests: number; skippedTests: number; passRate: number; testSuites: TestSuiteCoverage[]; executionTime: number; timestamp: string; } export interface TestSuiteCoverage { name: string; totalTests: number; passedTests: number; failedTests: number; skippedTests: number; passRate: number; tests: TestCaseCoverage[]; } export interface TestCaseCoverage { name: string; status: 'passed' | 'failed' | 'skipped'; duration: number; tags: string[]; file: string; } export class TestCoverageReporter { private coverageData: TestCoverage; private testResults: Map = new Map(); private suiteResults: Map = new Map(); private startTime: number = 0; private endTime: number = 0; constructor() { this.coverageData = { totalTests: 0, passedTests: 0, failedTests: 0, skippedTests: 0, passRate: 0, testSuites: [], executionTime: 0, timestamp: new Date().toISOString() }; } startCoverage(): void { this.startTime = Date.now(); testLogger.info('开始收集测试覆盖率数据'); } endCoverage(): void { this.endTime = Date.now(); this.coverageData.executionTime = this.endTime - this.startTime; this.calculateCoverage(); this.generateReport(); testLogger.info('测试覆盖率收集完成'); testLogger.info(`总测试数: ${this.coverageData.totalTests}`); testLogger.info(`通过测试数: ${this.coverageData.passedTests}`); testLogger.info(`失败测试数: ${this.coverageData.failedTests}`); testLogger.info(`跳过测试数: ${this.coverageData.skippedTests}`); testLogger.info(`通过率: ${this.coverageData.passRate.toFixed(2)}%`); } recordTestResult(suiteName: string, testName: string, status: 'passed' | 'failed' | 'skipped', duration: number, tags: string[], file: string): void { const testCase: TestCaseCoverage = { name: testName, status, duration, tags, file }; if (!this.testResults.has(suiteName)) { this.testResults.set(suiteName, []); } this.testResults.get(suiteName)!.push(testCase); } private calculateCoverage(): void { let totalTests = 0; let passedTests = 0; let failedTests = 0; let skippedTests = 0; for (const [suiteName, testCases] of this.testResults.entries()) { const suiteCoverage = this.calculateSuiteCoverage(suiteName, testCases); this.suiteResults.set(suiteName, suiteCoverage); totalTests += suiteCoverage.totalTests; passedTests += suiteCoverage.passedTests; failedTests += suiteCoverage.failedTests; skippedTests += suiteCoverage.skippedTests; } this.coverageData.totalTests = totalTests; this.coverageData.passedTests = passedTests; this.coverageData.failedTests = failedTests; this.coverageData.skippedTests = skippedTests; this.coverageData.passRate = totalTests > 0 ? (passedTests / totalTests) * 100 : 0; this.coverageData.testSuites = Array.from(this.suiteResults.values()); } private calculateSuiteCoverage(suiteName: string, testCases: TestCaseCoverage[]): TestSuiteCoverage { const totalTests = testCases.length; const passedTests = testCases.filter(tc => tc.status === 'passed').length; const failedTests = testCases.filter(tc => tc.status === 'failed').length; const skippedTests = testCases.filter(tc => tc.status === 'skipped').length; const passRate = totalTests > 0 ? (passedTests / totalTests) * 100 : 0; return { name: suiteName, totalTests, passedTests, failedTests, skippedTests, passRate, tests: testCases }; } private generateReport(): void { const reportDir = path.join(process.cwd(), 'test-results', 'coverage'); if (!fs.existsSync(reportDir)) { fs.mkdirSync(reportDir, { recursive: true }); } this.generateJSONReport(reportDir); this.generateHTMLReport(reportDir); this.generateMarkdownReport(reportDir); this.generateConsoleReport(); } private generateJSONReport(reportDir: string): void { const jsonPath = path.join(reportDir, 'coverage.json'); fs.writeFileSync(jsonPath, JSON.stringify(this.coverageData, null, 2), 'utf-8'); testLogger.info(`JSON覆盖率报告已生成: ${jsonPath}`); } private generateHTMLReport(reportDir: string): void { const htmlPath = path.join(reportDir, 'coverage.html'); const html = this.generateHTMLContent(); fs.writeFileSync(htmlPath, html, 'utf-8'); testLogger.info(`HTML覆盖率报告已生成: ${htmlPath}`); } private generateMarkdownReport(reportDir: string): void { const mdPath = path.join(reportDir, 'coverage.md'); const markdown = this.generateMarkdownContent(); fs.writeFileSync(mdPath, markdown, 'utf-8'); testLogger.info(`Markdown覆盖率报告已生成: ${mdPath}`); } private generateHTMLContent(): string { const { totalTests, passedTests, failedTests, skippedTests, passRate, executionTime, testSuites, timestamp } = this.coverageData; const passRateColor = passRate >= 80 ? '#52c41a' : passRate >= 60 ? '#faad14' : '#f5222d'; const passRateClass = passRate >= 80 ? 'success' : passRate >= 60 ? 'warning' : 'danger'; return ` 测试覆盖率报告

🧪 测试覆盖率报告

生成时间: ${timestamp}

执行时间: ${(executionTime / 1000).toFixed(2)}秒

总测试数
${totalTests}
通过测试
${passedTests}
失败测试
${failedTests}
跳过测试
${skippedTests}
通过率
${passRate.toFixed(2)}%

📊 测试套件详情

${testSuites.map(suite => `

${suite.name}

✓ ${suite.passedTests} ✗ ${suite.failedTests} ⊘ ${suite.skippedTests} ${suite.passRate.toFixed(1)}%
${suite.tests.map(testCase => `
${testCase.name}
${testCase.status}
${testCase.duration}ms
${testCase.tags.map(tag => `${tag}`).join('')}
`).join('')}
`).join('')}
`; } private generateMarkdownContent(): string { const { totalTests, passedTests, failedTests, skippedTests, passRate, executionTime, testSuites, timestamp } = this.coverageData; return `# 测试覆盖率报告 ## 概要 - **生成时间**: ${timestamp} - **执行时间**: ${(executionTime / 1000).toFixed(2)}秒 - **总测试数**: ${totalTests} - **通过测试**: ${passedTests} - **失败测试**: ${failedTests} - **跳过测试**: ${skippedTests} - **通过率**: ${passRate.toFixed(2)}% ## 测试套件详情 ${testSuites.map(suite => ` ### ${suite.name} - **总测试数**: ${suite.totalTests} - **通过测试**: ${suite.passedTests} - **失败测试**: ${suite.failedTests} - **跳过测试**: ${suite.skippedTests} - **通过率**: ${suite.passRate.toFixed(2)}% #### 测试用例 | 测试用例 | 状态 | 耗时 | 标签 | |---------|------|------|------| ${suite.tests.map(testCase => ` | ${testCase.name} | ${testCase.status} | ${testCase.duration}ms | ${testCase.tags.join(', ')} | `).join('')} `).join('')} ## 总结 ${passRate >= 80 ? '✅ 测试覆盖率优秀' : passRate >= 60 ? '⚠️ 测试覆盖率良好' : '❌ 测试覆盖率需要改进'} --- *Generated by TestCoverageReporter* `; } private generateConsoleReport(): void { const { totalTests, passedTests, failedTests, skippedTests, passRate, executionTime } = this.coverageData; console.log('\n' + '='.repeat(60)); console.log('📊 测试覆盖率报告'); console.log('='.repeat(60)); console.log(`生成时间: ${this.coverageData.timestamp}`); console.log(`执行时间: ${(executionTime / 1000).toFixed(2)}秒`); console.log(''); console.log('📈 总体统计:'); console.log(` 总测试数: ${totalTests}`); console.log(` 通过测试: ${passedTests} (${(passedTests / totalTests * 100).toFixed(2)}%)`); console.log(` 失败测试: ${failedTests} (${(failedTests / totalTests * 100).toFixed(2)}%)`); console.log(` 跳过测试: ${skippedTests} (${(skippedTests / totalTests * 100).toFixed(2)}%)`); console.log(` 通过率: ${passRate.toFixed(2)}%`); console.log(''); console.log('📋 测试套件详情:'); for (const suite of this.coverageData.testSuites) { console.log(`\n ${suite.name}:`); console.log(` 总测试数: ${suite.totalTests}`); console.log(` 通过测试: ${suite.passedTests} (${suite.passRate.toFixed(2)}%)`); console.log(` 失败测试: ${suite.failedTests}`); console.log(` 跳过测试: ${suite.skippedTests}`); } console.log('\n' + '='.repeat(60)); if (passRate >= 80) { console.log('✅ 测试覆盖率优秀'); } else if (passRate >= 60) { console.log('⚠️ 测试覆盖率良好'); } else { console.log('❌ 测试覆盖率需要改进'); } console.log('='.repeat(60) + '\n'); } getCoverage(): TestCoverage { return this.coverageData; } getSuiteCoverage(suiteName: string): TestSuiteCoverage | undefined { return this.suiteResults.get(suiteName); } exportCoverage(format: 'json' | 'html' | 'markdown' = 'json'): string { switch (format) { case 'json': return JSON.stringify(this.coverageData, null, 2); case 'html': return this.generateHTMLContent(); case 'markdown': return this.generateMarkdownContent(); default: return JSON.stringify(this.coverageData, null, 2); } } } export const testCoverageReporter = new TestCoverageReporter();