- 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
120 lines
5.4 KiB
Go
120 lines
5.4 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"html/template"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/web"
|
|
)
|
|
|
|
// partsSet is a bare template set holding only the shared parts, the way
|
|
// every production set holds them after web.ParseUIPartials.
|
|
func partsSet(t *testing.T) *template.Template {
|
|
t.Helper()
|
|
tmpl, err := web.ParseUIPartials(template.New("parts").Funcs(template.FuncMap{"routeURL": web.RouteURL}))
|
|
if err != nil {
|
|
t.Fatalf("ParseUIPartials: %v", err)
|
|
}
|
|
return tmpl
|
|
}
|
|
|
|
func renderPart(t *testing.T, name string, data any) (string, error) {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
err := partsSet(t).ExecuteTemplate(&buf, name, data)
|
|
return buf.String(), err
|
|
}
|
|
|
|
// TestStatusBadgeMapIsComplete pins page-anatomy "Status badges come from
|
|
// one map": every state the handlers emit has a label and a tone, labels
|
|
// are title case, and an unknown state is visible rather than styled.
|
|
func TestStatusBadgeMapIsComplete(t *testing.T) {
|
|
for _, state := range knownStates {
|
|
b, ok := badgeMap[state]
|
|
if !ok {
|
|
t.Errorf("state %q is emitted but missing from badgeMap", state)
|
|
continue
|
|
}
|
|
if b.Label == "" || b.Tone == "" {
|
|
t.Errorf("state %q has an incomplete badge %+v", state, b)
|
|
}
|
|
if first := b.Label[:1]; first != strings.ToUpper(first) {
|
|
t.Errorf("state %q label %q is not title case", state, b.Label)
|
|
}
|
|
}
|
|
tones := map[string]bool{"success": true, "secondary": true, "warning": true, "danger": true, "info": true, "light": true}
|
|
for state, b := range badgeMap {
|
|
if !tones[b.Tone] {
|
|
t.Errorf("state %q uses unknown tone %q", state, b.Tone)
|
|
}
|
|
}
|
|
if got := StatusBadge("something_new"); got.Label != "something_new" || got.Tone != "secondary" {
|
|
t.Errorf("unknown state must render raw and secondary, got %+v", got)
|
|
}
|
|
}
|
|
|
|
// TestPageHeaderRejectsCountAndAction pins "The right-hand slot holds one
|
|
// thing": rendering fails, naming the page, rather than rendering both.
|
|
func TestPageHeaderRejectsCountAndAction(t *testing.T) {
|
|
_, err := renderPart(t, "pageHeader", PageHeader{Title: "Grants", Count: "3 grants", Action: &Link{Label: "New", URL: "/x"}})
|
|
if err == nil || !strings.Contains(err.Error(), `page header "Grants"`) {
|
|
t.Fatalf("expected the one-slot error naming the page, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestPartsRender pins each part's markup: the one title size, the header
|
|
// slots, the section header and its summary variant, the badge tones and
|
|
// tooltip, and the two empty-state branches.
|
|
func TestPartsRender(t *testing.T) {
|
|
out, err := renderPart(t, "pageHeader", PageHeader{Title: "Domains", Lead: "Every live claim.", Count: "1 live claim", Back: &Link{Label: "People", URL: "/operator/persons"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{`<h1 class="h2 mb-0">Domains</h1>`, `<p class="text-body-secondary mb-0">Every live claim.</p>`, `>1 live claim</span>`, `href="/operator/persons">← Back to People</a>`} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("pageHeader missing %q in:\n%s", want, out)
|
|
}
|
|
}
|
|
out, _ = renderPart(t, "pageHeader", PageHeader{Title: "Products", Action: &Link{Label: "Organization types", URL: "/operator/org-types"}})
|
|
if !strings.Contains(out, `class="btn btn-sm btn-outline-secondary flex-shrink-0">Organization types</a>`) {
|
|
t.Errorf("pageHeader action must render as the outline button, got:\n%s", out)
|
|
}
|
|
out, _ = renderPart(t, "pageHeader", PageHeader{Title: "Products", Action: &Link{Label: "New product", URL: "/x", Primary: true}})
|
|
if !strings.Contains(out, `class="btn btn-sm btn-primary flex-shrink-0">New product</a>`) {
|
|
t.Errorf("primary action must render filled, got:\n%s", out)
|
|
}
|
|
|
|
out, _ = renderPart(t, "sectionHeader", SectionHeader{Title: "Placements", Count: "0 served names"})
|
|
if !strings.Contains(out, `<h2 class="h5 mb-0">Placements</h2>`) || !strings.Contains(out, `>0 served names</span>`) {
|
|
t.Errorf("sectionHeader markup wrong:\n%s", out)
|
|
}
|
|
out, _ = renderPart(t, "sectionSummary", SectionHeader{Title: "Claim history", Count: "4"})
|
|
if !strings.Contains(out, `<summary class="app-section-header mb-2"><h2 class="h5 d-inline mb-0">Claim history (4)</h2></summary>`) {
|
|
t.Errorf("sectionSummary markup wrong:\n%s", out)
|
|
}
|
|
|
|
out, _ = renderPart(t, "statusBadge", StatusBadge("active"))
|
|
if out != `<span class="badge text-bg-success">Active</span>` {
|
|
t.Errorf("statusBadge active rendered %q", out)
|
|
}
|
|
out, _ = renderPart(t, "statusBadge", StatusBadge("operator_root"))
|
|
if out != `<span class="badge text-bg-light border">Operator root</span>` {
|
|
t.Errorf("light badge must carry a border, rendered %q", out)
|
|
}
|
|
out, _ = renderPart(t, "statusBadge", StatusBadge("not_configured").WithTitle("Missing: a, b"))
|
|
if out != `<span class="badge text-bg-warning" title="Missing: a, b">Not configured</span>` {
|
|
t.Errorf("badge tooltip rendered %q", out)
|
|
}
|
|
|
|
out, _ = renderPart(t, "emptyState", EmptyStateParams{Headline: "No placements."})
|
|
if !strings.Contains(out, `<div class="text-center py-4">`) || !strings.Contains(out, `<p class="text-muted mb-2">No placements.</p>`) || strings.Contains(out, "border") {
|
|
t.Errorf("emptyState not-blocked branch wrong:\n%s", out)
|
|
}
|
|
out, _ = renderPart(t, "emptyState", EmptyStateParams{Blocked: true, BlockerCopy: "Create a product first.", PrerequisiteURL: "/operator/products", PrerequisiteLabel: "Products"})
|
|
if !strings.Contains(out, `alert alert-warning`) || !strings.Contains(out, `href="/operator/products"`) {
|
|
t.Errorf("emptyState blocked branch wrong:\n%s", out)
|
|
}
|
|
}
|