use super::*; /// A rule's properties as (name, value) pairs — spans are for diagnostics, /// not for these assertions. fn props(rule: &RuleIr) -> Vec<(String, Literal)> { rule.props .iter() .map(|p| (p.name.clone(), p.value.clone())) .collect() } const GOOD: &str = r##" (theme Light (tokens (color-accent "#4682b4") (color-accent-hover "#6495ed") (radius-m 8) (fg color-accent)) ; token referencing an earlier token (rule text :color "#111111" :font-size 14) (rule (container .button) :background color-accent :corner-radius radius-m) (rule (container .button :hover) :background color-accent-hover) (rule (container .button :active) :background "#483d8b")) "##; #[test] fn compiles_tokens_and_rules() { let theme = compile_theme(GOOD).expect("valid theme"); assert_eq!(theme.name, "Light"); assert_eq!(theme.rules.len(), 4); let button = &theme.rules[1]; assert_eq!(button.widget, "container"); assert_eq!(button.classes, ["button"]); assert_eq!(button.state, None); assert_eq!( props(button), vec![ ( "background".to_owned(), Literal::Color(0x46, 0x82, 0xb4, 255) ), ("corner-radius".to_owned(), Literal::Int(8)), ] ); let hover = &theme.rules[2]; assert_eq!(hover.state, Some(StateSel::Hover)); assert_eq!( props(hover), vec![( "background".to_owned(), Literal::Color(0x64, 0x95, 0xed, 255) )] ); } #[test] fn token_forward_reference_is_an_error() { let source = r##" (theme Broken (tokens (a b) (b "#fff")) (rule text :color a)) "##; let errors = compile_theme(source).unwrap_err(); assert!(errors[0].message.contains("not a defined token")); } #[test] fn state_rule_without_base_is_an_error() { let source = r##" (theme Broken (rule (container .button :hover) :background "#123456")) "##; let errors = compile_theme(source).unwrap_err(); assert!(errors[0].message.contains("no stateless rule covers it")); } #[test] fn base_coverage_accepts_broader_base() { // Base on `container` (no class) covers the classed state rule. let source = r##" (theme Ok (rule container :background "#ffffff") (rule (container .button :hover) :background "#123456")) "##; assert!(compile_theme(source).is_ok()); } #[test] fn layout_props_are_not_themable() { let source = r##" (theme Broken (rule (container .x) :padding 4)) "##; let errors = compile_theme(source).unwrap_err(); assert!(errors[0].message.contains("not themable")); } #[test] fn unknown_state_and_widget() { let errors = compile_theme(r##"(theme B (rule (container :visited) :background "#fff"))"##).unwrap_err(); assert!(errors[0].message.contains("unknown state")); // Deliberately not a plausible future widget: this test previously used // `slider`, and started failing the day one existed. let errors = compile_theme(r##"(theme B (rule (nonesuch .x) :background "#fff"))"##).unwrap_err(); assert!(errors[0].message.contains("unknown widget")); } #[test] fn path_valued_property_compiles() { use crate::ir::{Literal, PathCmd}; let theme = compile_theme( r##"(theme T (rule checkbox :check-mark (path (move 0.2 0.5) (line 0.4 0.7) (line 0.8 0.3))))"##, ) .expect("valid"); let prop = &theme.rules[0].props[0]; assert_eq!(prop.name, "check-mark"); assert_eq!( &prop.value, &Literal::Path(vec![ PathCmd::Move(0.2, 0.5), PathCmd::Line(0.4, 0.7), PathCmd::Line(0.8, 0.3), ]) ); } #[test] fn svg_path_property_compiles() { let theme = compile_theme(r##"(theme T (rule checkbox :check-mark (svg-path "M0.2 0.5 L0.8 0.5")))"##) .expect("valid"); assert!(matches!( theme.rules[0].props[0].value, crate::ir::Literal::Path(_) )); } #[test] fn theme_value_type_is_checked() { // A color where a graphic is expected… let errors = compile_theme(r##"(theme T (rule checkbox :check-mark "#ffffff"))"##).unwrap_err(); assert!( errors[0].message.contains("expects a graphic"), "got: {}", errors[0].message ); // …and a graphic where a color is expected. let errors = compile_theme(r##"(theme T (rule button :background (path (move 0 0))))"##).unwrap_err(); assert!( errors[0].message.contains("expects a color"), "got: {}", errors[0].message ); } #[test] fn a_graphic_token_takes_either_a_path_or_an_asset() { // The point of the Graphic type: one token, either representation, with // no vocabulary in the theme language to tell them apart. for value in [ r##"(path (move 0.2 0.5) (line 0.8 0.5))"##, r##"(svg-path "M0.2 0.5 L0.8 0.5")"##, r##"(asset "icons/check.png")"##, ] { let source = format!("(theme T (rule checkbox :check-mark {value}))"); let theme = compile_theme(&source).unwrap_or_else(|e| panic!("{value}: {e:?}")); assert!(matches!( theme.rules[0].props[0].value, Literal::Path(_) | Literal::Asset(_) )); } } #[test] fn unknown_themable_property_is_pointed() { let errors = compile_theme(r##"(theme T (rule button :check-mark (path (move 0 0))))"##).unwrap_err(); // A button has no check mark. assert!(errors[0].message.contains("has no themable property")); } #[test] fn default_theme_compiles() { // The shipped default theme must always parse (guiduck-core relies on it // at every tree construction). compile_theme(include_str!("../../../guiduck-core/src/default.gdt")) .expect("the default theme compiles"); } /// A theme styles a `.gdw`-declared widget exactly as it styles a builtin: /// its selector resolves, its declared tokens type-check, and everything the /// builtins get — classes, states, base coverage — applies unchanged. #[test] fn a_rule_can_style_a_manifest_declared_widget() { let mut registry = Registry::default(); for widget in crate::manifest::compile_manifest( r##" (widget markdown (type "::m::View") (token link-color Brush) (token gutter Number)) "##, ) .expect("valid manifest") { registry.insert(widget).expect("fresh name"); } let source = r##" (theme T (rule (markdown .doc) :link-color "#4682b4" :gutter 12) (rule (markdown .doc :hover) :link-color "#6495ed")) "##; let theme = compile_theme_with(source, ®istry).expect("valid theme"); assert_eq!(theme.rules[0].widget, "markdown"); assert_eq!(theme.rules[0].classes, ["doc"]); assert_eq!( props(&theme.rules[0]), vec![ ("link-color".to_owned(), Literal::Color(70, 130, 180, 255)), ("gutter".to_owned(), Literal::Int(12)), ] ); // A token it never declared is not themable on it... let errors = compile_theme_with( r##"(theme T (rule markdown :background "#fff"))"##, ®istry, ) .unwrap_err(); assert!( errors[0] .message .contains("`markdown` has no themable property `:background`"), "{errors:?}" ); // ...and a declared one still has to be given the right type. let errors = compile_theme_with(r##"(theme T (rule markdown :link-color 12))"##, ®istry).unwrap_err(); assert!( errors[0].message.contains("`:link-color` expects a color"), "{errors:?}" ); // Without the manifest, the name is simply not a widget. let errors = compile_theme(r##"(theme T (rule markdown :link-color "#fff"))"##).unwrap_err(); assert!( errors[0].message.contains("unknown widget `markdown`"), "{errors:?}" ); }