From 45205619ca2cc4213f18e9019bd38e2c32b3bcbe Mon Sep 17 00:00:00 2001
From: kat <00-kat@proton.me>
Date: Sun, 29 Mar 2026 01:48:59 +1100
Subject: [PATCH] cmd/mbf: jstest: add DOM reporter
---
.../internal/pkgserver/ui/jstest/index.html | 30 ++++++++++
.../internal/pkgserver/ui/jstest/jstest.ts | 58 ++++++++++++++++++-
.../internal/pkgserver/ui/jstest/style.css | 52 +++++++++++++++++
cmd/mbf/internal/pkgserver/ui/test_ui.go | 1 +
4 files changed, 140 insertions(+), 1 deletion(-)
create mode 100644 cmd/mbf/internal/pkgserver/ui/jstest/index.html
create mode 100644 cmd/mbf/internal/pkgserver/ui/jstest/style.css
diff --git a/cmd/mbf/internal/pkgserver/ui/jstest/index.html b/cmd/mbf/internal/pkgserver/ui/jstest/index.html
new file mode 100644
index 00000000..066ab18c
--- /dev/null
+++ b/cmd/mbf/internal/pkgserver/ui/jstest/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ PkgServer Tests
+
+
+
+
+PkgServer Tests
+
+
+
+ 0 succeeded, 0 failed.
+
+
+
+
+
+
+
+
+
diff --git a/cmd/mbf/internal/pkgserver/ui/jstest/jstest.ts b/cmd/mbf/internal/pkgserver/ui/jstest/jstest.ts
index 875fa174..5bae6b06 100644
--- a/cmd/mbf/internal/pkgserver/ui/jstest/jstest.ts
+++ b/cmd/mbf/internal/pkgserver/ui/jstest/jstest.ts
@@ -90,7 +90,63 @@ export class StreamReporter implements Reporter {
}
}
-const r = new StreamReporter({ writeln: console.log }, true);
+function assertGetElementById(id: string): HTMLElement {
+ let elem = document.getElementById(id);
+ if (elem == null) throw new ReferenceError(`element with ID '${id}' missing from DOM`);
+ return elem;
+}
+
+export class DOMReporter implements Reporter {
+ update(path: string[], result: TestResult) {
+ if (path.length === 0) throw new RangeError("path is empty");
+ const counter = assertGetElementById(result.success ? "successes" : "failures");
+ counter.innerText = (Number(counter.innerText) + 1).toString();
+ let parent = assertGetElementById("root");
+ for (const node of path) {
+ let child: HTMLDetailsElement | null = null;
+ let d: Element;
+ outer: for (d of parent.children) {
+ if (!(d instanceof HTMLDetailsElement)) continue;
+ 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) {
+ child = document.createElement("details");
+ child.className = "test-node";
+ child.ariaRoleDescription = "test";
+ const summary = document.createElement("summary");
+ summary.appendChild(document.createTextNode(node));
+ summary.ariaRoleDescription = "test name";
+ 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.logs.length) {
+ const pre = document.createElement("pre");
+ pre.appendChild(document.createTextNode(result.logs.join("\n")));
+ p.appendChild(pre);
+ } else {
+ p.classList.add("italic");
+ p.appendChild(document.createTextNode("No output."));
+ }
+ parent.appendChild(p);
+ }
+
+ finalize() {}
+}
+
+let r = globalThis.document ? new DOMReporter() : new StreamReporter({ writeln: console.log });
r.update(["alien", "can walk"], { success: false, logs: ["assertion failed"] });
r.update(["alien", "can speak"], { success: false, logs: ["Uncaught ReferenceError: larynx is not defined"] });
r.update(["alien", "sleep"], { success: true, logs: [] });
diff --git a/cmd/mbf/internal/pkgserver/ui/jstest/style.css b/cmd/mbf/internal/pkgserver/ui/jstest/style.css
new file mode 100644
index 00000000..0b54273b
--- /dev/null
+++ b/cmd/mbf/internal/pkgserver/ui/jstest/style.css
@@ -0,0 +1,52 @@
+:root {
+ --bg: #d3d3d3;
+ --fg: black;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg: #2c2c2c;
+ --fg: ghostwhite;
+ }
+}
+
+html {
+ background-color: var(--bg);
+ color: var(--fg);
+}
+
+h1, p, summary, noscript {
+ font-family: sans-serif;
+}
+
+noscript {
+ font-size: 16pt;
+}
+
+.root {
+ margin: 1rem 0;
+}
+
+details.test-node {
+ margin-left: 1rem;
+ padding: 0.2rem 0.5rem;
+ border-left: 2px dashed var(--fg);
+ > summary {
+ cursor: pointer;
+ }
+ &.failure > summary::marker {
+ color: red;
+ }
+}
+
+p.test-desc {
+ margin: 0 0 0 1rem;
+ padding: 2px 0;
+ > pre {
+ margin: 0;
+ }
+}
+
+.italic {
+ font-style: italic;
+}
diff --git a/cmd/mbf/internal/pkgserver/ui/test_ui.go b/cmd/mbf/internal/pkgserver/ui/test_ui.go
index aeb14b12..76c894fd 100644
--- a/cmd/mbf/internal/pkgserver/ui/test_ui.go
+++ b/cmd/mbf/internal/pkgserver/ui/test_ui.go
@@ -6,6 +6,7 @@ import "embed"
//go:generate tsc -p tsconfig.test.json
//go:generate cp index.html style.css favicon.ico static
+//go:generate cp jstest/index.html jstest/style.css static/jstest
//go:embed static
var _static embed.FS
var static = staticFS(_static)