| 1 | package slidestest |
| 2 | |
| 3 | import ( |
| 4 | "amadan.net/oficina/estilo.git/busy" |
| 5 | "bytes" |
| 6 | "fmt" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "regexp" |
| 10 | "sort" |
| 11 | "testing" |
| 12 | |
| 13 | "amadan.net/oficina/estilo.git/theme" |
| 14 | "amadan.net/rastrillo/rastrillo/ui" |
| 15 | ) |
| 16 | |
| 17 | // vendoredTheme is the theme this app was scaffolded with. Change it |
| 18 | // here when you swap static/theme.css for another of ui.ThemeNames(). |
| 19 | const vendoredTheme = "day" |
| 20 | |
| 21 | // vendoredIsMine names the vendored files this app has edited |
| 22 | // DELIBERATELY. Add one here when you mean to diverge: this test stops |
| 23 | // pinning it, and rastrillo doctor leaves it alone too. The file was |
| 24 | // yours from the moment the scaffold wrote it — this is only where you |
| 25 | // record that the edit was on purpose, so the next reader (and the next |
| 26 | // upgrade) can tell it from drift. |
| 27 | var vendoredIsMine = map[string]bool{ |
| 28 | // rastrillo's shim as estilo carries it: without the old busy rule, |
| 29 | // which would fight busy.js. Pinned to estilo by TestBusyFilesAreEstilos. |
| 30 | "rastrillo.js": true, |
| 31 | // estilo's busy.js, for the same reason: the busy rule as the suite |
| 32 | // carries it. ui's own copy moved on at rastrillo fb806a2 (pow.js |
| 33 | // arming) and estilo's has not, so the two can no longer both be |
| 34 | // pinned. Pinned to estilo by TestBusyFilesAreEstilos. |
| 35 | "busy.js": true, |
| 36 | // Calma with Slides' accent, from estilo (estilo AGENTS.md, decisions |
| 37 | // table, row 8) rather than rastrillo's day. Pinned to estilo instead, |
| 38 | // by TestThemeIsEstilosForSlides below. |
| 39 | "theme.css": true, |
| 40 | } |
| 41 | |
| 42 | // TestThemeIsEstilosForSlides pins static/theme.css to the estilo this app |
| 43 | // is built against. After an estilo upgrade, re-write it with |
| 44 | // `go run ./scripts/writeestilo` — never by hand. |
| 45 | func TestThemeIsEstilosForSlides(t *testing.T) { |
| 46 | want, ok := theme.ForApp("slides") |
| 47 | if !ok { |
| 48 | t.Fatal("estilo's theme ring has no slides") |
| 49 | } |
| 50 | got, err := os.ReadFile(filepath.Join("..", "slides", "static", "theme.css")) |
| 51 | if err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | if !bytes.Equal(got, want) { |
| 55 | t.Error("static/theme.css differs from estilo's theme for slides; run go run ./scripts/writeestilo") |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | // The scaffold delivered these files once; they are app-owned from |
| 60 | // then on. This test pins each vendored copy byte-identical to the |
| 61 | // library it came from: a reviewer runs the suite instead of reading |
| 62 | // ~56KB of assets as app diff, and a framework upgrade that forgets |
| 63 | // to re-copy is caught instead of drifting silently. |
| 64 | // |
| 65 | // The list is ui.VendoredAssets — the same function rastrillo new |
| 66 | // wrote these files from and rastrillo doctor compares them against — |
| 67 | // so a file added to the library's vendored set is pinned here without |
| 68 | // this file changing. |
| 69 | func TestVendoredAssetsMatchTheLibrary(t *testing.T) { |
| 70 | assets, ok := ui.VendoredAssets(vendoredTheme) |
| 71 | if !ok { |
| 72 | t.Fatalf("unknown theme %q", vendoredTheme) |
| 73 | } |
| 74 | for name, lib := range assets { |
| 75 | if vendoredIsMine[name] { |
| 76 | continue |
| 77 | } |
| 78 | vendored, err := os.ReadFile(filepath.Join("..", "slides", "static", name)) |
| 79 | if err != nil { |
| 80 | t.Errorf("read vendored %s: %v", name, err) |
| 81 | continue |
| 82 | } |
| 83 | if !bytes.Equal(vendored, lib) { |
| 84 | t.Errorf("static/%s differs from the library copy; re-copy it with rastrillo doctor --fix (or add it to vendoredIsMine if the edit was deliberate)", name) |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | // TestEveryRastrilloTokenSlidesUsesExists makes a misspelled design token a |
| 90 | // test failure instead of a browser falling back silently. The vocabulary is |
| 91 | // the committed tokens.css; usages include both authored templates and CSS. |
| 92 | func TestEveryRastrilloTokenSlidesUsesExists(t *testing.T) { |
| 93 | declared := map[string]bool{} |
| 94 | definitions := append(append([]byte{}, ui.TokensCSS()...), theme.CSS()...) |
| 95 | for _, match := range regexp.MustCompile(`--(rst-[a-z0-9-]+)\s*:`).FindAllSubmatch(definitions, -1) { |
| 96 | declared[string(match[1])] = true |
| 97 | } |
| 98 | if len(declared) == 0 { |
| 99 | t.Fatal("tokens.css declares no --rst-* tokens") |
| 100 | } |
| 101 | files, err := filepath.Glob(filepath.Join("..", "slides", "templates", "*.html")) |
| 102 | if err != nil { |
| 103 | t.Fatal(err) |
| 104 | } |
| 105 | css, err := filepath.Glob(filepath.Join("..", "slides", "static", "*.css")) |
| 106 | if err != nil { |
| 107 | t.Fatal(err) |
| 108 | } |
| 109 | for _, file := range css { |
| 110 | // tokens.css is rastrillo's vendored component implementation; |
| 111 | // TestVendoredAssetsMatchTheLibrary pins it. This gate is for |
| 112 | // Slides-authored CSS and inline template styles. |
| 113 | if filepath.Base(file) != "tokens.css" { |
| 114 | files = append(files, file) |
| 115 | } |
| 116 | } |
| 117 | used := map[string]bool{} |
| 118 | pattern := regexp.MustCompile(`var\(--(rst-[a-z0-9-]+)`) |
| 119 | comments := regexp.MustCompile(`(?s)/\*.*?\*/`) |
| 120 | for _, file := range files { |
| 121 | body, err := os.ReadFile(file) |
| 122 | if err != nil { |
| 123 | t.Fatal(err) |
| 124 | } |
| 125 | for _, match := range pattern.FindAllSubmatch(comments.ReplaceAll(body, nil), -1) { |
| 126 | used[string(match[1])] = true |
| 127 | } |
| 128 | } |
| 129 | var missing []string |
| 130 | for token := range used { |
| 131 | if !declared[token] { |
| 132 | missing = append(missing, token) |
| 133 | } |
| 134 | } |
| 135 | sort.Strings(missing) |
| 136 | for _, token := range missing { |
| 137 | t.Error(fmt.Sprintf("Slides uses --%s, but tokens.css does not declare it", token)) |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | // rastrillo's busy rule (Paul, 2026-09-29): a submit button's label gives |
| 142 | // way to a spinner for at least 650ms, the button disables, and Back |
| 143 | // resets it. static/ carries it as estilo does — the shim without the old |
| 144 | // rule, busy.js and busy.css — pinned here to the estilo this app builds |
| 145 | // against. After an estilo upgrade: go run ./scripts/writeestilo. |
| 146 | func TestBusyFilesAreEstilos(t *testing.T) { |
| 147 | for name, want := range map[string][]byte{ |
| 148 | "rastrillo.js": busy.ShimJS(), |
| 149 | "busy.js": busy.JS(), |
| 150 | "busy.css": busy.CSS(), |
| 151 | } { |
| 152 | got, err := os.ReadFile(filepath.Join("..", "slides", "static", name)) |
| 153 | if err != nil { |
| 154 | t.Fatal(err) |
| 155 | } |
| 156 | if !bytes.Equal(got, want) { |
| 157 | t.Errorf("static/%s differs from estilo's; run go run ./scripts/writeestilo", name) |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 | |