Phase 4.5: Add tests for StyleIndex

This commit is contained in:
Glint Dev
2026-07-22 13:10:09 +03:00
parent 06aa67b23f
commit fd2863678b

View File

@@ -461,6 +461,7 @@ impl StyleRule {
pub struct StyleSheet {
rules: Vec<StyleRule>,
index: Option<StyleIndex>,
epoch: u64,
}
impl Default for StyleSheet {
@@ -468,6 +469,7 @@ impl Default for StyleSheet {
Self {
rules: Vec::new(),
index: None,
epoch: 0,
}
}
}
@@ -481,11 +483,13 @@ impl StyleSheet {
for part in split_selectors(&selector) {
self.rules.push(StyleRule::build(part, properties.clone()));
}
self.index = None;
}
pub fn build_index(&mut self) {
self.epoch += 1;
let mut index = StyleIndex::new();
index.epoch += 1;
index.epoch = self.epoch;
for (i, rule) in self.rules.iter().enumerate() {
let specificity = rule.selector.specificity();
@@ -1205,3 +1209,153 @@ pub fn parse_size(s: &str) -> Option<f32> {
.parse::<f32>()
.ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_style_sheet(rules: &[(&str, &[(&str, &str)])]) -> StyleSheet {
let mut ss = StyleSheet::new();
for (selector, props) in rules {
let mut map = HashMap::new();
for (k, v) in *props {
map.insert(k.to_string(), v.to_string());
}
ss.add_rule(selector.to_string(), map);
}
ss.build_index();
ss
}
#[test]
fn test_style_index_basic() {
let ss = make_style_sheet(&[
("Button", &[("color", "red")]),
("Button.primary", &[("color", "blue")]),
("#submit", &[("font-size", "20")]),
("Label", &[("color", "green")]),
]);
let attrs = HashMap::new();
let structural = StructuralContext::default();
// Button class="" should match "Button" rule
let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs);
assert_eq!(matched.len(), 1, "Button should match 1 rule");
assert_eq!(matched[0].get("color").map(|s| s.as_str()), Some("red"));
// Button class="primary" should match both "Button" and "Button.primary"
let matched = ss.matching_rules("Button", None, &["primary"], &[], &structural, &[], &[], &attrs);
assert_eq!(matched.len(), 2, "Button.primary should match 2 rules");
// The more specific rule comes first (or last depending on order)
let colors: Vec<&str> = matched.iter().filter_map(|m| m.get("color").map(|s| s.as_str())).collect();
assert!(colors.contains(&"red"));
assert!(colors.contains(&"blue"));
// Button id="submit" should match "Button" and "#submit"
let matched = ss.matching_rules("Button", Some("submit"), &[], &[], &structural, &[], &[], &attrs);
assert_eq!(matched.len(), 2, "Button#submit should match 2 rules");
assert!(matched.iter().any(|m| m.contains_key("color")));
assert!(matched.iter().any(|m| m.contains_key("font-size")));
// Label should match "Label"
let matched = ss.matching_rules("Label", None, &[], &[], &structural, &[], &[], &attrs);
assert_eq!(matched.len(), 1);
assert_eq!(matched[0].get("color").map(|s| s.as_str()), Some("green"));
}
#[test]
fn test_style_index_empty() {
let ss = make_style_sheet(&[]);
let attrs = HashMap::new();
let structural = StructuralContext::default();
let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs);
assert!(matched.is_empty());
}
#[test]
fn test_style_index_pseudo() {
let ss = make_style_sheet(&[
("Button:hover", &[("color", "red")]),
("Button", &[("color", "blue")]),
]);
let attrs = HashMap::new();
let structural = StructuralContext::default();
// No pseudo
let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs);
assert_eq!(matched.len(), 1);
assert_eq!(matched[0].get("color").map(|s| s.as_str()), Some("blue"));
// With hover pseudo
let matched = ss.matching_rules("Button", None, &[], &["hover"], &structural, &[], &[], &attrs);
assert_eq!(matched.len(), 2, "Button:hover should match 2 rules with pseudo");
}
#[test]
fn test_style_index_complex() {
let ss = make_style_sheet(&[
("Panel > Button", &[("color", "red")]),
("Panel Button", &[("font-size", "16")]),
]);
let attrs = HashMap::new();
let structural = StructuralContext::default();
// Button with Panel ancestor
let ancestors = [AncestorInfo::new("Panel", vec![])];
let matched = ss.matching_rules("Button", None, &[], &[], &structural, &ancestors, &[], &attrs);
assert_eq!(matched.len(), 2, "Both complex rules should match");
// Button without Panel ancestor
let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs);
assert_eq!(matched.len(), 0, "No matching without Panel ancestor");
}
#[test]
fn test_style_index_off_by_one_same_as_fallback() {
// Verify index gives same results as fallback for various inputs
let ss = make_style_sheet(&[
("*", &[("margin", "0")]),
("Button", &[("padding", "10")]),
("Button.primary#submit", &[("color", "red")]),
("Label:hover", &[("color", "blue")]),
(".highlight", &[("background", "yellow")]),
]);
let attrs: HashMap<String, String> = HashMap::new();
let structural = StructuralContext::default();
// Verify index matches expected behavior
let matched = ss.matching_rules("Button", None, &[], &[], &structural, &[], &[], &attrs);
assert!(!matched.is_empty(), "Button should match universal rule");
assert!(matched.iter().any(|m| m.get("margin").map(|s| s.as_str()) == Some("0")), "Should include universal margin");
assert!(matched.iter().any(|m| m.get("padding").map(|s| s.as_str()) == Some("10")), "Should include Button padding");
let matched = ss.matching_rules("Button", Some("submit"), &["primary"], &[], &structural, &[], &[], &attrs);
assert!(matched.iter().any(|m| m.contains_key("color")), "#submit.primary should match color rule");
let matched = ss.matching_rules("Label", None, &["highlight"], &[], &structural, &[], &[], &attrs);
assert!(matched.iter().any(|m| m.contains_key("background")), "highlight class should match");
let matched = ss.matching_rules("Panel", None, &[], &[], &structural, &[], &[], &attrs);
assert!(matched.iter().any(|m| m.contains_key("margin")), "Panel should match universal rule");
}
#[test]
fn test_style_index_epoch_increases() {
let mut ss = make_style_sheet(&[("Button", &[("color", "red")])]);
let epoch1 = ss.index.as_ref().unwrap().epoch;
ss.add_rule("Label".to_string(), {
let mut m = HashMap::new();
m.insert("color".to_string(), "blue".to_string());
m
});
assert!(ss.index.is_none(), "add_rule should invalidate index");
ss.build_index();
let epoch2 = ss.index.as_ref().unwrap().epoch;
assert!(epoch2 > epoch1, "build_index should increase epoch");
}
}