+ 0 succeeded, 0 failed. +
+ +From f2fded062029f24a654386922a74945e369c1e18 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/pkgserver/ui_test: add DOM reporter --- cmd/pkgserver/main.go | 1 + cmd/pkgserver/test_ui.go | 61 +++++++++++++++++++++++++++++++ cmd/pkgserver/test_ui_stub.go | 7 ++++ cmd/pkgserver/ui_test/lib/test.ts | 58 ++++++++++++++++++++++++++++- cmd/pkgserver/ui_test/lib/ui.html | 30 +++++++++++++++ cmd/pkgserver/ui_test/lib/ui.scss | 52 ++++++++++++++++++++++++++ 6 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 cmd/pkgserver/test_ui_stub.go create mode 100644 cmd/pkgserver/ui_test/lib/ui.html create mode 100644 cmd/pkgserver/ui_test/lib/ui.scss diff --git a/cmd/pkgserver/main.go b/cmd/pkgserver/main.go index af59ff2e..e5151eee 100644 --- a/cmd/pkgserver/main.go +++ b/cmd/pkgserver/main.go @@ -82,6 +82,7 @@ func main() { }() var mux http.ServeMux uiRoutes(&mux) + testUiRoutes(&mux) index.registerAPI(&mux) server := http.Server{ Addr: flagAddr, diff --git a/cmd/pkgserver/test_ui.go b/cmd/pkgserver/test_ui.go index 74d6f237..0b974b4b 100644 --- a/cmd/pkgserver/test_ui.go +++ b/cmd/pkgserver/test_ui.go @@ -2,4 +2,65 @@ package main +import ( + "embed" + "net/http" + "path" + "strings" +) + //go:generate tsc -p ui_test +//go:generate sass ui_test/lib/ui.scss ui_test/lib/ui.css +//go:embed ui_test/* +var test_content embed.FS + +func serveTestWebUI(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-XSS-Protection", "1") + w.Header().Set("X-Frame-Options", "DENY") + + http.ServeFileFS(w, r, test_content, "ui_test/lib/ui.html") +} + +func serveTestWebUIStaticContent(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/testui/style.css": + http.ServeFileFS(w, r, test_content, "ui_test/lib/ui.css") + default: + http.NotFound(w, r) + } +} + +func serveTestLibrary(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/test/lib/test.js": + http.ServeFileFS(w, r, test_content, "ui_test/lib/test.js") + default: + http.NotFound(w, r) + } +} + +func redirectUI(w http.ResponseWriter, r *http.Request) { + // The base path should not redirect to the root. + if r.URL.Path == "/ui/" { + http.NotFound(w, r) + return + } + if path.Ext(r.URL.Path) != ".js" { + http.Error(w, "403 forbidden", http.StatusForbidden) + return + } + + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + + http.Redirect(w, r, strings.TrimPrefix(r.URL.Path, "/ui"), http.StatusFound) +} + +func testUiRoutes(mux *http.ServeMux) { + mux.HandleFunc("GET /test.html", serveTestWebUI) + mux.HandleFunc("GET /testui/", serveTestWebUIStaticContent) + mux.HandleFunc("GET /test/lib/", serveTestLibrary) + mux.HandleFunc("GET /ui/", redirectUI) +} diff --git a/cmd/pkgserver/test_ui_stub.go b/cmd/pkgserver/test_ui_stub.go new file mode 100644 index 00000000..65d86baa --- /dev/null +++ b/cmd/pkgserver/test_ui_stub.go @@ -0,0 +1,7 @@ +//go:build !(frontend && frontend_test) + +package main + +import "net/http" + +func testUiRoutes(mux *http.ServeMux) {} diff --git a/cmd/pkgserver/ui_test/lib/test.ts b/cmd/pkgserver/ui_test/lib/test.ts index 875fa174..5bae6b06 100644 --- a/cmd/pkgserver/ui_test/lib/test.ts +++ b/cmd/pkgserver/ui_test/lib/test.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/pkgserver/ui_test/lib/ui.html b/cmd/pkgserver/ui_test/lib/ui.html new file mode 100644 index 00000000..5ce0edea --- /dev/null +++ b/cmd/pkgserver/ui_test/lib/ui.html @@ -0,0 +1,30 @@ + + +
+ + + ++ 0 succeeded, 0 failed. +
+ +