285 lines
9.2 KiB
TypeScript
285 lines
9.2 KiB
TypeScript
import { coloredString as cs } from '@push.rocks/consolecolor';
|
||
import * as plugins from './tstest.plugins.js';
|
||
|
||
export interface LogOptions {
|
||
quiet?: boolean;
|
||
verbose?: boolean;
|
||
noColor?: boolean;
|
||
json?: boolean;
|
||
logFile?: string;
|
||
}
|
||
|
||
export interface TestFileResult {
|
||
file: string;
|
||
passed: number;
|
||
failed: number;
|
||
total: number;
|
||
duration: number;
|
||
tests: Array<{
|
||
name: string;
|
||
passed: boolean;
|
||
duration: number;
|
||
error?: string;
|
||
}>;
|
||
}
|
||
|
||
export interface TestSummary {
|
||
totalFiles: number;
|
||
totalTests: number;
|
||
totalPassed: number;
|
||
totalFailed: number;
|
||
totalDuration: number;
|
||
fileResults: TestFileResult[];
|
||
}
|
||
|
||
export class TsTestLogger {
|
||
private options: LogOptions;
|
||
private startTime: number;
|
||
private fileResults: TestFileResult[] = [];
|
||
private currentFileResult: TestFileResult | null = null;
|
||
|
||
constructor(options: LogOptions = {}) {
|
||
this.options = options;
|
||
this.startTime = Date.now();
|
||
}
|
||
|
||
private format(text: string, color?: string): string {
|
||
if (this.options.noColor || !color) {
|
||
return text;
|
||
}
|
||
return cs(text, color as any);
|
||
}
|
||
|
||
private log(message: string) {
|
||
if (this.options.json) return;
|
||
console.log(message);
|
||
|
||
if (this.options.logFile) {
|
||
// TODO: Implement file logging
|
||
}
|
||
}
|
||
|
||
// Section separators
|
||
sectionStart(title: string) {
|
||
if (this.options.quiet || this.options.json) return;
|
||
this.log(this.format(`\n━━━ ${title} ━━━`, 'cyan'));
|
||
}
|
||
|
||
sectionEnd() {
|
||
if (this.options.quiet || this.options.json) return;
|
||
this.log(this.format('─'.repeat(50), 'dim'));
|
||
}
|
||
|
||
// Progress indication
|
||
progress(current: number, total: number, message: string) {
|
||
if (this.options.quiet || this.options.json) return;
|
||
const percentage = Math.round((current / total) * 100);
|
||
const filled = Math.round((current / total) * 20);
|
||
const empty = 20 - filled;
|
||
|
||
this.log(this.format(`\n📊 Progress: ${current}/${total} (${percentage}%)`, 'cyan'));
|
||
this.log(this.format(`[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${message}`, 'dim'));
|
||
}
|
||
|
||
// Test discovery
|
||
testDiscovery(count: number, pattern: string, executionMode: string) {
|
||
if (this.options.json) {
|
||
console.log(JSON.stringify({ event: 'discovery', count, pattern, executionMode }));
|
||
return;
|
||
}
|
||
|
||
if (this.options.quiet) {
|
||
this.log(`Found ${count} tests`);
|
||
} else {
|
||
this.log(this.format(`\n🔍 Test Discovery`, 'bold'));
|
||
this.log(this.format(` Mode: ${executionMode}`, 'dim'));
|
||
this.log(this.format(` Pattern: ${pattern}`, 'dim'));
|
||
this.log(this.format(` Found: ${count} test file(s)`, 'green'));
|
||
}
|
||
}
|
||
|
||
// Test execution
|
||
testFileStart(filename: string, runtime: string, index: number, total: number) {
|
||
this.currentFileResult = {
|
||
file: filename,
|
||
passed: 0,
|
||
failed: 0,
|
||
total: 0,
|
||
duration: 0,
|
||
tests: []
|
||
};
|
||
|
||
if (this.options.json) {
|
||
console.log(JSON.stringify({ event: 'fileStart', filename, runtime, index, total }));
|
||
return;
|
||
}
|
||
|
||
if (this.options.quiet) return;
|
||
|
||
this.log(this.format(`\n▶️ ${filename} (${index}/${total})`, 'blue'));
|
||
this.log(this.format(` Runtime: ${runtime}`, 'dim'));
|
||
}
|
||
|
||
testResult(testName: string, passed: boolean, duration: number, error?: string) {
|
||
if (this.currentFileResult) {
|
||
this.currentFileResult.tests.push({ name: testName, passed, duration, error });
|
||
this.currentFileResult.total++;
|
||
if (passed) {
|
||
this.currentFileResult.passed++;
|
||
} else {
|
||
this.currentFileResult.failed++;
|
||
}
|
||
this.currentFileResult.duration += duration;
|
||
}
|
||
|
||
if (this.options.json) {
|
||
console.log(JSON.stringify({ event: 'testResult', testName, passed, duration, error }));
|
||
return;
|
||
}
|
||
|
||
const icon = passed ? '✅' : '❌';
|
||
const color = passed ? 'green' : 'red';
|
||
|
||
if (this.options.quiet) {
|
||
this.log(`${icon} ${testName}`);
|
||
} else {
|
||
this.log(this.format(` ${icon} ${testName} (${duration}ms)`, color));
|
||
if (error && !passed) {
|
||
this.log(this.format(` ${error}`, 'red'));
|
||
}
|
||
}
|
||
}
|
||
|
||
testFileEnd(passed: number, failed: number, duration: number) {
|
||
if (this.currentFileResult) {
|
||
this.fileResults.push(this.currentFileResult);
|
||
this.currentFileResult = null;
|
||
}
|
||
|
||
if (this.options.json) {
|
||
console.log(JSON.stringify({ event: 'fileEnd', passed, failed, duration }));
|
||
return;
|
||
}
|
||
|
||
if (!this.options.quiet) {
|
||
const total = passed + failed;
|
||
const status = failed === 0 ? 'PASSED' : 'FAILED';
|
||
const color = failed === 0 ? 'green' : 'red';
|
||
this.log(this.format(` Summary: ${passed}/${total} ${status}`, color));
|
||
}
|
||
}
|
||
|
||
// TAP output forwarding
|
||
tapOutput(message: string, isError: boolean = false) {
|
||
if (this.options.json) return;
|
||
|
||
if (this.options.verbose || isError) {
|
||
const prefix = isError ? ' ⚠️ ' : ' ';
|
||
const color = isError ? 'red' : 'dim';
|
||
this.log(this.format(`${prefix}${message}`, color));
|
||
}
|
||
}
|
||
|
||
// Browser console
|
||
browserConsole(message: string, level: string = 'log') {
|
||
if (this.options.json) {
|
||
console.log(JSON.stringify({ event: 'browserConsole', message, level }));
|
||
return;
|
||
}
|
||
|
||
if (!this.options.quiet) {
|
||
const prefix = level === 'error' ? '🌐❌' : '🌐';
|
||
const color = level === 'error' ? 'red' : 'magenta';
|
||
this.log(this.format(` ${prefix} ${message}`, color));
|
||
}
|
||
}
|
||
|
||
// Final summary
|
||
summary() {
|
||
const totalDuration = Date.now() - this.startTime;
|
||
const summary: TestSummary = {
|
||
totalFiles: this.fileResults.length,
|
||
totalTests: this.fileResults.reduce((sum, r) => sum + r.total, 0),
|
||
totalPassed: this.fileResults.reduce((sum, r) => sum + r.passed, 0),
|
||
totalFailed: this.fileResults.reduce((sum, r) => sum + r.failed, 0),
|
||
totalDuration,
|
||
fileResults: this.fileResults
|
||
};
|
||
|
||
if (this.options.json) {
|
||
console.log(JSON.stringify({ event: 'summary', summary }));
|
||
return;
|
||
}
|
||
|
||
if (this.options.quiet) {
|
||
const status = summary.totalFailed === 0 ? 'PASSED' : 'FAILED';
|
||
this.log(`\nSummary: ${summary.totalPassed}/${summary.totalTests} | ${totalDuration}ms | ${status}`);
|
||
return;
|
||
}
|
||
|
||
// Detailed summary
|
||
this.log(this.format('\n📊 Test Summary', 'bold'));
|
||
this.log(this.format('┌────────────────────────────────┐', 'dim'));
|
||
this.log(this.format(`│ Total Files: ${summary.totalFiles.toString().padStart(14)} │`, 'white'));
|
||
this.log(this.format(`│ Total Tests: ${summary.totalTests.toString().padStart(14)} │`, 'white'));
|
||
this.log(this.format(`│ Passed: ${summary.totalPassed.toString().padStart(14)} │`, 'green'));
|
||
this.log(this.format(`│ Failed: ${summary.totalFailed.toString().padStart(14)} │`, summary.totalFailed > 0 ? 'red' : 'green'));
|
||
this.log(this.format(`│ Duration: ${totalDuration.toString().padStart(14)}ms │`, 'white'));
|
||
this.log(this.format('└────────────────────────────────┘', 'dim'));
|
||
|
||
// File results
|
||
if (summary.totalFailed > 0) {
|
||
this.log(this.format('\n❌ Failed Tests:', 'red'));
|
||
this.fileResults.forEach(fileResult => {
|
||
if (fileResult.failed > 0) {
|
||
this.log(this.format(`\n ${fileResult.file}`, 'yellow'));
|
||
fileResult.tests.filter(t => !t.passed).forEach(test => {
|
||
this.log(this.format(` ❌ ${test.name}`, 'red'));
|
||
if (test.error) {
|
||
this.log(this.format(` ${test.error}`, 'dim'));
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// Performance metrics
|
||
if (this.options.verbose) {
|
||
const avgDuration = Math.round(totalDuration / summary.totalTests);
|
||
const slowestTest = this.fileResults
|
||
.flatMap(r => r.tests)
|
||
.sort((a, b) => b.duration - a.duration)[0];
|
||
|
||
this.log(this.format('\n⏱️ Performance Metrics:', 'cyan'));
|
||
this.log(this.format(` Average per test: ${avgDuration}ms`, 'white'));
|
||
if (slowestTest) {
|
||
this.log(this.format(` Slowest test: ${slowestTest.name} (${slowestTest.duration}ms)`, 'yellow'));
|
||
}
|
||
}
|
||
|
||
// Final status
|
||
const status = summary.totalFailed === 0 ? 'ALL TESTS PASSED! 🎉' : 'SOME TESTS FAILED! ❌';
|
||
const statusColor = summary.totalFailed === 0 ? 'green' : 'red';
|
||
this.log(this.format(`\n${status}`, statusColor));
|
||
}
|
||
|
||
// Error display
|
||
error(message: string, file?: string, stack?: string) {
|
||
if (this.options.json) {
|
||
console.log(JSON.stringify({ event: 'error', message, file, stack }));
|
||
return;
|
||
}
|
||
|
||
if (this.options.quiet) {
|
||
console.error(`ERROR: ${message}`);
|
||
} else {
|
||
this.log(this.format('\n⚠️ Error', 'red'));
|
||
if (file) this.log(this.format(` File: ${file}`, 'yellow'));
|
||
this.log(this.format(` ${message}`, 'red'));
|
||
if (stack && this.options.verbose) {
|
||
this.log(this.format(` Stack:`, 'dim'));
|
||
this.log(this.format(stack.split('\n').map(line => ` ${line}`).join('\n'), 'dim'));
|
||
}
|
||
}
|
||
}
|
||
} |