check / check (push) Waiting to run
Each page's script turns a one-character braille spinner next to the status whenever a stats answer counts more messages than the one before. Its colour cycles as rtnetmon's does. A separate check runs every second. Once no such answer has come for three refresh periods, 15 seconds on / and 6 on /status, it shows "stale" and the seconds since, in the page's disconnected colour. A failed or hung request counts as no fresh answer. On / the card has no room beside "Connected", so the indicator sits on the line under it. A test renders both pages and checks the indicator follows the status element. Model: opus-5-5
101 lines
2.4 KiB
Go
101 lines
2.4 KiB
Go
package templates_test
|
|
|
|
import (
|
|
"bytes"
|
|
"html/template"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/templates"
|
|
)
|
|
|
|
// renderPage renders a page with the data every page needs.
|
|
func renderPage(t *testing.T, tmpl *template.Template) string {
|
|
t.Helper()
|
|
|
|
var page bytes.Buffer
|
|
|
|
err := tmpl.Execute(&page, struct{ CSPNonce string }{})
|
|
if err != nil {
|
|
t.Fatalf("rendering the page: %v", err)
|
|
}
|
|
|
|
return page.String()
|
|
}
|
|
|
|
// TestFooterLicense renders the home and status pages and expects each footer
|
|
// to name the licence in LICENSE, MIT, and not WTFPL.
|
|
func TestFooterLicense(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
license = "MIT"
|
|
wrongLicense = "WTFPL"
|
|
)
|
|
|
|
pages := map[string]*template.Template{
|
|
"index": templates.IndexTemplate(),
|
|
"status": templates.StatusTemplate(),
|
|
}
|
|
|
|
for name, tmpl := range pages {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, footer, found := strings.Cut(renderPage(t, tmpl), "<footer")
|
|
if !found {
|
|
t.Fatal("the page has no footer")
|
|
}
|
|
|
|
footer, _, _ = strings.Cut(footer, "</footer>")
|
|
|
|
if !strings.Contains(footer, license) {
|
|
t.Errorf("footer does not name %s: %q", license, footer)
|
|
}
|
|
|
|
if strings.Contains(footer, wrongLicense) {
|
|
t.Errorf("footer names %s: %q", wrongLicense, footer)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFreshnessIndicator renders the home and status pages and expects each to
|
|
// have, right after the status element, the element where the page's script
|
|
// shows its spinner or "stale".
|
|
func TestFreshnessIndicator(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
pages := map[string]struct {
|
|
tmpl *template.Template
|
|
status string
|
|
indicator string
|
|
}{
|
|
"index": {
|
|
tmpl: templates.IndexTemplate(),
|
|
status: `<span id="stat-status">-</span>`,
|
|
indicator: `<span id="freshness"></span>`,
|
|
},
|
|
"status": {
|
|
tmpl: templates.StatusTemplate(),
|
|
status: `<span class="metric-value" id="connected">-</span>`,
|
|
indicator: `<span class="metric-value" id="freshness"></span>`,
|
|
},
|
|
}
|
|
|
|
for name, page := range pages {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, afterStatus, found := strings.Cut(renderPage(t, page.tmpl), page.status)
|
|
if !found {
|
|
t.Fatalf("the page has no status element %s", page.status)
|
|
}
|
|
|
|
if !strings.HasPrefix(strings.TrimSpace(afterStatus), page.indicator) {
|
|
t.Errorf("the status element is not followed by %s", page.indicator)
|
|
}
|
|
})
|
|
}
|
|
}
|