Files
Charon/tests/reporters/debug-reporter.ts
2026-03-04 18:34:49 +00:00

152 lines
5.5 KiB
TypeScript

/**
* Debug Reporter for Playwright E2E Tests
*
* Custom reporter that:
* - Tracks test step timing and identifies slow operations
* - Aggregates failures by type (timeout, assertion, network)
* - Outputs structured summary to stdout for CI consumption
* - Logs timing statistics and slowest tests
*/
import { Reporter, TestCase, TestResult, Suite, FullResult } from '@playwright/test/reporter';
interface StepMetrics {
name: string;
duration: number;
status: 'passed' | 'failed' | 'skipped';
}
interface TestMetrics {
title: string;
duration: number;
steps: StepMetrics[];
status: 'passed' | 'failed' | 'skipped';
error?: string;
}
export default class DebugReporter implements Reporter {
private tests: TestMetrics[] = [];
private failuresByType = new Map<string, number>();
private slowTests: { title: string; duration: number }[] = [];
onTestEnd(test: TestCase, result: TestResult): void {
// Parse step information from result
const steps: StepMetrics[] = [];
if (result.steps && result.steps.length > 0) {
result.steps.forEach(step => {
steps.push({
name: step.title,
duration: step.duration,
status: step.error ? 'failed' : 'passed',
});
});
}
const metrics: TestMetrics = {
title: test.title,
duration: result.duration,
steps,
status: result.status as any,
error: result.error?.message,
};
this.tests.push(metrics);
// Track failure types
if (result.status === 'failed' && result.error) {
const errorMsg = result.error.message || '';
let failureType = 'other';
if (errorMsg.includes('timeout') || errorMsg.includes('Timeout')) {
failureType = 'timeout';
} else if (errorMsg.includes('assertion') || errorMsg.includes('Assertion')) {
failureType = 'assertion';
} else if (errorMsg.includes('network') || errorMsg.includes('Network')) {
failureType = 'network';
} else if (errorMsg.includes('not found') || errorMsg.includes('Cannot find')) {
failureType = 'locator';
}
this.failuresByType.set(failureType, (this.failuresByType.get(failureType) || 0) + 1);
}
// Track slow tests
if (result.duration > 5000) {
// Tests slower than 5 seconds
this.slowTests.push({
title: test.title,
duration: result.duration,
});
}
}
onEnd(result: FullResult): void {
// Sort slow tests by duration
this.slowTests.sort((a, b) => b.duration - a.duration);
// Print summary to stdout for CI parsing
this.printSummary();
this.printSlowTests();
this.printFailureAnalysis();
}
// ────────────────────────────────────────────────────────────────────
// Private methods
// ────────────────────────────────────────────────────────────────────
private printSummary(): void {
const total = this.tests.length;
const passed = this.tests.filter(t => t.status === 'passed').length;
const failed = this.tests.filter(t => t.status === 'failed').length;
const skipped = this.tests.filter(t => t.status === 'skipped').length;
const passRate = total > 0 ? Math.round((passed / total) * 100) : 0;
console.log('\n╔════════════════════════════════════════════════════════════╗');
console.log('║ E2E Test Execution Summary ║');
console.log('╠════════════════════════════════════════════════════════════╣');
console.log(`║ Total Tests: ${String(total).padEnd(42)}`);
console.log(`║ ✅ Passed: ${String(`${passed} (${passRate}%)`).padEnd(42)}`);
console.log(`║ ❌ Failed: ${String(failed).padEnd(42)}`);
console.log(`║ ⏭️ Skipped: ${String(skipped).padEnd(42)}`);
console.log('╚════════════════════════════════════════════════════════════╝\n');
}
private printSlowTests(): void {
if (this.slowTests.length === 0) {
return;
}
console.log('⏱️ Slow Tests (>5s):');
console.log('─'.repeat(60));
// Show top 10 slowest tests
this.slowTests.slice(0, 10).forEach((test, index) => {
const duration = (test.duration / 1000).toFixed(2);
const name = test.title.substring(0, 40).padEnd(40);
console.log(`${index + 1}. ${name} ${duration}s`);
});
console.log('');
}
private printFailureAnalysis(): void {
if (this.failuresByType.size === 0) {
return;
}
console.log('🔍 Failure Analysis by Type:');
console.log('─'.repeat(60));
const total = Array.from(this.failuresByType.values()).reduce((a, b) => a + b, 0);
this.failuresByType.forEach((count, type) => {
const percent = Math.round((count / total) * 100);
const bar = '█'.repeat(Math.round(percent / 5));
console.log(`${type.padEnd(12)}${bar.padEnd(20)} ${count}/${total} (${percent}%)`);
});
console.log('');
}
}