From 61a25c88ae3484831dda82f49688ec115c7062cd 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 0000000..f9ee099 --- /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();