tests.rs
raw
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:?}"
);
}