package view // GUARD FILE — docs/plans/panel-redesign.md § The contract, rule 7. Not edited // together with internal/web, and never to make an implementation pass: a // failing guard means the implementation is wrong. // // These tests are the design contract as a program. They hold everything that // has left legacy_pages.txt to the accepted mockups under // docs/assets/panel-redesign/panel/, and they read their rules from that // directory — the Bulma subset and the width allow-list from check.py, the // shell's classes from build.py, the stylesheet from theme.css and brand.css, // the skeletons from outlines.json — so the mockups stay the one source and a // design change has one route: mockup first. // // What the guards expect of the implementation (it may not be renamed away): // - pageFiles (view.go) maps every page name to its template files; a // redesigned page carries the name of its mockup (out-domains, in-domain…); // - templates/components.html holds every component partial, layout.html the // shell; a page template calls them and writes no component markup itself; // - static/panel.css is theme.css + brand.css, rule for rule; // - Engine.Render renders a page with the fixture in fixtures_test.go. import ( "crypto/sha256" "encoding/hex" "encoding/json" "go/ast" "go/parser" "go/token" "io/fs" "net/http" "net/http/httptest" "os" "path" "path/filepath" "regexp" "sort" "strconv" "strings" "testing" ) const mockupDir = "../../../docs/assets/panel-redesign/panel" // Screens that are drawn but not part of 2.0: each belongs to a roadmap item // with its own plan (panel-redesign.md § Room for roadmap items). They are the // only keys of outlines.json the finished panel may lack. var deferredScreens = map[string]string{ "in-log": "inbound-antispam-panel, 2.1.0", "in-filter-lists": "inbound-antispam-panel, 2.1.0", "in-queue": "queue as a table / inbound queue, undecided", "in-quarantine": "inbound-quarantine, candidate", "in-quarantine-message": "inbound-quarantine, candidate", "preflight": "preflight, candidate", } // Pages whose 2.0 content is deliberately not the mockup's (decided // 2026-10-09: both queues ship the old content inside the new shell until the // queue is a table). They obey every rule except the outline comparison. var outlineExempt = map[string]string{ "out-queue": "old postqueue output in the new shell until queue-as-a-table is decided", } // Pages that are redesigned but have no outline: the kit page is the // vocabulary itself, and the signed-out screens have no shell to outline. var noOutline = map[string]bool{"components": true, "login": true, "setup": true} // The classes a page template may write itself: text and table-cell utilities // and the form grid. Every other sp- class is a component's and is written in // components.html (or layout.html) only. var pageUtilities = set("sp-muted", "sp-small", "sp-mono", "sp-nowrap", "sp-clip", "sp-actions", "sp-form", "sp-row", "sp-pair") // The partials of panel-redesign.md § Three layers. Each is a {{define}} in // components.html. var requiredPartials = []string{ "page_head", "postmark", "box_head", "box_foot", "flash", "health_card", "dns_record", "facts", "side_menu", "timeline", "log_pane", "empty_state", "confirm_list", "help_topic", } // The typed inputs of § Component kit. Each is declared in components.go with // a doc comment that says where it is used. var requiredInputs = []string{"Head", "Box", "Record", "Fact", "SideMenu", "Tag"} // Vendored files, pinned. A change here is a deliberate upgrade (or, for the // icon subset, a design change that added an icon) and nothing else. var vendoredSHA256 = map[string]string{ "bulma.min.css": "67fa26df1ca9e95d8f2adc7c04fa1b15fa3d24257470ebc10cc68b9aab914bee", "tabler-icons.css": "956cfe253a5588684af3e970bd98b666e97c7044d6c2eca3a1c34ebb10f4298e", "tabler-icons.woff2": "4a0e1ab8991d10497efa72e1c37c307db7b9509e96450198fc421d8d36b78187", } // Everything /static may hold. A second framework, icon set or script does not // get in by being dropped into the directory. var staticFiles = set( "OFL.txt", "favicon.png", "favicon.svg", "htmx.min.js", "panel.js", "panel.css", "ibm-plex-sans.woff2", "ibm-plex-mono-400.woff2", "ibm-plex-mono-600.woff2", "bulma.min.css", "tabler-icons.css", "tabler-icons.woff2", "logo.svg", "wordmark.svg", "perf-edge.svg", ) // What only the old pages need; gone with the last of them. var legacyStaticFiles = set("legacy.css", "logo-compact.svg") func set(items ...string) map[string]bool { m := make(map[string]bool, len(items)) for _, i := range items { m[i] = true } return m } func sorted(m map[string]bool) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } sort.Strings(out) return out } // ratchet is legacy_pages.txt: what is still exempt. type ratchet struct { pages map[string]bool kit bool // the kit is not complete routes bool // the old route table is still in place } func readRatchet(t *testing.T) ratchet { t.Helper() b, err := os.ReadFile("legacy_pages.txt") if err != nil { if os.IsNotExist(err) { return ratchet{pages: map[string]bool{}} } t.Fatalf("read legacy_pages.txt: %v", err) } r := ratchet{pages: map[string]bool{}} for _, line := range strings.Split(string(b), "\n") { line = strings.TrimSpace(line) switch { case line == "" || strings.HasPrefix(line, "#"): case line == "@kit": r.kit = true case line == "@routes": r.routes = true case strings.HasPrefix(line, "@"): t.Fatalf("legacy_pages.txt: unknown entry %q", line) default: r.pages[line] = true } } return r } // skipUntilKit keeps the kit's own tests quiet while stage 0 is under way. The // entry can only be removed, so the quiet ends for good with the kit. func skipUntilKit(t *testing.T) ratchet { t.Helper() r := readRatchet(t) if r.kit { t.Skip("legacy_pages.txt still lists @kit: the component kit is not complete, so its contract is not enforced yet") } return r } func mockup(t *testing.T, name string) string { t.Helper() b, err := os.ReadFile(filepath.Join(mockupDir, name)) if err != nil { t.Fatalf("the guard tests read the accepted mockups and cannot run without them: %v", err) } return string(b) } func embedded(t *testing.T, name string) string { t.Helper() b, err := fs.ReadFile(assetsFS, name) if err != nil { t.Fatalf("%s is not embedded: %v", name, err) } return string(b) } func quoted(src string) []string { var out []string for _, m := range regexp.MustCompile(`"([^"]*)"`).FindAllStringSubmatch(src, -1) { out = append(out, m[1]) } return out } // contract is the vocabulary check.py and build.py define. type contract struct { bulma map[string]bool // the Bulma subset a screen may use shellBulma map[string]bool // Bulma classes of the shell (navbar…), layout.html only shell map[string]bool // sp- classes of the shell, layout.html only signedOut map[string]bool // sp- classes of the signed-out screens widthOK []string } func readContract(t *testing.T) contract { t.Helper() py := mockup(t, "check.py") grab := func(re string) string { m := regexp.MustCompile(re).FindStringSubmatch(py) if m == nil { t.Fatalf("check.py no longer matches %s — the guard reads its lists from there", re) } return m[1] } c := contract{ bulma: set(strings.Fields(grab(`(?s)BULMA = set\("""(.*?)"""`))...), shell: set(quoted(grab(`SHELL = \{([^}]*)\}`))...), signedOut: set(quoted(grab(`SIGNED_OUT = \{([^}]*)\}`))...), widthOK: quoted(grab(`(?s)WIDTH_OK = \((.*?)\)\n`)), shellBulma: map[string]bool{}, } for _, cls := range staticClasses(mockup(t, "build.py")) { if !strings.HasPrefix(cls, "sp-") && !strings.HasPrefix(cls, "ti") && !strings.ContainsAny(cls, "{}%") { c.shellBulma[cls] = true } } if len(c.bulma) < 40 || len(c.shell) < 4 || len(c.signedOut) < 3 || len(c.widthOK) < 5 || !c.shellBulma["navbar"] { t.Fatalf("check.py / build.py were read but the lists look wrong: %d Bulma, %d shell, %d signed-out, %d widths, %d shell Bulma", len(c.bulma), len(c.shell), len(c.signedOut), len(c.widthOK), len(c.shellBulma)) } return c } var spClass = regexp.MustCompile(`\.(sp-[a-z0-9-]+)`) func definedSP(css string) map[string]bool { out := map[string]bool{} for _, m := range spClass.FindAllStringSubmatch(cssComment.ReplaceAllString(css, ""), -1) { out[m[1]] = true } return out } func definedIcons(t *testing.T) map[string]bool { out := map[string]bool{"ti": true} for _, m := range regexp.MustCompile(`\.(ti-[a-z0-9-]+):`).FindAllStringSubmatch(embedded(t, "static/tabler-icons.css"), -1) { out[m[1]] = true } return out } func newGuardEngine(t *testing.T) *Engine { t.Helper() engine, err := New("9.9.9-test") if err != nil { t.Fatalf("New: %v", err) } engine.SetInboundEnabled(true) engine.SetDMARCEnabled(true) return engine } // renderFixture renders a redesigned page the way a handler would. func renderFixture(t *testing.T, engine *Engine, page string) *gnode { t.Helper() fixture, ok := pageFixtures[page] if !ok { t.Fatalf("page %q has left legacy_pages.txt but has no fixture in fixtures_test.go, so nothing can check it", page) } rec := httptest.NewRecorder() engine.Render(rec, http.StatusOK, page, fixture()) if rec.Code != http.StatusOK { t.Fatalf("page %q does not render with its fixture: status %d, %s", page, rec.Code, strings.TrimSpace(rec.Body.String())) } return parseHTML(rec.Body.String()) } // redesigned lists the engine's pages that are held to the contract. func redesigned(engine *Engine, r ratchet) []string { var out []string for name := range engine.Pages() { if !r.pages[name] { out = append(out, name) } } sort.Strings(out) return out } // pageTemplates lists the template files of the redesigned pages — everything // but the kit's own two files. func pageTemplates(engine *Engine, r ratchet) []string { seen := map[string]bool{} for _, name := range redesigned(engine, r) { for _, f := range pageFiles[name] { if base := path.Base(f); base != "components.html" && base != "layout.html" { seen[f] = true } } } return sorted(seen) } // The ratchet and the engine must describe the same panel: a name that is not // a page exempts nothing and only hides a typo, and a page that is off the // list must be a screen that was actually drawn. func TestLegacyRatchetMatchesTheEngine(t *testing.T) { r := readRatchet(t) engine := newGuardEngine(t) var golden map[string]any if err := json.Unmarshal([]byte(mockup(t, "outlines.json")), &golden); err != nil { t.Fatalf("outlines.json: %v", err) } for name := range r.pages { if engine.Page(name) == nil { t.Errorf("legacy_pages.txt lists %q, which is not a page of the engine — delete the line", name) } } for _, name := range redesigned(engine, r) { if _, drawn := golden[name]; !drawn && !noOutline[name] { t.Errorf("page %q is not on legacy_pages.txt and is not a drawn screen: a redesigned page carries the name of its mockup", name) } if _, deferred := deferredScreens[name]; deferred { t.Errorf("page %q belongs to a roadmap item (%s) and is not built with the redesign", name, deferredScreens[name]) } } if len(r.pages) == 0 && !r.kit { for name := range golden { if _, deferred := deferredScreens[name]; !deferred && engine.Page(name) == nil { t.Errorf("screen %q is drawn and in scope, but the engine has no such page", name) } } } for _, name := range redesigned(engine, r) { if r.kit && name != "components" { t.Errorf("page %q has left legacy_pages.txt while @kit is still listed: no page is restyled before the kit is accepted", name) } } } // The Go port of outline() must read the mockups exactly as check.py does, or // the comparison below would be between two different measures. func TestOutlinePortMatchesCheckPy(t *testing.T) { var golden map[string]any if err := json.Unmarshal([]byte(mockup(t, "outlines.json")), &golden); err != nil { t.Fatalf("outlines.json: %v", err) } if len(golden) < 30 { t.Fatalf("outlines.json has %d screens; it should hold every drawn screen", len(golden)) } for name, want := range golden { root := parseHTML(mockup(t, name+".html")) pages := root.find("sp-page") if len(pages) != 1 { t.Errorf("%s.html: %d sp-page containers", name, len(pages)) continue } if got := outlineJSON(outline(pages[0])); got != outlineJSON(want) { t.Errorf("%s: the Go outline differs from outlines.json\n got: %s\nwant: %s", name, got, outlineJSON(want)) } } } // A different design cannot pass as this one: rendered with its fixture, a // page has the component skeleton recorded for its mockup. func TestPanelOutlinesMatchMockups(t *testing.T) { r := skipUntilKit(t) engine := newGuardEngine(t) var golden map[string]any if err := json.Unmarshal([]byte(mockup(t, "outlines.json")), &golden); err != nil { t.Fatalf("outlines.json: %v", err) } for _, name := range redesigned(engine, r) { if noOutline[name] || outlineExempt[name] != "" { continue } t.Run(name, func(t *testing.T) { want, ok := golden[name] if !ok { t.Fatalf("no outline is recorded for %q", name) } pages := renderFixture(t, engine, name).find("sp-page") if len(pages) != 1 { t.Fatalf("the rendered page has %d sp-page containers; the layout provides exactly one", len(pages)) } if got := outlineJSON(outline(pages[0])); got != outlineJSON(want) { t.Errorf("the page's component skeleton is not its mockup's\n got: %s\nwant: %s", got, outlineJSON(want)) } }) } } // The structure rules of check.py, on the rendered page. func TestPanelPageStructure(t *testing.T) { r := skipUntilKit(t) engine := newGuardEngine(t) for _, name := range redesigned(engine, r) { t.Run(name, func(t *testing.T) { root := renderFixture(t, engine, name) if n := len(root.byTag("h1")); n != 1 { t.Errorf("%d