Phase 4.5: Add tests for StyleIndex
This commit is contained in:
@@ -461,6 +461,7 @@ impl StyleRule {
|
|||||||
pub struct StyleSheet {
|
pub struct StyleSheet {
|
||||||
rules: Vec<StyleRule>,
|
rules: Vec<StyleRule>,
|
||||||
index: Option<StyleIndex>,
|
index: Option<StyleIndex>,
|
||||||
|
epoch: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for StyleSheet {
|
impl Default for StyleSheet {
|
||||||
@@ -468,6 +469,7 @@ impl Default for StyleSheet {
|
|||||||
Self {
|
Self {
|
||||||
rules: Vec::new(),
|
rules: Vec::new(),
|
||||||
index: None,
|
index: None,
|
||||||
|
epoch: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -481,11 +483,13 @@ impl StyleSheet {
|
|||||||
for part in split_selectors(&selector) {
|
for part in split_selectors(&selector) {
|
||||||
self.rules.push(StyleRule::build(part, properties.clone()));
|
self.rules.push(StyleRule::build(part, properties.clone()));
|
||||||
}
|
}
|
||||||
|
self.index = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_index(&mut self) {
|
pub fn build_index(&mut self) {
|
||||||
|
self.epoch += 1;
|
||||||
let mut index = StyleIndex::new();
|
let mut index = StyleIndex::new();
|
||||||
index.epoch += 1;
|
index.epoch = self.epoch;
|
||||||
|
|
||||||
for (i, rule) in self.rules.iter().enumerate() {
|
for (i, rule) in self.rules.iter().enumerate() {
|
||||||
let specificity = rule.selector.specificity();
|
let specificity = rule.selector.specificity();
|
||||||
@@ -1205,3 +1209,153 @@ pub fn parse_size(s: &str) -> Option<f32> {
|
|||||||
.parse::<f32>()
|
.parse::<f32>()
|
||||||
.ok()
|
.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