internal/web/classes_test.go

3bcdce33fb9a2309312854331359d376171c7368
gitbay/internal/web/classes_test.go history · blame · raw

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}