feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
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;
|
||||
@@ -0,0 +1,130 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user