| 1 | package memoria |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "path/filepath" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | // Every tone `tone()` can produce is one the stylesheets define. |
| 11 | // |
| 12 | // The template gate reads files rather than rendering them, so it cannot |
| 13 | // follow `data-tone="{{.FlashKind | tone}}"` to a value. This is the other |
| 14 | // end of that: one function produces every dynamic tone in the app, and |
| 15 | // its whole range is checked against the same stylesheets — including the |
| 16 | // default, which is the arm an unrecognised kind lands on. |
| 17 | // |
| 18 | // It exists because the untoned-callout bug reached every flash message in |
| 19 | // the product and three tests agreed with it. A mapping nothing checks is |
| 20 | // how that happens twice. |
| 21 | // |
| 22 | // MUTATION-TESTED 2026-09-07, each verified red then reverted: |
| 23 | // |
| 24 | // tone returns "danger" for the default -> red |
| 25 | // tone returns "" for an unknown kind -> red |
| 26 | // tone returns "good" unchanged -> red |
| 27 | func TestEveryToneIsOneTheStylesheetsDefine(t *testing.T) { |
| 28 | var css string |
| 29 | for _, name := range []string{"tokens.css", "theme.css", "memoria.css"} { |
| 30 | raw, err := os.ReadFile(filepath.Join("static", name)) |
| 31 | if err != nil { |
| 32 | t.Fatalf("read %s: %v", name, err) |
| 33 | } |
| 34 | css += string(raw) |
| 35 | } |
| 36 | |
| 37 | // Every kind the handlers actually set, plus one nobody sets — the |
| 38 | // default arm is the one a new flash kind lands on, and it must be |
| 39 | // styled too. |
| 40 | // |
| 41 | // **Matched against the ATTRIBUTE selector**, not against the string |
| 42 | // anywhere in the file. The previous version looked for |
| 43 | // `data-tone="negative"`, which occurs in the class selector sitting |
| 44 | // beside the attribute one — so it confirmed a spelling the templates |
| 45 | // could not use. Grepping for a string is not reading a selector. |
| 46 | for _, kind := range []string{"good", "danger", "warn", "", "something-new"} { |
| 47 | got := tone(kind) |
| 48 | if got == "" { |
| 49 | t.Errorf("tone(%q) is empty: the callout renders untoned", kind) |
| 50 | continue |
| 51 | } |
| 52 | if !strings.Contains(css, `[rst-callout][rst-tone~="`+got+`"]`) { |
| 53 | t.Errorf("tone(%q) = %q, and no stylesheet selects "+ |
| 54 | "[rst-callout][rst-tone~=%q]: the callout renders as the right "+ |
| 55 | "box in the wrong colour, on a message the reader is meant to "+ |
| 56 | "act on", kind, got, got) |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | // Every kind maps somewhere distinct enough to be readable: a success |
| 61 | // and a failure must not look the same, and neither may collapse into |
| 62 | // the warning. Comparing only good against danger let a mutation send |
| 63 | // "warn" to positive. |
| 64 | seen := map[string]string{} |
| 65 | for _, kind := range []string{"good", "danger", "warn"} { |
| 66 | got := tone(kind) |
| 67 | if prev, dup := seen[got]; dup { |
| 68 | t.Errorf("tone(%q) and tone(%q) both render %q; the tone stops "+ |
| 69 | "carrying information", kind, prev, got) |
| 70 | } |
| 71 | seen[got] = kind |
| 72 | } |
| 73 | } |
| 74 | |