Files
member-console/internal/server/index_render_test.go
T
cgalo5758 408fa6f5a6 Add page anatomy parts and UI quality gate
- Add shared ui_*.html parts (pageHeader, sectionHeader, statusBadge,
  emptyState) parsed into every template set
- Add anatomy lint rules with a shrinking allowlist and screen-coverage
  check
- Add make screens capture harness with contact sheets and baseline diff
- Compose member and FedWiki regions server-side so pages arrive
  complete
- Rebuild Domains and Integrations on the parts as pilots
2026-08-30 04:05:31 -05:00

139 lines
4.9 KiB
Go

package server
import (
"html/template"
"io/fs"
"net/http/httptest"
"strings"
"testing"
"git.coopcloud.tech/wiki-cafe/member-console/internal/config"
"git.coopcloud.tech/wiki-cafe/member-console/internal/embeds"
"git.coopcloud.tech/wiki-cafe/member-console/internal/web"
"log/slog"
)
// indexPageData mirrors the anonymous struct the "/" handler feeds
// index.html (server.go); keep the field sets in sync.
type indexPageData struct {
Name string
Username string
Email string
KeycloakAccountURL string
CSRFToken string
IsOperator bool
HasMultipleWorkspaces bool
CheckoutStatus string
DashboardCards []DashboardCardView
Workspaces template.HTML
PendingDomainClaims []PendingDomainClaim
Shell Shell
}
func renderIndex(t *testing.T, data indexPageData) string {
t.Helper()
sub, err := fs.Sub(embeds.Templates, "templates")
if err != nil {
t.Fatalf("sub templates FS: %v", err)
}
// index.html references no other templates, so it parses standalone; the
// pending-domain notice uses routeURL, registered for real.
tmpl, err := template.New("root").Funcs(template.FuncMap{
"routeURL": web.RouteURL,
"deploymentName": config.DeploymentName,
}).ParseFS(sub, "index.html")
if err != nil {
t.Fatalf("parse index.html: %v", err)
}
tmpl = template.Must(web.ParseUIPartials(tmpl))
rec := httptest.NewRecorder()
NewSafeTemplates(tmpl, slog.Default()).Render(rec, "index.html", data)
if rec.Code != 200 {
t.Fatalf("render = %d, body: %s", rec.Code, rec.Body.String())
}
return rec.Body.String()
}
// The raw template bytes must be integration-free too — a rendered-output
// check could miss a reference hidden behind an untaken template branch.
func TestIndexTemplateNamesNoIntegration(t *testing.T) {
raw, err := fs.ReadFile(embeds.Templates, "templates/index.html")
if err != nil {
t.Fatalf("read embedded index.html: %v", err)
}
for _, banned := range []string{"fedwiki", "discourse", "stripe"} {
if strings.Contains(strings.ToLower(string(raw)), banned) {
t.Errorf("core index.html template references integration %q", banned)
}
}
}
// The dashboard is registry-driven: core's index.html must not name any
// integration, and with no declared cards it must render no card section
// chrome and no integration script tags.
func TestIndexNoDashboardCards(t *testing.T) {
body := renderIndex(t, indexPageData{CSRFToken: "tok"})
if strings.Contains(strings.ToLower(body), "fedwiki") {
t.Errorf("index.html without cards still references fedwiki")
}
if strings.Contains(strings.ToLower(body), "discourse") {
t.Errorf("index.html without cards references discourse")
}
if strings.Contains(body, "hx-trigger=") {
t.Errorf("card shell rendered despite zero declared cards")
}
}
// Declared cards render as generic shells in declaration (registry) order,
// with the declared partial path, trigger spec, and page-level deferred
// script tags.
func TestIndexRendersDeclaredCards(t *testing.T) {
body := renderIndex(t, indexPageData{
CSRFToken: "tok",
DashboardCards: []DashboardCardView{
{DashboardCard: DashboardCard{
Title: "Stub Sites",
PartialPath: "/partials/stub/sites",
RefreshEvent: "refreshStub",
Scripts: []string{"/static/stub/stub-form.js"},
}, Body: "<p>stub body</p>"},
{DashboardCard: DashboardCard{
Title: "Other Service",
PartialPath: "/partials/other/status",
}, Body: "<p>other body</p>"},
},
})
first := strings.Index(body, "Stub Sites")
second := strings.Index(body, "Other Service")
if first == -1 || second == -1 {
t.Fatalf("declared card titles missing (first=%d second=%d)", first, second)
}
if first > second {
t.Errorf("cards rendered out of declaration order")
}
// Bodies are composed on the server (page-anatomy "A page arrives
// complete"): both render inline; only the card with a refresh event
// keeps its partial route as an HTMX swap source, and nothing fetches
// after load.
if !strings.Contains(body, "<p>stub body</p>") || !strings.Contains(body, "<p>other body</p>") {
t.Errorf("composed card bodies missing")
}
if !strings.Contains(body, `<div hx-get="/partials/stub/sites" hx-trigger="refreshStub from:body" hx-swap="innerHTML"><p>stub body</p></div>`) {
t.Errorf("refresh-event card must keep its partial route as the swap source")
}
if strings.Contains(body, `hx-get="/partials/other/status"`) {
t.Errorf("a card without a refresh event must not carry an hx-get")
}
if strings.Contains(body, `hx-trigger="load`) || strings.Contains(body, "spinner-border") {
t.Errorf("nothing on the dashboard may fetch after load or show a spinner")
}
if !strings.Contains(body, `<script defer src="/static/stub/stub-form.js"></script>`) {
t.Errorf("declared card script not rendered as page-level deferred tag")
}
if strings.Contains(strings.ToLower(body), "fedwiki") {
t.Errorf("generic shell leaked an integration name")
}
}