08ea5fbe98
添加用户管理视图、API和状态管理文件
251 lines
7.5 KiB
TypeScript
251 lines
7.5 KiB
TypeScript
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from '@playwright/test/reporter';
|
|
import * as colors from 'ansi-colors';
|
|
import * as readline from 'readline';
|
|
|
|
interface TestProgress {
|
|
total: number;
|
|
passed: number;
|
|
failed: number;
|
|
skipped: number;
|
|
started: number;
|
|
startTime: number;
|
|
tests: Map<string, TestResultInfo>;
|
|
}
|
|
|
|
interface TestResultInfo {
|
|
status: 'passed' | 'failed' | 'skipped' | 'running';
|
|
startTime: number;
|
|
duration: number;
|
|
file: string;
|
|
title: string[];
|
|
}
|
|
|
|
export class ProgressReporter implements Reporter {
|
|
private progress: TestProgress;
|
|
private interval: NodeJS.Timeout | null = null;
|
|
private lastUpdate: number = 0;
|
|
private updateInterval: number = 1000;
|
|
private showProgressBar: boolean = true;
|
|
private quietMode: boolean = false;
|
|
|
|
constructor(options?: { showProgressBar?: boolean; quietMode?: boolean; updateInterval?: number }) {
|
|
this.showProgressBar = options?.showProgressBar ?? true;
|
|
this.quietMode = options?.quietMode ?? false;
|
|
this.updateInterval = options?.updateInterval ?? 1000;
|
|
|
|
this.progress = {
|
|
total: 0,
|
|
passed: 0,
|
|
failed: 0,
|
|
skipped: 0,
|
|
started: 0,
|
|
startTime: 0,
|
|
tests: new Map()
|
|
};
|
|
}
|
|
|
|
onBegin(config: FullConfig, suite: Suite) {
|
|
this.progress.startTime = Date.now();
|
|
this.progress.total = this.countTotalTests(suite);
|
|
|
|
if (!this.quietMode) {
|
|
console.log('\n' + colors.bold('🧪 开始执行测试'));
|
|
console.log(colors.gray(`总测试数: ${this.progress.total}`));
|
|
console.log(colors.gray(`并行数: ${config.workers}`));
|
|
console.log('');
|
|
|
|
if (this.showProgressBar) {
|
|
this.startProgressUpdate();
|
|
}
|
|
}
|
|
}
|
|
|
|
onTestBegin(test: TestCase, result: TestResult) {
|
|
this.progress.started++;
|
|
this.progress.tests.set(test.id, {
|
|
status: 'running',
|
|
startTime: Date.now(),
|
|
duration: 0,
|
|
file: test.location.file,
|
|
title: test.titlePath()
|
|
});
|
|
|
|
if (!this.quietMode && this.showProgressBar) {
|
|
this.updateProgress();
|
|
}
|
|
}
|
|
|
|
onTestEnd(test: TestCase, result: TestResult) {
|
|
const testInfo = this.progress.tests.get(test.id);
|
|
if (testInfo) {
|
|
testInfo.status = result.status === 'passed' ? 'passed' :
|
|
result.status === 'failed' ? 'failed' : 'skipped';
|
|
testInfo.duration = result.duration;
|
|
|
|
if (result.status === 'passed') {
|
|
this.progress.passed++;
|
|
} else if (result.status === 'failed') {
|
|
this.progress.failed++;
|
|
} else {
|
|
this.progress.skipped++;
|
|
}
|
|
}
|
|
|
|
if (!this.quietMode && this.showProgressBar) {
|
|
this.updateProgress();
|
|
}
|
|
}
|
|
|
|
onEnd(result: FullResult) {
|
|
if (this.interval) {
|
|
clearInterval(this.interval);
|
|
this.interval = null;
|
|
}
|
|
|
|
const duration = Date.now() - this.progress.startTime;
|
|
|
|
if (!this.quietMode) {
|
|
if (this.showProgressBar) {
|
|
this.clearProgress();
|
|
}
|
|
|
|
this.printSummary(result, duration);
|
|
}
|
|
}
|
|
|
|
private countTotalTests(suite: Suite): number {
|
|
let count = 0;
|
|
for (const child of suite.suites) {
|
|
count += this.countTotalTests(child);
|
|
}
|
|
count += suite.tests.length;
|
|
return count;
|
|
}
|
|
|
|
private startProgressUpdate(): void {
|
|
this.interval = setInterval(() => {
|
|
this.updateProgress();
|
|
}, this.updateInterval);
|
|
}
|
|
|
|
private updateProgress(): void {
|
|
const now = Date.now();
|
|
if (now - this.lastUpdate < this.updateInterval) {
|
|
return;
|
|
}
|
|
|
|
this.lastUpdate = now;
|
|
this.printProgressBar();
|
|
}
|
|
|
|
private printProgressBar(): void {
|
|
const { total, passed, failed, skipped, started, startTime } = this.progress;
|
|
const completed = passed + failed + skipped;
|
|
const percentage = total > 0 ? (completed / total) * 100 : 0;
|
|
const elapsed = (now() - startTime) / 1000;
|
|
const avgTime = completed > 0 ? elapsed / completed : 0;
|
|
const remaining = (total - completed) * avgTime;
|
|
|
|
const barWidth = 40;
|
|
const filledWidth = Math.round((completed / total) * barWidth);
|
|
const emptyWidth = barWidth - filledWidth;
|
|
|
|
const bar = colors.green('█'.repeat(filledWidth)) +
|
|
colors.gray('░'.repeat(emptyWidth));
|
|
|
|
const statusLine = [
|
|
colors.bold(`[${percentage.toFixed(1)}%]`),
|
|
bar,
|
|
colors.green(`✓ ${passed}`),
|
|
failed > 0 ? colors.red(`✗ ${failed}`) : colors.gray(`✗ ${failed}`),
|
|
colors.yellow(`⊘ ${skipped}`),
|
|
colors.gray(`⏱ ${elapsed.toFixed(1)}s`),
|
|
remaining > 0 ? colors.gray(`⏳ ${remaining.toFixed(1)}s`) : ''
|
|
].filter(Boolean).join(' ');
|
|
|
|
readline.cursorTo(process.stdout, 0);
|
|
process.stdout.write(statusLine);
|
|
}
|
|
|
|
private clearProgress(): void {
|
|
readline.cursorTo(process.stdout, 0);
|
|
readline.clearLine(process.stdout, 0);
|
|
}
|
|
|
|
private printSummary(result: FullResult, duration: number): void {
|
|
const { total, passed, failed, skipped } = this.progress;
|
|
const passRate = total > 0 ? (passed / total) * 100 : 0;
|
|
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log(colors.bold('📊 测试执行完成'));
|
|
console.log('='.repeat(60));
|
|
console.log(colors.gray(`执行时间: ${(duration / 1000).toFixed(2)}秒`));
|
|
console.log('');
|
|
console.log(colors.bold('📈 测试结果:'));
|
|
console.log(` 总测试数: ${colors.bold(total)}`);
|
|
console.log(` 通过测试: ${colors.green(passed)} (${(passed / total * 100).toFixed(2)}%)`);
|
|
console.log(` 失败测试: ${failed > 0 ? colors.red(failed) : colors.gray(failed)} (${(failed / total * 100).toFixed(2)}%)`);
|
|
console.log(` 跳过测试: ${colors.yellow(skipped)} (${(skipped / total * 100).toFixed(2)}%)`);
|
|
console.log(` 通过率: ${passRate >= 80 ? colors.green : passRate >= 60 ? colors.yellow : colors.red}${passRate.toFixed(2)}%`);
|
|
console.log('');
|
|
|
|
if (failed > 0) {
|
|
console.log(colors.bold('❌ 失败的测试:'));
|
|
this.printFailedTests();
|
|
console.log('');
|
|
}
|
|
|
|
if (skipped > 0) {
|
|
console.log(colors.bold('⚠️ 跳过的测试:'));
|
|
this.printSkippedTests();
|
|
console.log('');
|
|
}
|
|
|
|
console.log('='.repeat(60));
|
|
|
|
if (result.status === 'passed') {
|
|
console.log(colors.green.bold('✅ 所有测试通过'));
|
|
} else {
|
|
console.log(colors.red.bold('❌ 存在失败的测试'));
|
|
}
|
|
|
|
console.log('='.repeat(60) + '\n');
|
|
}
|
|
|
|
private printFailedTests(): void {
|
|
const failedTests = Array.from(this.progress.tests.entries())
|
|
.filter(([_, info]) => info.status === 'failed')
|
|
.slice(0, 10);
|
|
|
|
failedTests.forEach(([testId, info]) => {
|
|
const fileName = info.file.split('/').pop();
|
|
console.log(` ${colors.red('✗')} ${colors.bold(fileName)}: ${info.title.join(' > ')}`);
|
|
});
|
|
|
|
if (failedTests.length >= 10) {
|
|
console.log(` ${colors.gray(`... 还有 ${this.progress.failed - 10} 个失败的测试`)}`);
|
|
}
|
|
}
|
|
|
|
private printSkippedTests(): void {
|
|
const skippedTests = Array.from(this.progress.tests.entries())
|
|
.filter(([_, info]) => info.status === 'skipped')
|
|
.slice(0, 5);
|
|
|
|
skippedTests.forEach(([testId, info]) => {
|
|
const fileName = info.file.split('/').pop();
|
|
console.log(` ${colors.yellow('⊘')} ${colors.bold(fileName)}: ${info.title.join(' > ')}`);
|
|
});
|
|
|
|
if (skippedTests.length >= 5) {
|
|
console.log(` ${colors.gray(`... 还有 ${this.progress.skipped - 5} 个跳过的测试`)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function now(): number {
|
|
return Date.now();
|
|
}
|
|
|
|
export default ProgressReporter;
|