//! Palette contrast tests, mirroring bezel's `tests/theme.rs`: each light text //! token must land within ~0.5 of its dark counterpart's contrast ratio //! against its own background, and the primary text must keep the ratio the //! palette was tuned for. use gpui::Hsla; use theme::{Brand, ThemeColors, ThemeMode, Tint, contrast_ratio, flatten}; // The shipped palette's measured floors (AppKit-style ink, per bezel): // text/bg is 14.4:1 dark and 15.1:1 light, muted lands near 6.3 dark and // 4.0 light, and faint sits below 3:1 in both appearances by design. const TEXT_RATIO: f32 = 13.9; const MUTED_RATIO: f32 = 3.9; const FAINT_RATIO: f32 = 1.8; const TEXT_PARITY: f32 = 0.7; fn ratio(fg: Hsla, bg: Hsla) -> f32 { contrast_ratio(flatten(fg, bg), bg) } #[test] fn text_keeps_its_contrast() { for appearance in [ThemeMode::Dark, ThemeMode::Light] { let colors = ThemeColors::for_appearance(appearance); assert!( ratio(colors.text, colors.bg) >= TEXT_RATIO, "{appearance:?}: text on bg = {}", ratio(colors.text, colors.bg) ); assert!( ratio(colors.text, colors.surface) >= TEXT_RATIO, "{appearance:?}: text on surface" ); assert!( ratio(colors.text_muted, colors.bg) >= MUTED_RATIO, "{appearance:?}: muted on bg = {}", ratio(colors.text_muted, colors.bg) ); assert!( ratio(colors.text_faint, colors.bg) >= FAINT_RATIO, "{appearance:?}: faint on bg = {}", ratio(colors.text_faint, colors.bg) ); } } #[test] fn light_matches_dark_contrast() { let dark = ThemeColors::for_appearance(ThemeMode::Dark); let light = ThemeColors::for_appearance(ThemeMode::Light); // Primary text keeps near-parity across appearances. Muted and faint do // not (the light values sit further from the background by design, see // Ink::APPKIT); their floors are asserted in `text_keeps_its_contrast`. let pairs = [ ("text/bg", dark.text, dark.bg, light.text, light.bg), ( "text/surface", dark.text, dark.surface, light.text, light.surface, ), ]; for (name, dark_fg, dark_bg, light_fg, light_bg) in pairs { let dark_ratio = ratio(dark_fg, dark_bg); let light_ratio = ratio(light_fg, light_bg); let delta = (dark_ratio - light_ratio).abs(); assert!( delta <= TEXT_PARITY, "{name}: light deviates by {delta} (dark {dark_ratio:.3}, light {light_ratio:.3})" ); } } #[test] fn default_brand_is_a_no_op() { let brand = Brand::default(); assert!(brand.is_default()); for appearance in [ThemeMode::Dark, ThemeMode::Light] { let base = ThemeColors::for_appearance(appearance); let branded = brand.branded(appearance); assert_eq!( base, branded, "{appearance:?}: default brand must not move the palette" ); } } #[test] fn brand_tint_rotates_greys_and_keeps_semantics() { let brand = Brand { tint: Tint::new(257.417, 0.046), accent: Tint::new(276.935, 0.182), ..Brand::default() }; let base = ThemeColors::for_appearance(ThemeMode::Dark); let branded = brand.branded(ThemeMode::Dark); assert_ne!(branded.bg, base.bg, "bg takes the tint"); assert_ne!(branded.accent, base.accent, "accent takes the accent hue"); assert_eq!( branded.danger, base.danger, "danger is semantic and keeps its hue" ); assert_eq!( branded.warning, base.warning, "warning is semantic and keeps its hue" ); } #[test] fn warning_wash_carries_its_label() { // warning_muted is a translucent amber wash, so the label pairing is // measured against the plate flattened onto the content plane. for appearance in [ThemeMode::Dark, ThemeMode::Light] { let colors = ThemeColors::for_appearance(appearance); let plate = flatten(colors.warning_muted, colors.bg); assert!( ratio(colors.warning, plate) >= 3.0, "{appearance:?}: warning on warning_muted = {}", ratio(colors.warning, plate) ); } }