1
0
forked from rosa/hakurei

6 Commits

Author SHA1 Message Date
Kat
2e7d16c1fb TODO: actually write tests lol 2026-03-24 02:55:37 +11:00
Kat
c15a47acab cmd/pkgserver: add noscript warning to test web UI 2026-03-24 02:55:37 +11:00
Kat
c7b6f4b44e cmd/pkgserver: relocate JS tests 2026-03-24 02:55:37 +11:00
Kat
bab3d7d47d 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-24 02:55:37 +11:00
Kat
aa3e492af7 cmd/pkgserver: provide role descriptions for test nodes in web UI 2026-03-24 02:55:37 +11:00
Kat
3633621709 cmd/pkgserver: fix dark mode in test web UI 2026-03-24 02:55:37 +11:00
23 changed files with 231 additions and 83 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

@@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<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">
<title>PkgServer Tests</title>
</head>
<body>
<h1>PkgServer Tests</h1>
<main>
<p id="counters">
<span id="success-counter">0</span> succeeded, <span id="failure-counter">0</span>
failed<span id="skip-counter-text" class="hidden">, <span id="skip-counter">0</span> skipped</span>.
</p>
<div id="root">
</div>
<script type="module">
import "./static/all_tests.js";
import { DOMReporter, TESTS } from "./static/test.js";
TESTS.run(new DOMReporter());
</script>
</main>
</body>
</html>

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

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/testui/style.css">
<title>PkgServer Tests</title>
</head>
<body>
<noscript>
I hate JavaScript as much as you, but this page runs tests written in
JavaScript to test the functionality of JavaScript, so it wouldn't make
sense for it to work without JavaScript. <strong>Please turn JavaScript
on!</strong>
</noscript>
<h1>PkgServer Tests</h1>
<main>
<p id="counters">
<span id="success-counter">0</span> succeeded, <span id="failure-counter">0</span>
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 "/test/all_tests.js";
import { DOMReporter, TESTS } from "/test/lib/test.js";
TESTS.run(new DOMReporter());
</script>
</main>
</body>
</html>

View File

@@ -1,7 +1,28 @@
h1, p, summary {
: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;
}
@@ -9,7 +30,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 +48,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"
}
}