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();