From e0ed1f40d183673ff14b77d2a17f80d193104ffc Mon Sep 17 00:00:00 2001 From: Kat <00-kat@proton.me> Date: Sat, 14 Mar 2026 04:38:18 +1100 Subject: [PATCH] cmd/pkgserver: add basic CLI reporter for testing JS --- cmd/pkgserver/ui/static/test.ts | 86 +++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 cmd/pkgserver/ui/static/test.ts diff --git a/cmd/pkgserver/ui/static/test.ts b/cmd/pkgserver/ui/static/test.ts new file mode 100644 index 00000000..2f6facaf --- /dev/null +++ b/cmd/pkgserver/ui/static/test.ts @@ -0,0 +1,86 @@ +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();