export interface TestResult { success: boolean; output: string; } // ============================================================================= // Reporting export interface Reporter { update(path: string[], result: TestResult): void; } export interface Stream { writeln(s: string): void; } export class StreamReporter implements Reporter { stream: Stream; verbose: boolean; failures: ({ path: string[] } & TestResult)[]; counts: { successes: number, failures: number }; constructor(stream: Stream, verbose: boolean = false) { this.stream = stream; this.verbose = verbose; this.failures = []; this.counts = { successes: 0, failures: 0 }; } update(path: string[], result: TestResult) { if (path.length === 0) throw new RangeError("path is empty"); const pathStr = path.join(' ❯ '); if (result.success) { this.counts.successes++; if (this.verbose) this.stream.writeln(`✅️ ${pathStr}`); } else { this.counts.failures++; this.stream.writeln(`⚠️ ${pathStr}`); this.failures.push({ path, ...result }); } } display() { // Transform [{ path: ['a', 'b', 'c'] }, { path: ['a', 'b', 'd'] }] // into { 'a ❯ b': ['c', 'd'] }. let pathMap = new Map(); for (const f of this.failures) { const key = f.path.slice(0, -1).join(' ❯ '); if (!pathMap.has(key)) pathMap.set(key, []); pathMap.get(key).push({ name: f.path.at(-1), ...f }); } this.stream.writeln(''); this.stream.writeln('FAILURES'); this.stream.writeln('========'); for (const [path, tests] of pathMap) { if (tests.length === 1) { const t = tests[0]; const pathStr = path ? `${path} ❯ ` : ''; const output = t.output ? `: ${t.output}` : ''; this.stream.writeln(`${pathStr}${t.name}${output}`); } else { this.stream.writeln(path); for (const t of tests) { const output = t.output ? `: ${t.output}` : ''; this.stream.writeln(` - ${t.name}${output}`); } } } this.stream.writeln(''); const { successes, failures } = this.counts; this.stream.writeln(`${successes} succeeded, ${failures} failed`); } } const r = new StreamReporter({ writeln: console.log }, true); r.update(['alien', 'can walk'], { success: false, output: 'assertion failed' }); r.update(['alien', 'can speak'], { success: false, output: 'Uncaught ReferenceError: larynx is not defined' }); r.update(['alien', 'sleep'], { success: true, output: '' }); r.update(['Tetromino', 'generate', 'tessellates'], { success: false, output: 'assertion failed: 1 != 2' }); r.update(['Tetromino', 'solve', 'works'], { success: true, output: '' }); r.update(['discombobulate'], { success: false, output: 'hippopotamonstrosesquippedaliophobia' }); r.update(['recombobulate'], { success: true, output: '' }); r.display();