package web
import (
"bytes"
"html/template"
"regexp"
"strings"
"testing"
)
// The mirror table's action cell holds two forms. A form is block-level,
// so without display:contents they stack and every row becomes two and a
// half lines tall — which is what the fleet page looked like.
func TestMirrorActionsShareOneRow(t *testing.T) {
if !strings.Contains(baseHTML, "td.acts { display: flex") {
t.Error("the action cell is not a flex row")
}
if !strings.Contains(baseHTML, "td.acts form { display: contents; }") {
t.Error("the forms inside it still generate their own boxes, so the buttons will stack")
}
if !strings.Contains(fleetAdminSource, `<td class="acts">`) {
t.Error("the mirror row's action cell is not marked up as one")
}
}
// The footer is fixed, so every page must reserve its height plus a
// visible gap. Clearing it by a hair still reads as content running into
// it, and the reserve and the bar must come from one value or they drift.
func TestBodyClearsTheFixedFooter(t *testing.T) {
if !strings.Contains(baseHTML, "position: fixed; bottom: 0") {
t.Skip("footer is no longer fixed")
}
// The safe-area inset is part of the reserve AND the footer's own
// padding: on an iPhone with Safari's chrome collapsed, the home
// indicator overlays a bottom:0 bar, and the render time sat under
// it. Elsewhere env() is 0 and both terms are inert.
if !strings.Contains(baseHTML, "padding-bottom: calc(var(--footer-h) + 2rem + env(safe-area-inset-bottom))") {
t.Error("body does not reserve the footer's height plus the safe-area inset")
}
if !strings.Contains(baseHTML, "padding: 0 0 env(safe-area-inset-bottom) 0") {
t.Error("the footer does not pad above the home indicator")
}
if !strings.Contains(baseHTML, "viewport-fit=cover") {
t.Error("without viewport-fit=cover the safe-area env() is always zero")
}
if !strings.Contains(baseHTML, "min-height: var(--footer-h)") {
t.Error("the footer does not use the height the page reserved for it")
}
}
// The Popover API is click-driven; using it as a tooltip gave a click
// target that ALSO carried a native title, so two tooltips competed and
// the cursor turned into a question mark. There is no standard hover
// tooltip primitive (popover="hint" is Chrome-only), so column headers
// carry a real visible label instead — readable by touch and by a
// screen reader, neither of which can hover.
func TestColumnHeadersDoNotUsePopoverAsATooltip(t *testing.T) {
if strings.Contains(baseHTML, "popovertarget=\"tip-") {
t.Error("a column header still opens a popover as a tooltip")
}
if strings.Contains(baseHTML, "cursor: help") {
t.Error("the question-mark cursor is back")
}
if !strings.Contains(baseHTML, ".collabel") {
t.Error("column headers have no visible label to fall back from")
}
// The icon alone must still have an accessible name at every width.
if !strings.Contains(baseHTML, `.colhead { display: inline-flex`) {
t.Error("the header label and icon are not one unit")
}
}
// At the wide breakpoint the header labels appear and the tooltips
// that carried their words retire; below it the sr-only label names
// each column for a screen reader and the tooltip for a pointer. The
// legend that used to restate all four above the header is gone — it
// read as a duplicate of the header row itself.
func TestIconColumnsAreExplainedAtEveryWidth(t *testing.T) {
const labelAt = ".collabel { position: static; width: auto; height: auto; overflow: visible;"
if !strings.Contains(baseHTML, labelAt) {
t.Error("header labels do not appear at the wide breakpoint")
}
if !strings.Contains(baseHTML, `.colhead[data-tip]::after { display: none; }`) {
t.Error("tooltips do not retire where the visible label already says the word")
}
if strings.Contains(baseHTML, "collegend") {
t.Error("the legend is back; it duplicates the header row beneath it")
}
}
// The footer is one line, so something must give on a narrow screen —
// but the render time is the reason that bar earns its space. It is
// therefore the part that never shrinks, and the forge name and version
// collapse ahead of it. Truncating the whole left half dropped the
// timing FIRST, which is exactly backwards.
func TestFooterKeepsTheRenderTime(t *testing.T) {
if !strings.Contains(baseHTML, ".f-time { flex: none; }") {
t.Error("the render time can shrink; it must not")
}
if strings.Contains(baseHTML, ".foot-l { min-width: 0; overflow: hidden; text-overflow: ellipsis;") {
t.Error("the whole left half truncates again, which cuts the timing off the end")
}
// The two lower-priority parts must retire before it, in order.
brand := strings.Index(baseHTML, "@media (max-width: 34rem) { .f-brand { display: none; } }")
ver := strings.Index(baseHTML, "@media (max-width: 26rem) { .f-ver { display: none; } }")
if brand < 0 || ver < 0 {
t.Fatal("the collapse order is not declared")
}
// A wider breakpoint means it goes first: brand must retire before version.
if !strings.Contains(baseHTML, "34rem") || !strings.Contains(baseHTML, "26rem") {
t.Error("the two collapse breakpoints are not distinct")
}
}
// Datastar morphs #fleet and #tl from the server on every change. A
// widget holding its own state inside one of those regions is destroyed
// mid-interaction by a repaint — an open menu vanishes, a half-typed
// filter resets. This is the failure Morpheus was built to work around
// in JS-state component kits; quarry avoids it structurally by keeping
// every popover in page chrome, OUTSIDE the morphed region.
//
// That is currently true by luck of layout rather than by rule, which
// is why it is asserted: the natural next feature is a per-row menu,
// and it would break in a way nobody reproduces by hand.
var tmplComment = regexp.MustCompile(`(?s)\{\{-? */\*.*?\*/ *-?\}\}`)
func TestMorphedRegionsHoldNoStatefulWidgets(t *testing.T) {
region := func(src, define string) string {
i := strings.Index(src, `{{define "`+define+`"}}`)
if i < 0 {
t.Fatalf("no %s template", define)
}
rest := src[i:]
if j := strings.Index(rest[10:], "{{define "); j >= 0 {
rest = rest[:j+10]
}
return rest
}
for _, tc := range []struct{ name, src, define string }{
{"fleet", fleetSource, "fleet-body"},
{"timeline", timelineSource, "tl-body"},
} {
// Template comments are stripped: the prose beside these rules
// discusses popovers, and a substring search finds the word in
// the explanation rather than in any markup.
body := tmplComment.ReplaceAllString(region(tc.src, tc.define), "")
for _, bad := range []string{"popover", "data-tip"} {
if strings.Contains(body, bad) {
t.Errorf("%s is morphed over SSE but contains %q; a repaint would destroy it mid-interaction",
tc.define, bad)
}
}
}
}
// The fleet templates read mirror structs whose fields exist only to be
// named in the template — nothing else touches them, so the compiler
// cannot notice when a template gains a field the struct lacks. That
// drift is invisible until an operator loads the page and gets a
// template error: {{.Health.Gaps}} against a struct without Gaps took
// the whole /fleet surface down while every test stayed green.
//
// Executing the templates against the real structs is the check the
// compiler cannot do.
func TestFleetTemplatesExecuteAgainstTheirStructs(t *testing.T) {
var row fleetHealthRow
row.Name, row.Code = "alpha", "alpha"
row.Health.Gaps, row.Health.Failures = 1, 2
row.Health.LastErr = "boom"
row.Health.Stalled, row.Health.RetryAt = true, "2026-08-19T00:00:00Z"
for _, tc := range []struct {
name string
page *template.Template
def string
data pageData
}{
{"fleet admin", fleetAdminPage, "content",
pageData{FleetHealth: []fleetHealthRow{row}, Unhealthy: 1,
ErrorTotal: 3, ServerErrors: []errlogRow{{
Message: "boom", Surface: "web", Path: "/x",
Count: 3, Last: "2026-08-19T10:00:00Z"}}}},
{"fleet", fleetPage, "fleet-body",
pageData{Fleet: []repoInfo{{Name: "alpha"}}}},
} {
t.Run(tc.name, func(t *testing.T) {
var buf bytes.Buffer
if err := tc.page.ExecuteTemplate(&buf, tc.def, tc.data); err != nil {
t.Fatalf("template no longer matches its structs: %v", err)
}
})
}
}