| 1 | package guards |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "path/filepath" |
| 6 | "regexp" |
| 7 | "sort" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | |
| 11 | "amadan.net/oficina/estilo.git/base" |
| 12 | "amadan.net/oficina/estilo.git/chrome" |
| 13 | estilomenu "amadan.net/oficina/estilo.git/menu" |
| 14 | "amadan.net/oficina/estilo.git/theme" |
| 15 | ) |
| 16 | |
| 17 | // ourStylesheets are the CSS files this app writes. The vendored ones |
| 18 | // are not checked: they define their own custom properties and know |
| 19 | // nothing about rastrillo's. |
| 20 | var ourStylesheets = []string{ |
| 21 | "internal/sheets/static/sheets.css", |
| 22 | } |
| 23 | |
| 24 | // tokenSources are the files that DEFINE rastrillo's tokens: the |
| 25 | // structural ones copied by `rastrillo doctor` and the shared palette. |
| 26 | var tokenSources = []string{ |
| 27 | "internal/sheets/static/tokens.css", |
| 28 | "Estilo theme module", |
| 29 | } |
| 30 | |
| 31 | var ( |
| 32 | tokenUse = regexp.MustCompile(`var\(\s*(--rst-[a-zA-Z0-9-]+)`) |
| 33 | tokenDefine = regexp.MustCompile(`(--rst-[a-zA-Z0-9-]+)\s*:`) |
| 34 | ) |
| 35 | |
| 36 | // TestEveryTokenWeUseExists is the check that would have caught five |
| 37 | // invented token names shipping to a canary. |
| 38 | // |
| 39 | // CSS resolves an undefined custom property to its fallback without a |
| 40 | // warning anywhere — no console message, no parse error, nothing in a |
| 41 | // Go test. So `var(--rst-border, 1px solid #ccc)` renders a plausible |
| 42 | // page that is simply not themed, and the only way to notice is to look |
| 43 | // at it in both schemes and know what you expected. That is not a gate. |
| 44 | // |
| 45 | // The five were --rst-border, --rst-text-mut, --rst-warning, |
| 46 | // --rst-font-mono and --rst-shell-bar-h. The real names are |
| 47 | // --rst-line, --rst-text-muted, --rst-tone-warning-fg and --rst-font; |
| 48 | // the last had no equivalent at all and the layout was measuring |
| 49 | // something the framework never published. |
| 50 | func TestEveryTokenWeUseExists(t *testing.T) { |
| 51 | root := repoRoot(t) |
| 52 | |
| 53 | defined := map[string]bool{} |
| 54 | for _, rel := range tokenSources { |
| 55 | var body []byte |
| 56 | var err error |
| 57 | if rel == "Estilo theme module" { |
| 58 | body = theme.CSS() |
| 59 | } else { |
| 60 | body, err = os.ReadFile(filepath.Join(root, rel)) |
| 61 | } |
| 62 | if err != nil { |
| 63 | t.Fatalf("guards: read %s: %v — the token sources are what `rastrillo doctor` copies; "+ |
| 64 | "if one moved, this test needs updating, not deleting", rel, err) |
| 65 | } |
| 66 | for _, m := range tokenDefine.FindAllStringSubmatch(string(body), -1) { |
| 67 | defined[m[1]] = true |
| 68 | } |
| 69 | } |
| 70 | if len(defined) == 0 { |
| 71 | t.Fatal("guards: found no --rst-* definitions at all; this test is not checking anything") |
| 72 | } |
| 73 | |
| 74 | var used int |
| 75 | for _, rel := range ourStylesheets { |
| 76 | body, err := os.ReadFile(filepath.Join(root, rel)) |
| 77 | if err != nil { |
| 78 | t.Fatalf("guards: read %s: %v", rel, err) |
| 79 | } |
| 80 | var missing []string |
| 81 | for _, m := range tokenUse.FindAllStringSubmatch(string(body), -1) { |
| 82 | used++ |
| 83 | if !defined[m[1]] { |
| 84 | missing = append(missing, m[1]) |
| 85 | } |
| 86 | } |
| 87 | sort.Strings(missing) |
| 88 | for _, name := range dedupe(missing) { |
| 89 | t.Errorf("%s: var(%s) is not defined in %s — CSS resolves an unknown "+ |
| 90 | "custom property to its fallback in silence, so this renders a page "+ |
| 91 | "that is simply not themed", |
| 92 | rel, name, strings.Join(tokenSources, " or ")) |
| 93 | } |
| 94 | } |
| 95 | if used == 0 { |
| 96 | t.Fatal("guards: our stylesheets reference no tokens at all; either they stopped " + |
| 97 | "using the design system or this test is looking in the wrong place") |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | func dedupe(in []string) []string { |
| 102 | var out []string |
| 103 | for i, s := range in { |
| 104 | if i == 0 || s != in[i-1] { |
| 105 | out = append(out, s) |
| 106 | } |
| 107 | } |
| 108 | return out |
| 109 | } |
| 110 | |
| 111 | // TestEveryEstiloPropertyWeUseExists is the same check for the estilo |
| 112 | // custom properties sheets.css reads (--estilo-float-radius and the |
| 113 | // rest), against the three stylesheets that define them. The same |
| 114 | // failure as a misspelled --rst- token: CSS takes the fallback in |
| 115 | // silence, so a float drawn with an estilo property nobody defines is |
| 116 | // simply not the suite's float. |
| 117 | func TestEveryEstiloPropertyWeUseExists(t *testing.T) { |
| 118 | defined := map[string]bool{} |
| 119 | def := regexp.MustCompile(`(--estilo-[a-zA-Z0-9-]+)\s*:`) |
| 120 | for _, css := range [][]byte{base.CSS(), estilomenu.CSS(), chrome.CSS()} { |
| 121 | for _, m := range def.FindAllStringSubmatch(string(css), -1) { |
| 122 | defined[m[1]] = true |
| 123 | } |
| 124 | } |
| 125 | if len(defined) == 0 { |
| 126 | t.Fatal("guards: estilo defines no --estilo-* properties; this test is reading the wrong stylesheets") |
| 127 | } |
| 128 | use := regexp.MustCompile(`var\(\s*(--estilo-[a-zA-Z0-9-]+)`) |
| 129 | for _, rel := range ourStylesheets { |
| 130 | body := readRepoFile(t, repoRoot(t), rel) |
| 131 | for _, m := range use.FindAllStringSubmatch(body, -1) { |
| 132 | if !defined[m[1]] { |
| 133 | t.Errorf("%s: var(%s) is not defined by estilo's base, menu or chrome stylesheet", rel, m[1]) |
| 134 | } |
| 135 | } |
| 136 | } |
| 137 | } |
| 138 | |