tests/light_theme.rs 6.7 KiB 226 lines Raw
1 //! Light theme (m6, f0rge issue #19):2 //! CSS vars in `:root` (Tokyo Night dark) + light overrides under3 //! `prefers-color-scheme`; `color-scheme: dark light`; diff vars for #22.4 //! `theme-color` meta tags are owned by #16 (layout-level) — not asserted5 //! here to keep this change to `style.css` + tests only.6 7 use std::path::PathBuf;8 9 fn manifest() -> PathBuf {10     PathBuf::from(env!("CARGO_MANIFEST_DIR"))11 }12 13 fn css() -> String {14     std::fs::read_to_string(manifest().join("static").join("style.css")).expect("read style.css")15 }16 17 /// Strip `/* ... */` comments so palette hex in prose is not mistaken18 /// for hardcoded rule colors.19 fn without_comments(s: &str) -> String {20     let chars: Vec<char> = s.chars().collect();21     let mut out = String::with_capacity(s.len());22     let mut i = 0;23     while i < chars.len() {24         if i + 1 < chars.len() && chars[i] == '/' && chars[i + 1] == '*' {25             i += 2;26             while i + 1 < chars.len() && !(chars[i] == '*' && chars[i + 1] == '/') {27                 i += 1;28             }29             i += 2;30         } else {31             out.push(chars[i]);32             i += 1;33         }34     }35     out36 }37 38 /// Remove every `:root { ... }` block (brace-matched) so var definitions39 /// may carry palette hex; what remains must reference vars, not hex.40 fn without_root_blocks(s: &str) -> String {41     let chars: Vec<char> = s.chars().collect();42     let mut out = String::with_capacity(s.len());43     let mut i = 0;44     while i < chars.len() {45         if chars[i..].iter().take(5).collect::<String>() == ":root" {46             if let Some(open) = chars[i..].iter().position(|c| *c == '{') {47                 let mut depth = 0;48                 let mut j = i + open;49                 while j < chars.len() {50                     if chars[j] == '{' {51                         depth += 1;52                     } else if chars[j] == '}' {53                         depth -= 1;54                         if depth == 0 {55                             break;56                         }57                     }58                     j += 1;59                 }60                 i = j + 1;61                 continue;62             }63         }64         out.push(chars[i]);65         i += 1;66     }67     out68 }69 70 /// Inner text of the `@media (prefers-color-scheme:light)` block71 /// (brace-matched), so later file content cannot pollute assertions.72 fn light_block_inner(css: &str) -> &str {73     let at = css74         .find("@media (prefers-color-scheme:light)")75         .expect("light block");76     let open = css[at..].find('{').expect("light {") + at;77     let bytes = css.as_bytes();78     let mut depth = 0;79     let mut j = open;80     while j < bytes.len() {81         if bytes[j] == b'{' {82             depth += 1;83         } else if bytes[j] == b'}' {84             depth -= 1;85             if depth == 0 {86                 break;87             }88         }89         j += 1;90     }91     &css[open + 1..j]92 }93 94 #[test]95 fn color_scheme_dark_light() {96     let css = css();97     assert!(98         css.contains("color-scheme:dark light"),99         ":root carries `color-scheme:dark light` so form controls follow the browser"100     );101 }102 103 #[test]104 fn dark_root_defines_vars() {105     let css = css();106     for var in [107         "--bg:#1a1b26",108         "--fg:#c0caf5",109         "--blue:#7aa2f7",110         "--cyan:#7dcfff",111         "--green:#9ece6a",112         "--red:#f7768e",113         "--border:#292e42",114     ] {115         assert!(css.contains(var), ":root defines dark {var}");116     }117 }118 119 #[test]120 fn light_media_block_overrides_vars() {121     let css = css();122     assert!(123         css.contains("@media (prefers-color-scheme:light)"),124         "light media block present (zero-JS OS switch)"125     );126     // Tokyo Night Light palette spot-checks inside the light block.127     let inner = light_block_inner(&css);128     for var in [129         "--bg:#f6f7f9",130         "--fg:#343b58",131         "--muted:#4a5273",132         "--surface:#ffffff",133         "--blue:#34548a",134         "--cyan:#0f4b6e",135         "--red:#8c4351",136         "--green:#33635c",137     ] {138         assert!(inner.contains(var), "light overrides {var}");139     }140 }141 142 #[test]143 fn light_block_has_no_duplicated_rule_bodies() {144     let css = css();145     let inner = light_block_inner(&css);146     // Light block must only redefine vars: no component selectors re-opened.147     for sel in [148         ".site-header",149         ".site-nav",150         "tr:nth-child",151         "pre{",152         "table{",153     ] {154         assert!(155             !inner.contains(sel),156             "light block must not duplicate rule bodies ({sel} found)"157         );158     }159     assert!(inner.contains(":root"), "light overrides live in :root");160 }161 162 #[test]163 fn no_hardcoded_palette_hex_outside_root() {164     let css = without_root_blocks(&without_comments(&css()));165     let palette = [166         "#1a1b26", "#16161e", "#c0caf5", "#565f89", "#7aa2f7", "#7dcfff", "#9ece6a", "#e0af68",167         "#f7768e", "#292e42", "#1f2335", "#9aa5ce", "#f6f7f9", "#343b58", "#34548a", "#0f4b6e",168         "#8c4351", "#33635c", "#eceef2", "#e4e7ee", "#5a4a78", "#7d5f33", "#ffffff", "#233328",169         "#38232e", "#c6d4ca", "#d9c2c7", "#d6dae2", "#4a5273", "#1f2335", "#24283b", "#bb9af7",170         "#ff9e64", "#7f87ad", "#2ac3de", "#8c6c3e", "#2a534d", "#7a3543", "#6a7590", "#8a93a5",171     ];172     for hex in palette {173         assert!(174             !css.to_lowercase().contains(hex),175             "no hardcoded {hex} outside :root var definitions (use var(--...))"176         );177     }178 }179 180 #[test]181 fn rules_reference_vars() {182     let css = css();183     for needle in [184         "background:var(--bg",185         "background:var(--bg-alt",186         "color:var(--fg",187         "color:var(--blue",188         "color:var(--cyan",189         "border:1px solid var(--border",190     ] {191         assert!(css.contains(needle), "rules reference {needle}");192     }193 }194 195 #[test]196 fn diff_vars_defined_dark_and_light() {197     let css = css();198     // Dark (current values) + light (best Tokyo Light pick; #22 may adjust).199     for var in [200         "--diff-add:#9ece6a",201         "--diff-del:#f7768e",202         "--diff-hunk:#7dcfff",203         "--diff-add:#2a534d",204         "--diff-del:#7a3543",205         "--diff-hunk:#0f4b6e",206     ] {207         assert!(css.contains(var), "diff var defined: {var}");208     }209     for var in ["--diff-add-bg", "--diff-del-bg", "--diff-file"] {210         assert!(css.contains(var), "diff var present: {var}");211     }212     // Forward-compat classes for the #22 renderer.213     for cls in [".diff-add", ".diff-del", ".diff-hunk", ".diff-file"] {214         assert!(css.contains(cls), "diff class styled: {cls}");215     }216 }217 218 #[test]219 fn light_budget_and_sharp_corners() {220     let path = manifest().join("static").join("style.css");221     let len = std::fs::metadata(&path).expect("style.css exists").len();222     assert!(223         len < 32768,224         "static/style.css is {len} bytes, budget is 32768 (see gl0w.md)"225     );226 }