import { FullConfig, Suite, TestCase, TestResult, Reporter } from '@playwright/test/reporter'; import colors from 'ansi-colors'; interface TestProgress { total: number; passed: number; failed: number; skipped: number; current: string; startTime: number; } class TestProgressBar { private progress: TestProgress; private barWidth: number = 40; private lastUpdate: number = 0; constructor(total: number) { this.progress = { total, passed: 0, failed: 0, skipped: 0, current: '', startTime: Date.now() }; } update(testName: string, result?: TestResult) { if (result) { if (result.status === 'passed') this.progress.passed++; else if (result.status === 'failed') this.progress.failed++; else if (result.status === 'skipped') this.progress.skipped++; } this.progress.current = testName; this.render(); } private render() { const now = Date.now(); if (now - this.lastUpdate < 100) return; this.lastUpdate = now; const completed = this.progress.passed + this.progress.failed + this.progress.skipped; const percentage = Math.min(100, Math.round((completed / this.progress.total) * 100)); const filled = Math.round((this.barWidth * percentage) / 100); const empty = this.barWidth - filled; const elapsed = Date.now() - this.progress.startTime; const elapsedSeconds = Math.floor(elapsed / 1000); const avgTime = completed > 0 ? elapsed / completed : 0; const remaining = (this.progress.total - completed) * avgTime; const remainingSeconds = Math.floor(remaining / 1000); const bar = colors.cyan('█').repeat(filled) + colors.gray('░').repeat(empty); const statusColor = this.progress.failed > 0 ? colors.red : colors.green; const statusText = statusColor(`✓ ${this.progress.passed} | ✗ ${this.progress.failed} | ⊘ ${this.progress.skipped}`); const timeText = colors.gray(`⏱ ${elapsedSeconds}s | ⏳ ~${remainingSeconds}s`); const currentText = colors.yellow(this.progress.current.substring(0, 50)); process.stdout.write('\r' + ' '.repeat(200)); process.stdout.write(`\r[${bar}] ${percentage}% | ${statusText} | ${timeText}`); process.stdout.write(`\n ${colors.blue('▶')} ${currentText}`); } finalize() { const elapsed = Date.now() - this.progress.startTime; const elapsedSeconds = (elapsed / 1000).toFixed(2); process.stdout.write('\r' + ' '.repeat(200)); process.stdout.write('\n'); const statusColor = this.progress.failed > 0 ? colors.red : colors.green; const statusText = statusColor( `测试完成: ${this.progress.passed} 通过, ${this.progress.failed} 失败, ${this.progress.skipped} 跳过` ); console.log(colors.bold('\n' + '═'.repeat(60))); console.log(colors.bold(' 测试执行完成')); console.log('═'.repeat(60)); console.log(` ${statusText}`); console.log(` ${colors.gray(`总用时: ${elapsedSeconds}秒`)}`); console.log(` ${colors.gray(`总测试数: ${this.progress.total}`)}`); console.log('═'.repeat(60) + '\n'); } } class ProgressReporter implements Reporter { private progressBar: TestProgressBar | null = null; private totalTests: number = 0; onBegin(config: FullConfig, suite: Suite) { this.totalTests = this.countTests(suite); console.log(colors.bold('\n' + '═'.repeat(60))); console.log(colors.bold(' 开始执行测试')); console.log('═'.repeat(60)); console.log(` ${colors.blue(`总测试数: ${this.totalTests}`)}`); console.log(` ${colors.gray(`测试套件: ${suite.allTests().length}`)}`); console.log('═'.repeat(60) + '\n'); this.progressBar = new TestProgressBar(this.totalTests); } onTestBegin(test: TestCase) { if (this.progressBar) { this.progressBar.update(test.title); } } onTestEnd(test: TestCase, result: TestResult) { if (this.progressBar) { this.progressBar.update(test.title, result); } } onEnd() { if (this.progressBar) { this.progressBar.finalize(); } } private countTests(suite: Suite): number { let count = 0; suite.allTests().forEach(() => count++); return count; } } export default ProgressReporter;