Phase 4.5: Add tests for StyleIndex
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user