1
0
forked from rosa/hakurei

5 Commits

Author SHA1 Message Date
Kat
fbbaa91b44 TODO: actually write tests lol 2026-03-24 01:46:02 +11:00
Kat
ad09cbdd9e cmd/pkgserver: relocate JS tests (TODO: rebase into parents) 2026-03-24 01:46:02 +11:00
Kat
5f4231514d cmd/pkgserver: aria-describe test node summary with state
The summary marker does not appear in the AOM, so setting its alt text
is fruitless.
2026-03-23 19:51:52 +11:00
Kat
7e5e754e04 cmd/pkgserver: provide role descriptions for test nodes in web UI 2026-03-23 19:51:51 +11:00
Kat
cbde1bc8dd cmd/pkgserver: fix dark mode in test web UI 2026-03-23 19:51:51 +11:00
22 changed files with 193 additions and 57 deletions

4
.gitignore vendored
View File

@@ -31,6 +31,10 @@ go.work.sum
/cmd/pkgserver/ui/static/*.js
/cmd/pkgserver/ui/static/*.css*
/cmd/pkgserver/ui/static/*.css.map
/cmd/pkgserver/ui_test/*.js
/cmd/pkgserver/ui_test/lib/*.js
/cmd/pkgserver/ui_test/lib/*.css*
/cmd/pkgserver/ui_test/lib/*.css.map
/internal/pkg/testdata/testtool
/internal/rosa/hakurei_current.tar.gz

View File

@@ -82,6 +82,7 @@ func main() {
}()
var mux http.ServeMux
uiRoutes(&mux)
testUiRoutes(&mux)
index.registerAPI(&mux)
server := http.Server{
Addr: flagAddr,

97
cmd/pkgserver/test_ui.go Normal file
View File

@@ -0,0 +1,97 @@
//go:build frontend && frontend_test
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")
case "/testui/skip-closed.svg":
http.ServeFileFS(w, r, test_content, "ui_test/lib/skip-closed.svg")
case "/testui/skip-open.svg":
http.ServeFileFS(w, r, test_content, "ui_test/lib/skip-open.svg")
case "/testui/success-closed.svg":
http.ServeFileFS(w, r, test_content, "ui_test/lib/success-closed.svg")
case "/testui/success-open.svg":
http.ServeFileFS(w, r, test_content, "ui_test/lib/success-open.svg")
case "/testui/failure-closed.svg":
http.ServeFileFS(w, r, test_content, "ui_test/lib/failure-closed.svg")
case "/testui/failure-open.svg":
http.ServeFileFS(w, r, test_content, "ui_test/lib/failure-open.svg")
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 serveTests(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/test/" {
http.Redirect(w, r, "/test.html", http.StatusMovedPermanently)
return
}
testPath := strings.TrimPrefix(r.URL.Path, "/test/")
if path.Ext(testPath) != ".js" {
http.Error(w, "403 forbidden", http.StatusForbidden)
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
http.ServeFileFS(w, r, test_content, "ui_test/"+testPath)
}
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 /test/", serveTests)
mux.HandleFunc("GET /ui/", redirectUI)
}

View File

@@ -0,0 +1,7 @@
//go:build !(frontend && frontend_test)
package main
import "net/http"
func testUiRoutes(mux *http.ServeMux) {}

View File

@@ -25,38 +25,14 @@ 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")
case "/static/all_tests.js":
http.ServeFileFS(w, r, content, "ui/static/all_tests.js")
case "/static/test_tests.js":
http.ServeFileFS(w, r, content, "ui/static/test_tests.js")
case "/static/success-closed.svg":
http.ServeFileFS(w, r, content, "ui/static/success-closed.svg")
case "/static/success-open.svg":
http.ServeFileFS(w, r, content, "ui/static/success-open.svg")
case "/static/failure-closed.svg":
http.ServeFileFS(w, r, content, "ui/static/failure-closed.svg")
case "/static/failure-open.svg":
http.ServeFileFS(w, r, content, "ui/static/failure-open.svg")
case "/static/skip-closed.svg":
http.ServeFileFS(w, r, content, "ui/static/skip-closed.svg")
case "/static/skip-open.svg":
http.ServeFileFS(w, r, content, "ui/static/skip-open.svg")
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)
}

View File

@@ -1,6 +0,0 @@
<?xml version="1.0"?>
<!-- See failure-open.svg for an explanation of the view box dimensions. -->
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
<!-- When updating this triangle, also update the other five SVGs. -->
<polygon points="0,0 100,50 0,100" fill="none" stroke="black" stroke-width="15" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 369 B

View File

@@ -1,6 +0,0 @@
<?xml version="1.0"?>
<!-- See failure-open.svg for an explanation of the view box dimensions. -->
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
<!-- When updating this triangle, also update the other five SVGs. -->
<polygon points="0,0 100,0 50,100" fill="none" stroke="black" stroke-width="15" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 369 B

View File

@@ -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 && sass ui/static/test.scss ui/static/test.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 && tsc -p ui/static"
//go:embed ui/*
var content embed.FS

View File

@@ -4,7 +4,7 @@
// provides faster iteration, especially for those acclimated to test-driven
// development.
import "./all_tests.js";
import "../all_tests.js";
import { StreamReporter, TESTS } from "./test.js";
// TypeScript doesn't like process and Deno as their type definitions aren't

View File

Before

Width:  |  Height:  |  Size: 788 B

After

Width:  |  Height:  |  Size: 788 B

View File

@@ -1,3 +1,3 @@
import "./all_tests.js";
import "../all_tests.js";
import { GoTestReporter, TESTS } from "./test.js";
TESTS.run(new GoTestReporter());

View File

Before

Width:  |  Height:  |  Size: 812 B

After

Width:  |  Height:  |  Size: 812 B

View File

Before

Width:  |  Height:  |  Size: 812 B

After

Width:  |  Height:  |  Size: 812 B

View File

@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!-- See failure-open.svg for an explanation of the view box dimensions. -->
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
<style>
.adaptive-stroke {
stroke: black;
}
@media (prefers-color-scheme: dark) {
.adaptive-stroke {
stroke: ghostwhite;
}
}
</style>
<!-- When updating this triangle, also update the other five SVGs. -->
<polygon points="0,0 100,50 0,100" fill="none" class="adaptive-stroke" stroke-width="15" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 572 B

View File

@@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!-- See failure-open.svg for an explanation of the view box dimensions. -->
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
<style>
.adaptive-stroke {
stroke: black;
}
@media (prefers-color-scheme: dark) {
.adaptive-stroke {
stroke: ghostwhite;
}
}
</style>
<!-- When updating this triangle, also update the other five SVGs. -->
<polygon points="0,0 100,0 50,100" fill="none" class="adaptive-stroke" stroke-width="15" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 572 B

View File

@@ -301,6 +301,7 @@ export class DOMReporter implements Reporter {
let parent = assertGetElementById("root");
for (const node of path) {
let child: HTMLDetailsElement | null = null;
let summary: HTMLElement | null = null;
let d: Element;
outer: for (d of parent.children) {
if (!(d instanceof HTMLDetailsElement)) continue;
@@ -308,17 +309,21 @@ export class DOMReporter implements Reporter {
if (!(s instanceof HTMLElement)) continue;
if (!(s.tagName === "SUMMARY" && s.innerText === node)) continue;
child = d;
summary = s;
break outer;
}
}
if (!child) {
child = document.createElement("details");
child.className = "test-node";
const summary = document.createElement("summary");
child.ariaRoleDescription = "test";
summary = document.createElement("summary");
summary.appendChild(document.createTextNode(node));
summary.ariaRoleDescription = "test name";
child.appendChild(summary);
parent.appendChild(child);
}
if (!summary) throw new Error("unreachable as assigned above");
switch (result.state) {
case "failure":
@@ -326,17 +331,18 @@ export class DOMReporter implements Reporter {
child.classList.add("failure");
child.classList.remove("skip");
child.classList.remove("success");
summary.setAttribute("aria-labelledby", "failure-description");
break;
case "skip":
if (child.classList.contains("failure")) break;
child.classList.add("skip");
child.classList.remove("success");
summary.setAttribute("aria-labelledby", "skip-description");
break;
case "success":
if (!(child.classList.contains("failure") ||
child.classList.contains("skip"))) {
child.classList.add("success");
}
if (child.classList.contains("failure") || child.classList.contains("skip")) break;
child.classList.add("success");
summary.setAttribute("aria-labelledby", "success-description");
break;
}

View File

@@ -3,8 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="static/style.css">
<link rel="stylesheet" href="static/test.css">
<link rel="stylesheet" href="/testui/style.css">
<title>PkgServer Tests</title>
</head>
<body>
@@ -16,12 +15,16 @@
failed<span id="skip-counter-text" class="hidden">, <span id="skip-counter">0</span> skipped</span>.
</p>
<p hidden id="success-description">Successful test</p>
<p hidden id="failure-description">Failed test</p>
<p hidden id="skip-description">Partially or fully skipped test</p>
<div id="root">
</div>
<script type="module">
import "./static/all_tests.js";
import { DOMReporter, TESTS } from "./static/test.js";
import "/test/all_tests.js";
import { DOMReporter, TESTS } from "/test/lib/test.js";
TESTS.run(new DOMReporter());
</script>
</main>

View File

@@ -1,3 +1,19 @@
: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 {
font-family: sans-serif;
}
@@ -9,7 +25,7 @@ h1, p, summary {
details.test-node {
margin-left: 1rem;
padding: 0.2rem 0.5rem;
border-left: 2px dashed black;
border-left: 2px dashed var(--fg);
> summary {
cursor: pointer;
}
@@ -27,25 +43,25 @@ details.test-node {
* [2]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/summary#default_style
* [3]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/summary#changing_the_summarys_icon
*/
color: black;
content: url("/static/success-closed.svg") / "success";
color: var(--fg);
content: url("/testui/success-closed.svg") / "success";
}
&.success[open] > summary::marker {
content: url("/static/success-open.svg") / "success";
content: url("/testui/success-open.svg") / "success";
}
&.failure > summary::marker {
color: red;
content: url("/static/failure-closed.svg") / "failure";
content: url("/testui/failure-closed.svg") / "failure";
}
&.failure[open] > summary::marker {
content: url("/static/failure-open.svg") / "failure";
content: url("/testui/failure-open.svg") / "failure";
}
&.skip > summary::marker {
color: blue;
content: url("/static/skip-closed.svg") / "skip";
content: url("/testui/skip-closed.svg") / "skip";
}
&.skip[open] > summary::marker {
content: url("/static/skip-open.svg") / "skip";
content: url("/testui/skip-open.svg") / "skip";
}
}

View File

@@ -1,4 +1,4 @@
import { NoOpReporter, TestRegistrar, context, group, suite, test } from "./test.js";
import { NoOpReporter, TestRegistrar, context, group, suite, test } from "./lib/test.js";
suite("dog", [
group("tail", [

View File

@@ -0,0 +1,6 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2024"
}
}