internal/web/classes_test.go
107 lines · 2488 bytes
1package web
2
3import (
4 "io/fs"
5 "regexp"
6 "sort"
7 "strings"
8 "testing"
9)
10
11// TestEveryTemplateClassHasARule fails on a class a template uses that
12// no selector in style.css mentions. Template actions are replaced by a
13// marker and any class token carrying one (chip-{{.State}}, l{{.Level}})
14// is skipped; the rules for the concrete values cover those.
15func TestEveryTemplateClassHasARule(t *testing.T) {
16 css := string(StyleCSS)
17 // Every ".name" that appears in a selector position: outside braces.
18 depth := 0
19 var sel strings.Builder
20 for _, r := range css {
21 switch r {
22 case '{':
23 depth++
24 case '}':
25 depth--
26 default:
27 if depth == 0 {
28 sel.WriteRune(r)
29 }
30 }
31 }
32 // Media queries wrap rules one level deeper; strip their headers and
33 // scan again at depth one.
34 depth = 0
35 inMedia := false
36 for i := 0; i < len(css); i++ {
37 if strings.HasPrefix(css[i:], "@media") {
38 inMedia = true
39 }
40 switch css[i] {
41 case '{':
42 depth++
43 if depth == 1 && !inMedia {
44 // ordinary rule; already scanned above
45 }
46 case '}':
47 depth--
48 if depth == 0 {
49 inMedia = false
50 }
51 default:
52 if inMedia && depth == 1 {
53 sel.WriteByte(css[i])
54 }
55 }
56 }
57 classRe := regexp.MustCompile(`\.([a-zA-Z_][a-zA-Z0-9_-]*)`)
58 styled := map[string]bool{}
59 for _, m := range classRe.FindAllStringSubmatch(sel.String(), -1) {
60 styled[m[1]] = true
61 }
62
63 attrRe := regexp.MustCompile(`class="([^"]*)"`)
64 actionRe := regexp.MustCompile(`(?s)\{\{.*?\}\}`)
65 missing := map[string][]string{}
66 entries, err := fs.ReadDir(templateFS, "templates")
67 if err != nil {
68 t.Fatal(err)
69 }
70 for _, e := range entries {
71 src, err := templateFS.ReadFile("templates/" + e.Name())
72 if err != nil {
73 t.Fatal(err)
74 }
75 clean := actionRe.ReplaceAllString(string(src), "\x00")
76 for _, m := range attrRe.FindAllStringSubmatch(clean, -1) {
77 for _, c := range strings.Fields(m[1]) {
78 if strings.Contains(c, "\x00") {
79 continue // composed at render time; the concrete values have rules
80 }
81 if !styled[c] {
82 missing[c] = append(missing[c], e.Name())
83 }
84 }
85 }
86 }
87 var names []string
88 for c := range missing {
89 names = append(names, c)
90 }
91 sort.Strings(names)
92 for _, c := range names {
93 t.Errorf("class %q in %s has no rule in style.css", c, strings.Join(uniq(missing[c]), ", "))
94 }
95}
96
97func uniq(in []string) []string {
98 seen := map[string]bool{}
99 var out []string
100 for _, s := range in {
101 if !seen[s] {
102 seen[s] = true
103 out = append(out, s)
104 }
105 }
106 return out
107}