From 0b3be27b9a6d9c770eda768c845d5466230ad80c Mon Sep 17 00:00:00 2001 From: Kat <00-kat@proton.me> Date: Sat, 14 Mar 2026 09:55:21 +1100 Subject: [PATCH] cmd/pkgserver: add DOM reporter for JS tests --- cmd/pkgserver/ui.go | 8 ++++++ cmd/pkgserver/ui/static/test.scss | 24 ++++++++++++++++ cmd/pkgserver/ui/static/test.ts | 48 +++++++++++++++++++++++++++++-- cmd/pkgserver/ui/test.html | 24 ++++++++++++++++ cmd/pkgserver/ui_full.go | 2 +- 5 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 cmd/pkgserver/ui/static/test.scss create mode 100644 cmd/pkgserver/ui/test.html diff --git a/cmd/pkgserver/ui.go b/cmd/pkgserver/ui.go index e4c4283..5b1c510 100644 --- a/cmd/pkgserver/ui.go +++ b/cmd/pkgserver/ui.go @@ -25,14 +25,22 @@ func serveStaticContent(w http.ResponseWriter, r *http.Request) { http.ServeFileFS(w, r, content, "ui/static/favicon.ico") case "/static/index.js": http.ServeFileFS(w, r, content, "ui/static/index.js") + case "/static/test.js": + http.ServeFileFS(w, r, content, "ui/static/test.js") + case "/static/test.css": + http.ServeFileFS(w, r, content, "ui/static/test.css") default: http.NotFound(w, r) } } +func serveTester(w http.ResponseWriter, r *http.Request) { + http.ServeFileFS(w, r, content, "ui/test.html") +} func uiRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /{$}", serveWebUI) mux.HandleFunc("GET /favicon.ico", serveStaticContent) mux.HandleFunc("GET /static/", serveStaticContent) + mux.HandleFunc("GET /test.html", serveTester) } diff --git a/cmd/pkgserver/ui/static/test.scss b/cmd/pkgserver/ui/static/test.scss new file mode 100644 index 0000000..28d7044 --- /dev/null +++ b/cmd/pkgserver/ui/static/test.scss @@ -0,0 +1,24 @@ +.root { + margin: 1rem 0; +} + +details.test-node { + margin-left: 1rem; + padding: 0.2rem 0.5rem; + border-left: 2px dashed black; + > summary { + cursor: pointer; + } + &.failure > summary::marker { + color: red; + } +} + +p.test-desc { + margin: 0 0 0 1rem; + padding: 2px 0; +} + +.italic { + font-style: italic; +} diff --git a/cmd/pkgserver/ui/static/test.ts b/cmd/pkgserver/ui/static/test.ts index f9ee099..7c22096 100644 --- a/cmd/pkgserver/ui/static/test.ts +++ b/cmd/pkgserver/ui/static/test.ts @@ -75,7 +75,51 @@ export class StreamReporter implements Reporter { } } -const r = new StreamReporter({ writeln: console.log }, true); +export class DOMReporter implements Reporter { + update(path: string[], result: TestResult) { + if (path.length === 0) throw new RangeError("path is empty"); + const counter = document.getElementById(result.success ? "successes" : "failures"); + counter.innerText = (Number(counter.innerText) + 1).toString(); + let parent = document.getElementById("root"); + for (const node of path) { + let child = null; + outer: for (const d of parent.children) { + for (const s of d.children) { + if (!(s instanceof HTMLElement)) continue; + if (s.tagName !== "SUMMARY" || s.innerText !== node) continue; + child = d; + break outer; + } + } + if (child === null) { + child = document.createElement("details"); + child.className = "test-node"; + const summary = document.createElement("summary"); + summary.appendChild(document.createTextNode(node)); + child.appendChild(summary); + parent.appendChild(child); + } + if (!result.success) { + child.open = true; + child.classList.add("failure"); + } + parent = child; + } + const p = document.createElement("p"); + p.classList.add("test-desc"); + if (result.output) { + const code = document.createElement("code"); + code.appendChild(document.createTextNode(result.output)); + p.appendChild(code); + } else { + p.classList.add("italic"); + p.appendChild(document.createTextNode("No output.")); + } + parent.appendChild(p); + } +} + +let r = typeof document !== "undefined" ? new DOMReporter() : new StreamReporter({ writeln: console.log }); 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: "" }); @@ -83,4 +127,4 @@ r.update(["Tetromino", "generate", "tessellates"], { success: false, output: "as r.update(["Tetromino", "solve", "works"], { success: true, output: "" }); r.update(["discombobulate"], { success: false, output: "hippopotamonstrosesquippedaliophobia" }); r.update(["recombobulate"], { success: true, output: "" }); -r.display(); +if (r instanceof StreamReporter) r.display(); diff --git a/cmd/pkgserver/ui/test.html b/cmd/pkgserver/ui/test.html new file mode 100644 index 0000000..79d2153 --- /dev/null +++ b/cmd/pkgserver/ui/test.html @@ -0,0 +1,24 @@ + + + + + + + + PkgServer Tests + + +

PkgServer Tests

+ +
+
+ 0 succeeded, 0 failed. +
+ +
+
+ + +
+ + diff --git a/cmd/pkgserver/ui_full.go b/cmd/pkgserver/ui_full.go index f9ca881..837d64d 100644 --- a/cmd/pkgserver/ui_full.go +++ b/cmd/pkgserver/ui_full.go @@ -4,6 +4,6 @@ package main import "embed" -//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && tsc -p ui/static" +//go:generate sh -c "sass ui/static/dark.scss ui/static/dark.css && sass ui/static/light.scss ui/static/light.css && sass ui/static/test.scss ui/static/test.css && tsc -p ui/static" //go:embed ui/* var content embed.FS