package docs import ( "os" "path/filepath" "strings" "testing" ) // TestUIControlLabels holds the UI documentation to the source of the UI. // The glossary (docs/developer/ui-controls.md) is the canonical spelling of // every control the docs cite; this test greps that spelling against the // server-rendered templates, so a renamed button fails make test in the // same commit as the rename — the class of rot screenshots could never be // gated. Dynamic (Alpine-composed) labels are OR-checked with their `also` // fragments instead of requiring the composed string to exist. // // Scope is substring-level (not word-boundary): the point is catching // renames and deletions, not stopping coincidental substrings. New controls // cited in prose get a glossary entry in the same commit (writing-ui-docs.md // is the standard; the filter docs are the hands-off exemplar). const ( labelGlossaryRel = "../../docs/developer/ui-controls.md" labelTemplRootRel = "../../templates" labelTSRootRel = "../../web/src" ) type docGlossaryEntry struct { label string where string does string source string reason string also []string skip bool } // docCorpus concatenates every file under root ending in suffix. func docCorpus(t *testing.T, root string, suffix string) string { t.Helper() abs, err := filepath.Abs(root) if err != nil { t.Fatalf("abs %s: %v", root, err) } var b strings.Builder err = filepath.WalkDir(abs, func(path string, d os.DirEntry, err error) error { if err != nil { return err } if d.IsDir() || !strings.HasSuffix(path, suffix) { return nil } raw, err := os.ReadFile(path) if err != nil { return err } b.Write(raw) b.WriteString("\n") return nil }) if err != nil { t.Fatalf("walk %s: %v", root, err) } return b.String() } func corpusName(source string) string { if source == "ts" { return "web/src" } return "templ" } func parseUIControlsBlock(t *testing.T, raw string) []docGlossaryEntry { t.Helper() var block strings.Builder inBlock, foundAny := false, false for _, line := range strings.Split(raw, "\n") { trimmed := strings.TrimSpace(line) switch { case strings.HasPrefix(trimmed, "```controls"): inBlock, foundAny = true, true continue case trimmed == "```" && inBlock: inBlock = false continue } if inBlock { block.WriteString(line) block.WriteString("\n") } } if !foundAny { t.Fatalf("no ```controls block found in %s", labelGlossaryRel) } var entries []docGlossaryEntry haveCur := false cur := docGlossaryEntry{} flush := func() { if haveCur { entries = append(entries, cur) haveCur = false } } for _, line := range strings.Split(block.String(), "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } key, val, ok := strings.Cut(line, ":") if !ok { t.Fatalf("malformed controls line %q (want `key: value`)", line) } key, val = strings.TrimSpace(key), strings.TrimSpace(val) switch key { case "label": flush() haveCur = true cur = docGlossaryEntry{label: val} case "where": cur.where = val case "does": cur.does = val case "source": cur.source = val case "also": for _, frag := range strings.Split(val, "|") { if frag = strings.TrimSpace(frag); frag != "" { cur.also = append(cur.also, frag) } } case "skip": cur.skip = val == "true" case "reason": cur.reason = val case "note": // free text for humans default: t.Fatalf("unknown field in controls block: %q (allowed: label|where|does|source|also|skip|reason|note)", key) } } flush() return entries } func TestUIControlLabels(t *testing.T) { raw, err := os.ReadFile(labelGlossaryRel) if err != nil { t.Fatalf("read glossary: %v", err) } entries := parseUIControlsBlock(t, string(raw)) if len(entries) == 0 { t.Fatalf("glossary has no entries — add at least one before citing controls") } tmplRequired, tsRequired := false, false for _, e := range entries { if e.source == "ts" { tsRequired = true } else { tmplRequired = true } } corpora := map[string]string{} if tmplRequired { corpora[corpusName("")] = docCorpus(t, labelTemplRootRel, ".templ") } if tsRequired { corpora[corpusName("ts")] = docCorpus(t, labelTSRootRel, ".ts") } seen := map[string]int{} for _, e := range entries { if e.label == "" { t.Errorf("entry with empty label (where: %q, does: %q)", e.where, e.does) continue } seen[e.label]++ if e.where == "" || e.does == "" { t.Errorf("control %q: `where` and `does` are required", e.label) } if e.skip && e.reason == "" { t.Errorf("control %q: skip=true requires a reason", e.label) } switch e.source { case "", "templ", "ts": default: t.Errorf("control %q: unknown source %q (templ|ts)", e.label, e.source) } if e.skip { continue } fragments := append([]string{e.label}, e.also...) found := false for _, frag := range fragments { if strings.Contains(corpora[corpusName(e.source)], frag) { found = true break } } if !found { t.Errorf("control %q (%s): none of the fragments [%s] found in %s sources — the label was renamed or the glossary is stale. Update ui-controls.md in the same commit as the rename.", e.label, e.where, strings.Join(fragments, " | "), corpusName(e.source)) } } for label, n := range seen { if n > 1 { t.Errorf("control %q listed %d times — one entry per control", label, n) } } }