import { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from '@playwright/test/reporter'; import * as fs from 'fs'; import * as path from 'path'; class CustomReporter implements Reporter { private results: Map = new Map(); private suiteResults: Map = new Map(); private startTime: number = Date.now(); private testResults: TestResult[] = []; onBegin(config: FullConfig) { console.log(`๐Ÿš€ ๅผ€ๅง‹ๆต‹่ฏ•ๆ‰ง่กŒ: ${config.projects.map(p => p.name).join(', ')}`); this.startTime = Date.now(); } onTestBegin(test: TestCase, result: TestResult) { console.log(`๐Ÿ“ ๅผ€ๅง‹ๆต‹่ฏ•: ${test.title}`); } onTestEnd(test: TestCase, result: TestResult) { console.log(`โœ… ๆต‹่ฏ•ๅฎŒๆˆ: ${test.title} - ${result.status}`); this.testResults.push(result); } onEnd(result: FullResult) { const endTime = Date.now(); const duration = endTime - this.startTime; console.log(`๐ŸŽ‰ ๆต‹่ฏ•ๆ‰ง่กŒๅฎŒๆˆ`); console.log(`โฑ๏ธ ๆ€ป่€—ๆ—ถ: ${this.formatDuration(duration)}`); const stats = this.calculateStats(result); this.generateConsoleReport(stats); this.generateHtmlReport(result, stats); this.generateJsonReport(result, stats); } private calculateStats(result: FullResult): TestStats { const allTests = this.testResults; if (allTests.length === 0) { return { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0, passRate: 0, failRate: 0, skipRate: 0, flakyRate: 0, totalDuration: 0, avgDuration: 0, slowestTests: [], failedTests: [], }; } const passed = allTests.filter(t => t.status === 'passed'); const failed = allTests.filter(t => t.status === 'failed'); const skipped = allTests.filter(t => t.status === 'skipped'); const flaky = allTests.filter(t => t.status === 'passed' && t.retry >= 1); const totalDuration = allTests.reduce((sum, t) => sum + t.duration, 0); const avgDuration = totalDuration / allTests.length; const passRate = (passed.length / allTests.length) * 100; const failRate = (failed.length / allTests.length) * 100; const skipRate = (skipped.length / allTests.length) * 100; const flakyRate = (flaky.length / allTests.length) * 100; return { total: allTests.length, passed: passed.length, failed: failed.length, skipped: skipped.length, flaky: flaky.length, passRate, failRate, skipRate, flakyRate, totalDuration, avgDuration, slowestTests: allTests .filter(t => t.duration > 0) .sort((a, b) => b.duration - a.duration) .slice(0, 10), failedTests: failed, }; } private generateConsoleReport(stats: TestStats) { console.log(''); console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); console.log('๐Ÿ“Š ๆต‹่ฏ•็ปŸ่ฎกๆŠฅๅ‘Š'); console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•'); console.log(''); console.log(`๐Ÿ“ˆ ๆ€ปๆต‹่ฏ•ๆ•ฐ: ${stats.total}`); console.log(`โœ… ้€š่ฟ‡: ${stats.passed} (${stats.passRate.toFixed(2)}%)`); console.log(`โŒ ๅคฑ่ดฅ: ${stats.failed} (${stats.failRate.toFixed(2)}%)`); console.log(`โญ๏ธ ่ทณ่ฟ‡: ${stats.skipped} (${stats.skipRate.toFixed(2)}%)`); console.log(`๐Ÿ”„ ไธ็จณๅฎš: ${stats.flaky} (${stats.flakyRate.toFixed(2)}%)`); console.log(''); console.log(`โฑ๏ธ ๆ€ป่€—ๆ—ถ: ${this.formatDuration(stats.totalDuration)}`); console.log(`โฑ๏ธ ๅนณๅ‡่€—ๆ—ถ: ${this.formatDuration(stats.avgDuration)}`); console.log(''); console.log('๐ŸŒ ๆœ€ๆ…ข็š„10ไธชๆต‹่ฏ•:'); stats.slowestTests.forEach((test, index) => { console.log(` ${index + 1}. ${test.title} - ${this.formatDuration(test.duration || 0)}`); }); console.log(''); if (stats.failedTests.length > 0) { console.log('โŒ ๅคฑ่ดฅ็š„ๆต‹่ฏ•:'); stats.failedTests.forEach((test, index) => { console.log(` ${index + 1}. ${test.title || 'ๆœชๅ‘ฝๅๆต‹่ฏ•'}`); if (test.location?.file) { console.log(` ไฝ็ฝฎ: ${test.location.file}:${test.location.line || 0}`); } if (test.error?.message) { console.log(` ้”™่ฏฏ: ${test.error.message}`); } }); console.log(''); } } private generateHtmlReport(result: FullResult, stats: TestStats) { const html = ` ๆต‹่ฏ•ๆŠฅๅ‘Š - Novalon็ฎก็†็ณป็ปŸ

๐Ÿงช Novalon็ฎก็†็ณป็ปŸๆต‹่ฏ•ๆŠฅๅ‘Š

็”Ÿๆˆๆ—ถ้—ด: ${new Date().toLocaleString('zh-CN')}

้€š่ฟ‡ๆต‹่ฏ•

${stats.passed}
${stats.passRate.toFixed(2)}%

ๅคฑ่ดฅๆต‹่ฏ•

${stats.failed}
${stats.failRate.toFixed(2)}%

ไธ็จณๅฎšๆต‹่ฏ•

${stats.flaky}
${stats.flakyRate.toFixed(2)}%

ๆ€ปๆต‹่ฏ•ๆ•ฐ

${stats.total}
100%

๐Ÿ“ˆ ๆต‹่ฏ•็ปŸ่ฎก

  • ๆ€ป่€—ๆ—ถ
    ${this.formatDuration(stats.totalDuration)}
  • ๅนณๅ‡่€—ๆ—ถ
    ${this.formatDuration(stats.avgDuration)}
  • ่ทณ่ฟ‡ๆต‹่ฏ•
    ${stats.skipped} (${stats.skipRate.toFixed(2)}%)
${stats.failedTests.length > 0 ? `

โŒ ๅคฑ่ดฅๆต‹่ฏ•่ฏฆๆƒ…

    ${stats.failedTests.map(test => `
  • ${test.title}
    ${this.formatDuration(test.duration || 0)}
    ้”™่ฏฏ: ${test.error?.message || 'ๆœช็Ÿฅ้”™่ฏฏ'}
  • `).join('')}
` : ''}

๐ŸŒ ๆœ€ๆ…ข็š„10ไธชๆต‹่ฏ•

    ${stats.slowestTests.map((test, index) => `
  • ${index + 1}. ${test.title}
    ${this.formatDuration(test.duration || 0)}
  • `).join('')}
`; const reportPath = path.join(process.cwd(), 'test-results', 'custom-report.html'); fs.writeFileSync(reportPath, html, 'utf-8'); console.log(`๐Ÿ“„ HTMLๆŠฅๅ‘Šๅทฒ็”Ÿๆˆ: ${reportPath}`); } private generateJsonReport(result: FullResult, stats: TestStats) { const report = { summary: { timestamp: new Date().toISOString(), total: stats.total, passed: stats.passed, failed: stats.failed, skipped: stats.skipped, flaky: stats.flaky, passRate: stats.passRate, failRate: stats.failRate, skipRate: stats.skipRate, flakyRate: stats.flakyRate, totalDuration: stats.totalDuration, avgDuration: stats.avgDuration, }, failedTests: stats.failedTests.map(test => ({ title: test.title, location: test.location, error: test.error?.message, duration: test.duration, })), slowestTests: stats.slowestTests.map(test => ({ title: test.title, duration: test.duration, })), }; const reportPath = path.join(process.cwd(), 'test-results', 'custom-report.json'); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8'); console.log(`๐Ÿ“„ JSONๆŠฅๅ‘Šๅทฒ็”Ÿๆˆ: ${reportPath}`); } private formatDuration(ms: number): string { if (ms < 1000) { return `${ms}ms`; } else if (ms < 60000) { return `${(ms / 1000).toFixed(1)}s`; } else { return `${(ms / 60000).toFixed(1)}m`; } } } interface TestStats { total: number; passed: number; failed: number; skipped: number; flaky: number; passRate: number; failRate: number; skipRate: number; flakyRate: number; totalDuration: number; avgDuration: number; slowestTests: TestCase[]; } export default CustomReporter;