#[allow(unused_imports)]
pub use super::*;
#[cfg(test)]
#[allow(clippy::pedantic, clippy::nursery, clippy::too_many_lines)]
mod autotest_generated {
use azul_css::{
css::{CssNthChildPattern, CssScopeRange, NodeTypeTag},
OptionString,
};
use super::*;
use crate::{
dom::{AttributeNameValue, AttributeType, NodeType},
id::Node,
};
fn node(
parent: Option<usize>,
prev: Option<usize>,
next: Option<usize>,
last_child: Option<usize>,
) -> Node {
Node {
parent: parent.map(NodeId::new),
previous_sibling: prev.map(NodeId::new),
next_sibling: next.map(NodeId::new),
last_child: last_child.map(NodeId::new),
}
}
fn items(nodes: &[Node]) -> Vec<NodeHierarchyItem> {
nodes.iter().map(|n| NodeHierarchyItem::from(*n)).collect()
}
fn div_with(id: Option<&str>, class: Option<&str>) -> NodeData {
let mut nd = NodeData::create_div();
if let Some(i) = id {
nd.add_id(i.into());
}
if let Some(c) = class {
nd.add_class(c.into());
}
nd
}
fn node_with_attrs(attrs: Vec<AttributeType>) -> NodeData {
let mut nd = NodeData::create_div();
nd.set_attributes(attrs.into());
nd
}
fn custom(name: &str, value: &str) -> AttributeType {
AttributeType::Custom(AttributeNameValue {
attr_name: name.into(),
value: value.into(),
})
}
fn attr_sel(name: &str, op: AttributeMatchOp, value: Option<&str>) -> CssAttributeSelector {
CssAttributeSelector {
name: name.into(),
op,
value: value.map_or(OptionString::None, |v| OptionString::Some(v.into())),
}
}
fn info(index_in_parent: u32, is_last_child: bool) -> CascadeInfo {
CascadeInfo {
index_in_parent,
is_last_child,
}
}
fn sample_hierarchy() -> Vec<Node> {
vec![
node(None, None, None, Some(5)),
node(Some(0), None, Some(2), None),
node(Some(0), Some(1), Some(3), None),
node(Some(0), Some(2), Some(5), Some(4)),
node(Some(3), None, None, None),
node(Some(0), Some(3), None, None),
]
}
fn sample_node_data() -> Vec<NodeData> {
let mut p = NodeData::create_node(NodeType::P);
p.add_class("inner".into());
vec![
NodeData::create_body(),
div_with(Some("first"), Some("a")),
NodeData::create_text_do_not_use_without_block_level_wrapper("hello"),
div_with(None, Some("b")),
p,
div_with(None, Some("c")),
]
}
fn matches(
selectors: Vec<CssPathSelector>,
node_index: usize,
expected_path_ending: Option<CssPathPseudoSelector>,
) -> bool {
let hierarchy = sample_hierarchy();
let hier_items = items(&hierarchy);
let data = sample_node_data();
let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
let data_ref = NodeDataContainerRef::from_slice(&data);
let depths = hierarchy_ref.get_parents_sorted_by_depth();
let cascade = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
matches_html_element(
&CssPath::new(selectors),
NodeId::new(node_index),
&NodeDataContainerRef::from_slice(&hier_items),
&data_ref,
&cascade.as_ref(),
expected_path_ending,
)
}
#[test]
fn cascade_info_vec_as_container_preserves_every_entry() {
let v: CascadeInfoVec = vec![info(0, false), info(1, false), info(2, true)].into();
let c = v.as_container();
assert_eq!(c.len(), 3);
assert!(!c.is_empty());
assert_eq!(c[NodeId::new(0)], info(0, false));
assert_eq!(c[NodeId::new(2)], info(2, true));
assert_eq!(c.get(NodeId::new(3)), None);
assert_eq!(c.get(NodeId::new(usize::MAX)), None);
}
#[test]
fn cascade_info_vec_as_container_on_empty_vec_does_not_panic() {
let v: CascadeInfoVec = Vec::new().into();
let c = v.as_container();
assert_eq!(c.len(), 0);
assert!(c.is_empty());
assert_eq!(c.get(NodeId::new(0)), None);
}
#[test]
fn cascade_info_vec_as_container_survives_extreme_field_values() {
let v: CascadeInfoVec = vec![info(u32::MAX, true)].into();
let c = v.as_container();
assert_eq!(c[NodeId::new(0)].index_in_parent, u32::MAX);
assert!(c[NodeId::new(0)].is_last_child);
}
#[test]
fn css_group_iterator_new_records_the_path_and_yields_nothing_when_empty() {
let empty: [CssPathSelector; 0] = [];
let it = CssGroupIterator::new(&empty);
assert!(it.css_path.is_empty());
assert_eq!(CssGroupIterator::new(&empty).count(), 0);
}
#[test]
fn css_group_iterator_new_keeps_the_slice_it_was_given() {
let path = vec![
CssPathSelector::Global,
CssPathSelector::Children,
CssPathSelector::Class("x".into()),
];
let it = CssGroupIterator::new(&path);
assert_eq!(it.css_path.len(), 3);
assert_eq!(it.css_path, path.as_slice());
}
#[test]
fn css_group_iterator_splits_right_to_left_with_the_left_hand_combinator() {
let path = vec![
CssPathSelector::Type(NodeTypeTag::Body),
CssPathSelector::DirectChildren,
CssPathSelector::Class("foo".into()),
CssPathSelector::Class("main".into()),
CssPathSelector::Children,
CssPathSelector::Class("baz".into()),
];
let groups: Vec<_> = CssGroupIterator::new(&path).collect();
assert_eq!(groups.len(), 3);
assert_eq!(groups[0].0, vec![&path[5]]);
assert_eq!(groups[0].1, CssGroupSplitReason::Children);
assert_eq!(groups[1].0, vec![&path[2], &path[3]]);
assert_eq!(groups[1].1, CssGroupSplitReason::DirectChildren);
assert_eq!(groups[2].0, vec![&path[0]]);
}
#[test]
fn css_group_iterator_yields_an_empty_group_for_a_trailing_combinator() {
let path = vec![
CssPathSelector::Class("foo".into()),
CssPathSelector::DirectChildren,
];
let groups: Vec<_> = CssGroupIterator::new(&path).collect();
assert_eq!(groups.len(), 2);
assert!(
groups[0].0.is_empty(),
"the group to the right of the dangling combinator is empty"
);
assert_eq!(groups[0].1, CssGroupSplitReason::DirectChildren);
assert_eq!(groups[1].0, vec![&path[0]]);
}
#[test]
fn css_group_iterator_terminates_on_consecutive_combinators() {
let path = vec![
CssPathSelector::Class("a".into()),
CssPathSelector::Children,
CssPathSelector::DirectChildren,
CssPathSelector::Class("b".into()),
];
let groups: Vec<_> = CssGroupIterator::new(&path).collect();
assert_eq!(groups.len(), 3);
assert_eq!(groups[0].0, vec![&path[3]]);
assert!(
groups[1].0.is_empty(),
"the group between the two combinators is empty"
);
assert_eq!(groups[2].0, vec![&path[0]]);
}
#[test]
fn css_group_iterator_only_combinators_yields_only_empty_groups() {
let path = vec![
CssPathSelector::Children,
CssPathSelector::DirectChildren,
CssPathSelector::AdjacentSibling,
CssPathSelector::GeneralSibling,
];
let groups: Vec<_> = CssGroupIterator::new(&path).collect();
assert_eq!(groups.len(), 4);
assert!(groups.iter().all(|(g, _)| g.is_empty()));
}
#[test]
fn css_group_iterator_conserves_every_non_combinator_selector_on_a_huge_path() {
let mut path = Vec::new();
for i in 0..5_000u32 {
if i != 0 {
path.push(CssPathSelector::Children);
}
path.push(CssPathSelector::Class(alloc::format!(".c{i}").into()));
}
let groups: Vec<_> = CssGroupIterator::new(&path).collect();
assert_eq!(groups.len(), 5_000);
let total: usize = groups.iter().map(|(g, _)| g.len()).sum();
assert_eq!(
total, 5_000,
"every non-combinator selector must appear in exactly one group"
);
assert!(groups
.iter()
.all(|(_, r)| *r == CssGroupSplitReason::Children));
}
#[test]
fn rule_ends_with_empty_path_is_always_false() {
let empty = CssPath::new(Vec::new());
assert!(!rule_ends_with(&empty, None));
assert!(!rule_ends_with(&empty, Some(CssPathPseudoSelector::Hover)));
assert!(!rule_ends_with(
&empty,
Some(CssPathPseudoSelector::NthChild(CssNthChildSelector::Even))
));
}
#[test]
fn rule_ends_with_none_rejects_interactive_pseudos_but_keeps_structural_ones() {
let ends_with = |s: CssPathSelector| {
rule_ends_with(
&CssPath::new(vec![CssPathSelector::Class("a".into()), s]),
None,
)
};
for p in [
CssPathPseudoSelector::Hover,
CssPathPseudoSelector::Active,
CssPathPseudoSelector::Focus,
CssPathPseudoSelector::Backdrop,
CssPathPseudoSelector::Dragging,
CssPathPseudoSelector::DragOver,
] {
assert!(
!ends_with(CssPathSelector::PseudoSelector(p.clone())),
"interactive pseudo {p:?} must not end a `None` rule"
);
}
assert!(ends_with(CssPathSelector::PseudoSelector(
CssPathPseudoSelector::First
)));
assert!(ends_with(CssPathSelector::PseudoSelector(
CssPathPseudoSelector::Last
)));
assert!(ends_with(CssPathSelector::PseudoSelector(
CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd)
)));
assert!(ends_with(CssPathSelector::Global));
assert!(ends_with(CssPathSelector::Class("b".into())));
assert!(ends_with(CssPathSelector::Type(NodeTypeTag::Div)));
assert!(ends_with(CssPathSelector::DirectChildren));
}
#[test]
fn rule_ends_with_some_target_requires_an_exact_match_on_the_last_selector() {
let hover = CssPath::new(vec![
CssPathSelector::Class("a".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
]);
assert!(rule_ends_with(&hover, Some(CssPathPseudoSelector::Hover)));
assert!(!rule_ends_with(&hover, Some(CssPathPseudoSelector::Active)));
assert!(!rule_ends_with(&hover, Some(CssPathPseudoSelector::Focus)));
let plain = CssPath::new(vec![CssPathSelector::Class("a".into())]);
assert!(!rule_ends_with(&plain, Some(CssPathPseudoSelector::Hover)));
let compound = CssPath::new(vec![
CssPathSelector::Class("a".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::First),
]);
assert!(!rule_ends_with(
&compound,
Some(CssPathPseudoSelector::Hover)
));
assert!(rule_ends_with(
&compound,
Some(CssPathPseudoSelector::First)
));
}
#[test]
fn rule_ends_with_compares_nth_child_and_lang_payloads() {
let nth = |s| {
CssPath::new(vec![CssPathSelector::PseudoSelector(
CssPathPseudoSelector::NthChild(s),
)])
};
assert!(rule_ends_with(
&nth(CssNthChildSelector::Number(3)),
Some(CssPathPseudoSelector::NthChild(
CssNthChildSelector::Number(3)
))
));
assert!(!rule_ends_with(
&nth(CssNthChildSelector::Number(3)),
Some(CssPathPseudoSelector::NthChild(
CssNthChildSelector::Number(4)
))
));
assert!(!rule_ends_with(
&nth(CssNthChildSelector::Even),
Some(CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd))
));
let lang = |s: &str| {
CssPath::new(vec![CssPathSelector::PseudoSelector(
CssPathPseudoSelector::Lang(s.into()),
)])
};
assert!(rule_ends_with(
&lang("zh-Hant-🎉"),
Some(CssPathPseudoSelector::Lang("zh-Hant-🎉".into()))
));
assert!(!rule_ends_with(
&lang("zh-Hant-🎉"),
Some(CssPathPseudoSelector::Lang("zh".into()))
));
assert!(!rule_ends_with(
&lang(""),
Some(CssPathPseudoSelector::Lang("de".into()))
));
}
#[test]
fn match_first_and_last_child_read_only_their_own_field() {
assert!(match_first_child(info(0, false)));
assert!(!match_first_child(info(1, true)));
assert!(!match_first_child(info(u32::MAX, true)));
assert!(match_last_child(info(0, true)));
assert!(!match_last_child(info(0, false)));
assert!(match_last_child(info(u32::MAX, true)));
}
#[test]
fn match_nth_child_is_one_indexed() {
assert!(match_nth_child(
info(0, false),
&CssNthChildSelector::Number(1)
));
assert!(!match_nth_child(
info(0, false),
&CssNthChildSelector::Number(0)
));
assert!(match_nth_child(info(0, false), &CssNthChildSelector::Odd));
assert!(!match_nth_child(info(0, false), &CssNthChildSelector::Even));
assert!(match_nth_child(
info(1, false),
&CssNthChildSelector::Number(2)
));
assert!(match_nth_child(info(1, false), &CssNthChildSelector::Even));
assert!(!match_nth_child(info(1, false), &CssNthChildSelector::Odd));
}
#[test]
fn match_nth_child_number_matches_exactly_one_index() {
for n in 1..64u32 {
for idx in 0..64u32 {
let matched = match_nth_child(info(idx, false), &CssNthChildSelector::Number(n));
assert_eq!(matched, idx + 1 == n, "nth-child({n}) vs index {idx}");
}
}
for idx in 0..64u32 {
assert!(!match_nth_child(
info(idx, false),
&CssNthChildSelector::Number(0)
));
}
}
#[test]
fn match_nth_child_even_and_odd_partition_every_index() {
for idx in 0..1_000u32 {
let even = match_nth_child(info(idx, false), &CssNthChildSelector::Even);
let odd = match_nth_child(info(idx, false), &CssNthChildSelector::Odd);
assert_ne!(even, odd, "index {idx} must be exactly one of even/odd");
}
}
#[test]
fn match_nth_child_pattern_agrees_with_the_even_and_odd_shorthands() {
for idx in 0..256u32 {
let even_pat = CssNthChildSelector::Pattern(CssNthChildPattern {
pattern_repeat: 2,
offset: 0,
});
let odd_pat = CssNthChildSelector::Pattern(CssNthChildPattern {
pattern_repeat: 2,
offset: 1,
});
assert_eq!(
match_nth_child(info(idx, false), &even_pat),
match_nth_child(info(idx, false), &CssNthChildSelector::Even),
"2n disagrees with `even` at index {idx}"
);
assert_eq!(
match_nth_child(info(idx, false), &odd_pat),
match_nth_child(info(idx, false), &CssNthChildSelector::Odd),
"2n+1 disagrees with `odd` at index {idx}"
);
}
}
#[test]
fn match_nth_child_zero_repeat_never_divides_by_zero() {
let only_third = CssNthChildSelector::Pattern(CssNthChildPattern {
pattern_repeat: 0,
offset: 3,
});
let never = CssNthChildSelector::Pattern(CssNthChildPattern {
pattern_repeat: 0,
offset: 0,
});
for idx in 0..32u32 {
assert_eq!(match_nth_child(info(idx, false), &only_third), idx == 2);
assert!(
!match_nth_child(info(idx, false), &never),
"0n+0 must never match (index is 1-based)"
);
}
}
#[test]
fn match_nth_child_pattern_below_the_offset_does_not_underflow() {
let pat = CssNthChildSelector::Pattern(CssNthChildPattern {
pattern_repeat: 2,
offset: 5,
});
for idx in 0..4u32 {
assert!(
!match_nth_child(info(idx, false), &pat),
"index {idx} < offset"
);
}
assert!(match_nth_child(info(4, false), &pat)); assert!(!match_nth_child(info(5, false), &pat)); assert!(match_nth_child(info(6, false), &pat)); }
#[test]
fn match_nth_child_with_a_u32_max_offset_does_not_underflow() {
let pat = CssNthChildSelector::Pattern(CssNthChildPattern {
pattern_repeat: 1,
offset: u32::MAX,
});
assert!(!match_nth_child(info(0, false), &pat));
assert!(!match_nth_child(info(1_000, false), &pat));
assert!(match_nth_child(info(u32::MAX - 1, false), &pat));
}
#[cfg(feature = "std")]
#[test]
fn match_nth_child_at_u32_max_index_never_answers_wrongly() {
let even = std::panic::catch_unwind(|| {
match_nth_child(info(u32::MAX, false), &CssNthChildSelector::Even)
});
let odd = std::panic::catch_unwind(|| {
match_nth_child(info(u32::MAX, false), &CssNthChildSelector::Odd)
});
let one = std::panic::catch_unwind(|| {
match_nth_child(info(u32::MAX, false), &CssNthChildSelector::Number(1))
});
if let Ok(v) = even {
assert!(v, "2^32 is even");
}
if let Ok(v) = odd {
assert!(!v, "2^32 is not odd");
}
if let Ok(v) = one {
assert!(!v, "the 2^32-th child is not the 1st child");
}
}
#[test]
fn match_interactive_pseudo_needs_both_the_last_group_and_the_expected_ending() {
let hover = CssPathPseudoSelector::Hover;
let active = CssPathPseudoSelector::Active;
assert!(match_interactive_pseudo(&hover, Some(&hover), true));
assert!(!match_interactive_pseudo(&hover, Some(&hover), false));
assert!(!match_interactive_pseudo(&hover, Some(&active), true));
assert!(!match_interactive_pseudo(&hover, None, true));
assert!(!match_interactive_pseudo(&hover, None, false));
}
#[test]
fn match_pseudo_selector_routes_every_interactive_pseudo_through_the_gate() {
for p in [
CssPathPseudoSelector::Hover,
CssPathPseudoSelector::Active,
CssPathPseudoSelector::Focus,
CssPathPseudoSelector::Backdrop,
CssPathPseudoSelector::Dragging,
CssPathPseudoSelector::DragOver,
] {
assert!(
match_pseudo_selector(&p, info(0, false), Some(&p), true),
"{p:?} must match when it is the expected ending of the last group"
);
assert!(
!match_pseudo_selector(&p, info(0, false), Some(&p), false),
"{p:?} must not match outside the last content group"
);
assert!(
!match_pseudo_selector(&p, info(0, false), None, true),
"{p:?} must not match when no pseudo state is expected"
);
}
}
#[test]
fn match_pseudo_selector_structural_pseudos_ignore_the_expected_ending() {
let hover = CssPathPseudoSelector::Hover;
for (expected, is_last) in [(None, true), (Some(&hover), false), (Some(&hover), true)] {
assert!(match_pseudo_selector(
&CssPathPseudoSelector::First,
info(0, false),
expected,
is_last
));
assert!(!match_pseudo_selector(
&CssPathPseudoSelector::First,
info(1, false),
expected,
is_last
));
assert!(match_pseudo_selector(
&CssPathPseudoSelector::Last,
info(9, true),
expected,
is_last
));
assert!(match_pseudo_selector(
&CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(10)),
info(9, true),
expected,
is_last
));
}
}
#[test]
fn match_pseudo_selector_lang_only_matches_the_expected_lang() {
let de = CssPathPseudoSelector::Lang("de".into());
let en = CssPathPseudoSelector::Lang("en".into());
assert!(match_pseudo_selector(&de, info(0, false), Some(&de), true));
assert!(!match_pseudo_selector(&de, info(0, false), Some(&en), true));
assert!(!match_pseudo_selector(&de, info(0, false), None, true));
assert!(!match_pseudo_selector(
&de,
info(0, false),
Some(&CssPathPseudoSelector::Hover),
true
));
let emoji = CssPathPseudoSelector::Lang("de-🎉".into());
assert!(match_pseudo_selector(
&emoji,
info(0, false),
Some(&emoji),
true
));
assert!(!match_pseudo_selector(
&emoji,
info(0, false),
Some(&de),
true
));
let empty = CssPathPseudoSelector::Lang("".into());
assert!(match_pseudo_selector(
&empty,
info(0, false),
Some(&empty),
true
));
}
#[test]
fn match_attribute_selector_on_a_node_without_attributes_is_always_false() {
let nd = NodeData::create_div();
for op in [
AttributeMatchOp::Exists,
AttributeMatchOp::Eq,
AttributeMatchOp::Includes,
AttributeMatchOp::DashMatch,
AttributeMatchOp::Prefix,
AttributeMatchOp::Suffix,
AttributeMatchOp::Substring,
] {
assert!(!match_attribute_selector(
&attr_sel("data-x", op, Some("v")),
&nd
));
assert!(!match_attribute_selector(&attr_sel("", op, None), &nd));
}
}
#[test]
fn match_attribute_selector_exists_matches_any_value_including_the_empty_one() {
let nd = node_with_attrs(vec![custom("data-x", "")]);
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Exists, None),
&nd
));
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Exists, Some("nonsense")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("data-y", AttributeMatchOp::Exists, None),
&nd
));
}
#[test]
fn match_attribute_selector_operator_without_a_value_never_matches() {
let nd = node_with_attrs(vec![custom("data-x", "value")]);
for op in [
AttributeMatchOp::Eq,
AttributeMatchOp::Includes,
AttributeMatchOp::DashMatch,
AttributeMatchOp::Prefix,
AttributeMatchOp::Suffix,
AttributeMatchOp::Substring,
] {
assert!(
!match_attribute_selector(&attr_sel("data-x", op, None), &nd),
"{op:?} with a missing target value must be rejected, not matched"
);
}
}
#[test]
fn match_attribute_selector_empty_target_never_matches_the_substring_family() {
let nd = node_with_attrs(vec![custom("data-x", "abc")]);
for op in [
AttributeMatchOp::Prefix,
AttributeMatchOp::Suffix,
AttributeMatchOp::Substring,
AttributeMatchOp::Includes,
] {
assert!(
!match_attribute_selector(&attr_sel("data-x", op, Some("")), &nd),
"{op:?} with an empty target must not match"
);
}
assert!(!match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Eq, Some("")),
&nd
));
let empty = node_with_attrs(vec![custom("data-x", "")]);
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Eq, Some("")),
&empty
));
}
#[test]
fn match_attribute_selector_includes_matches_one_of_several_class_entries() {
let mut nd = NodeData::create_div();
nd.add_class("foo".into());
nd.add_class("primary".into());
nd.add_class("bar".into());
assert!(match_attribute_selector(
&attr_sel("class", AttributeMatchOp::Includes, Some("primary")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("class", AttributeMatchOp::Includes, Some("prim")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("class", AttributeMatchOp::Includes, Some("foo bar")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("class", AttributeMatchOp::Includes, Some("\t")),
&nd
));
}
#[test]
fn match_attribute_selector_dash_match_requires_a_dash_boundary() {
let nd = node_with_attrs(vec![AttributeType::Lang("en-US".into())]);
assert!(match_attribute_selector(
&attr_sel("lang", AttributeMatchOp::DashMatch, Some("en")),
&nd
));
assert!(match_attribute_selector(
&attr_sel("lang", AttributeMatchOp::DashMatch, Some("en-US")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("lang", AttributeMatchOp::DashMatch, Some("en-U")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("lang", AttributeMatchOp::DashMatch, Some("e")),
&nd
));
let english = node_with_attrs(vec![AttributeType::Lang("english".into())]);
assert!(!match_attribute_selector(
&attr_sel("lang", AttributeMatchOp::DashMatch, Some("en")),
&english
));
}
#[test]
fn match_attribute_selector_name_matching_is_exact_and_case_sensitive() {
let nd = node_with_attrs(vec![custom("data-foo", "v")]);
assert!(match_attribute_selector(
&attr_sel("data-foo", AttributeMatchOp::Eq, Some("v")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("data-fo", AttributeMatchOp::Eq, Some("v")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("data-foo2", AttributeMatchOp::Eq, Some("v")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("DATA-FOO", AttributeMatchOp::Eq, Some("v")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("", AttributeMatchOp::Exists, None),
&nd
));
}
#[test]
fn match_attribute_selector_handles_unicode_values_on_char_boundaries() {
let nd = node_with_attrs(vec![custom("data-x", "héllo-🎉-世界")]);
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Prefix, Some("hé")),
&nd
));
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Suffix, Some("世界")),
&nd
));
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Substring, Some("🎉")),
&nd
));
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Eq, Some("héllo-🎉-世界")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Substring, Some("e")),
&nd
));
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::DashMatch, Some("héllo")),
&nd
));
}
#[test]
fn match_attribute_selector_handles_huge_values() {
let huge = "ä".repeat(100_000);
let value = alloc::format!("{huge}-tail");
let nd = node_with_attrs(vec![custom("data-big", &value)]);
assert!(match_attribute_selector(
&attr_sel("data-big", AttributeMatchOp::Suffix, Some("-tail")),
&nd
));
assert!(match_attribute_selector(
&attr_sel("data-big", AttributeMatchOp::Prefix, Some("ää")),
&nd
));
assert!(match_attribute_selector(
&attr_sel("data-big", AttributeMatchOp::DashMatch, Some(huge.as_str())),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("data-big", AttributeMatchOp::Eq, Some(huge.as_str())),
&nd
));
}
#[test]
fn match_single_selector_global_matches_elements_but_never_text_nodes() {
let div = NodeData::create_div();
let text = NodeData::create_text_do_not_use_without_block_level_wrapper("hello");
assert!(match_single_selector(
&CssPathSelector::Global,
info(0, false),
&div,
NodeId::new(0),
None,
true,
false
));
assert!(!match_single_selector(
&CssPathSelector::Global,
info(u32::MAX, true),
&text,
NodeId::new(usize::MAX),
None,
false,
false
));
}
#[test]
fn global_matches_a_text_node_only_when_the_rule_is_scoped_to_exactly_it() {
let text = NodeData::create_text_do_not_use_without_block_level_wrapper("hello");
let nid = NodeId::new(7);
let self_scope = CssPathSelector::Root(CssScopeRange { start: 7, end: 7 });
let global = CssPathSelector::Global;
let group: Vec<&CssPathSelector> = vec![&self_scope, &global];
assert!(
selector_group_matches(&group, info(0, true), &text, nid, None, true),
"a node-only-scoped bare-decl rule must style its own text node"
);
assert!(
!selector_group_matches(&group, info(0, true), &text, NodeId::new(8), None, true),
"node-only scope must not leak to other text nodes"
);
let subtree_scope = CssPathSelector::Root(CssScopeRange { start: 0, end: 20 });
let group2: Vec<&CssPathSelector> = vec![&subtree_scope, &global];
assert!(
!selector_group_matches(&group2, info(0, true), &text, nid, None, true),
"subtree-scoped universal selectors must never match text nodes"
);
let group3: Vec<&CssPathSelector> = vec![&global];
assert!(!selector_group_matches(
&group3,
info(0, true),
&text,
nid,
None,
true
));
}
#[test]
fn match_single_selector_never_matches_a_combinator() {
let div = NodeData::create_div();
for c in [
CssPathSelector::DirectChildren,
CssPathSelector::Children,
CssPathSelector::AdjacentSibling,
CssPathSelector::GeneralSibling,
] {
assert!(!match_single_selector(
&c,
info(0, true),
&div,
NodeId::new(0),
None,
true,
false
));
}
}
#[test]
fn match_single_selector_root_scope_range_is_inclusive_on_both_ends() {
let div = NodeData::create_div();
let sel = CssPathSelector::Root(CssScopeRange { start: 2, end: 4 });
for id in [2usize, 3, 4] {
assert!(match_single_selector(
&sel,
info(0, false),
&div,
NodeId::new(id),
None,
true,
false
));
}
for id in [0usize, 1, 5, usize::MAX] {
assert!(!match_single_selector(
&sel,
info(0, false),
&div,
NodeId::new(id),
None,
true,
false
));
}
}
#[test]
fn match_single_selector_root_scope_with_an_inverted_or_full_range() {
let div = NodeData::create_div();
let inverted = CssPathSelector::Root(CssScopeRange { start: 9, end: 2 });
for id in [0usize, 2, 9, usize::MAX] {
assert!(!match_single_selector(
&inverted,
info(0, false),
&div,
NodeId::new(id),
None,
true,
false
));
}
let full = CssPathSelector::Root(CssScopeRange {
start: 0,
end: usize::MAX,
});
for id in [0usize, 1, usize::MAX] {
assert!(match_single_selector(
&full,
info(0, false),
&div,
NodeId::new(id),
None,
true,
false
));
}
}
#[test]
fn match_single_selector_type_class_and_id() {
let mut nd = div_with(Some("first"), Some("a"));
nd.add_class("日本語-🎉".into());
let hit = |s: &CssPathSelector| {
match_single_selector(s, info(0, false), &nd, NodeId::new(0), None, true, false)
};
assert!(hit(&CssPathSelector::Type(NodeTypeTag::Div)));
assert!(!hit(&CssPathSelector::Type(NodeTypeTag::P)));
assert!(hit(&CssPathSelector::Class("a".into())));
assert!(hit(&CssPathSelector::Class("日本語-🎉".into())));
assert!(
!hit(&CssPathSelector::Class("日本語".into())),
"no prefix matching"
);
assert!(!hit(&CssPathSelector::Class("".into())));
assert!(hit(&CssPathSelector::Id("first".into())));
assert!(!hit(&CssPathSelector::Id("firs".into())));
assert!(!hit(&CssPathSelector::Class("first".into())));
assert!(!hit(&CssPathSelector::Id("a".into())));
}
#[test]
fn selector_group_matches_requires_every_selector_in_the_group() {
let nd = div_with(Some("first"), Some("a"));
let div = CssPathSelector::Type(NodeTypeTag::Div);
let class_a = CssPathSelector::Class("a".into());
let class_z = CssPathSelector::Class("zzz".into());
let group: Vec<&CssPathSelector> = vec![&div, &class_a];
assert!(selector_group_matches(
&group,
info(0, false),
&nd,
NodeId::new(0),
None,
true
));
let group: Vec<&CssPathSelector> = vec![&div, &class_a, &class_z];
assert!(!selector_group_matches(
&group,
info(0, false),
&nd,
NodeId::new(0),
None,
true
));
}
#[test]
fn selector_group_matches_empty_group_matches_vacuously() {
let empty: Vec<&CssPathSelector> = Vec::new();
assert!(selector_group_matches(
&empty,
info(0, false),
&NodeData::create_div(),
NodeId::new(0),
None,
true
));
}
fn anonymous_fixture() -> (Vec<Node>, Vec<NodeData>) {
let hierarchy = vec![
node(None, None, None, Some(5)),
node(Some(0), None, Some(4), Some(2)),
node(Some(1), None, None, Some(3)),
node(Some(2), None, None, None),
node(Some(0), Some(1), Some(5), None),
node(Some(0), Some(4), None, None),
];
let mut anon1 = NodeData::create_div();
anon1.set_anonymous(true);
let mut anon2 = NodeData::create_div();
anon2.set_anonymous(true);
let mut anon4 = NodeData::create_div();
anon4.set_anonymous(true);
let data = vec![
NodeData::create_body(),
anon1,
anon2,
div_with(None, Some("deep")),
anon4,
div_with(None, Some("c")),
];
(hierarchy, data)
}
#[test]
fn find_non_anonymous_parent_skips_a_chain_of_anonymous_boxes() {
let (hierarchy, data) = anonymous_fixture();
let hier_items = items(&hierarchy);
let h = NodeDataContainerRef::from_slice(&hier_items);
let d = NodeDataContainerRef::from_slice(&data);
assert_eq!(
find_non_anonymous_parent(NodeId::new(3), &h, &d),
Some(NodeId::new(0))
);
assert_eq!(
find_non_anonymous_parent(NodeId::new(5), &h, &d),
Some(NodeId::new(0))
);
assert_eq!(find_non_anonymous_parent(NodeId::new(0), &h, &d), None);
}
#[test]
fn find_non_anonymous_parent_returns_none_when_every_ancestor_is_anonymous() {
let hierarchy = vec![
node(None, None, None, Some(1)),
node(Some(0), None, None, None),
];
let mut anon_root = NodeData::create_div();
anon_root.set_anonymous(true);
let data = vec![anon_root, NodeData::create_div()];
let hier_items = items(&hierarchy);
let h = NodeDataContainerRef::from_slice(&hier_items);
let d = NodeDataContainerRef::from_slice(&data);
assert_eq!(find_non_anonymous_parent(NodeId::new(1), &h, &d), None);
}
#[test]
fn find_non_anonymous_prev_sibling_skips_anonymous_siblings() {
let (hierarchy, data) = anonymous_fixture();
let hier_items = items(&hierarchy);
let h = NodeDataContainerRef::from_slice(&hier_items);
let d = NodeDataContainerRef::from_slice(&data);
assert_eq!(
find_non_anonymous_prev_sibling(NodeId::new(5), &h, &d),
None
);
assert_eq!(
find_non_anonymous_prev_sibling(NodeId::new(1), &h, &d),
None
);
assert_eq!(
find_non_anonymous_prev_sibling(NodeId::new(0), &h, &d),
None
);
}
#[test]
fn find_non_anonymous_prev_sibling_returns_the_nearest_real_sibling() {
let hierarchy = sample_hierarchy();
let data = sample_node_data();
let hier_items = items(&hierarchy);
let h = NodeDataContainerRef::from_slice(&hier_items);
let d = NodeDataContainerRef::from_slice(&data);
assert_eq!(
find_non_anonymous_prev_sibling(NodeId::new(3), &h, &d),
Some(NodeId::new(1))
);
assert_eq!(
find_non_anonymous_prev_sibling(NodeId::new(5), &h, &d),
Some(NodeId::new(3))
);
}
#[test]
fn construct_html_cascade_tree_on_an_empty_hierarchy_is_empty() {
let hierarchy: Vec<Node> = Vec::new();
let data: Vec<NodeData> = Vec::new();
let out = construct_html_cascade_tree(
&NodeHierarchyRef::from_slice(&hierarchy),
&[],
&NodeDataContainerRef::from_slice(&data),
);
assert_eq!(out.len(), 0);
assert!(out.is_empty());
}
#[test]
fn construct_html_cascade_tree_with_no_parents_defaults_every_node() {
let hierarchy = vec![node(None, None, None, None)];
let data = vec![NodeData::create_body()];
let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
let depths = hierarchy_ref.get_parents_sorted_by_depth();
assert!(depths.is_empty());
let out = construct_html_cascade_tree(
&hierarchy_ref,
&depths,
&NodeDataContainerRef::from_slice(&data),
);
assert_eq!(out.len(), 1);
assert_eq!(out.internal[0], CascadeInfo::default());
}
#[test]
fn construct_html_cascade_tree_does_not_count_text_nodes_as_element_siblings() {
let hierarchy = sample_hierarchy();
let data = sample_node_data();
let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
let data_ref = NodeDataContainerRef::from_slice(&data);
let depths = hierarchy_ref.get_parents_sorted_by_depth();
let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
assert_eq!(out.len(), hierarchy.len(), "one CascadeInfo per node");
assert_eq!(out.internal[0], info(0, true), "root");
assert_eq!(
out.internal[1],
info(0, false),
"div#first.a — 1st element child"
);
assert_eq!(
out.internal[3],
info(1, false),
"div.b — 2nd element child (text skipped)"
);
assert_eq!(
out.internal[4],
info(0, true),
"p.inner — only child of div.b"
);
assert_eq!(
out.internal[5],
info(2, true),
"div.c — 3rd element child and the last one"
);
assert!(!out.internal[2].is_last_child);
}
#[test]
fn construct_html_cascade_tree_ignores_trailing_text_nodes_for_is_last_child() {
let hierarchy = vec![
node(None, None, None, Some(3)),
node(Some(0), None, Some(2), None),
node(Some(0), Some(1), Some(3), None),
node(Some(0), Some(2), None, None),
];
let data = vec![
NodeData::create_body(),
NodeData::create_div(),
NodeData::create_text_do_not_use_without_block_level_wrapper("a"),
NodeData::create_text_do_not_use_without_block_level_wrapper("b"),
];
let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
let data_ref = NodeDataContainerRef::from_slice(&data);
let depths = hierarchy_ref.get_parents_sorted_by_depth();
let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
assert_eq!(
out.internal[1],
info(0, true),
"trailing text must not un-last the div"
);
}
#[test]
fn construct_html_cascade_tree_handles_a_wide_tree() {
const CHILDREN: usize = 2_000;
let mut hierarchy = vec![node(None, None, None, Some(CHILDREN))];
let mut data = vec![NodeData::create_body()];
for i in 1..=CHILDREN {
hierarchy.push(node(
Some(0),
if i == 1 { None } else { Some(i - 1) },
if i == CHILDREN { None } else { Some(i + 1) },
None,
));
data.push(NodeData::create_div());
}
let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
let data_ref = NodeDataContainerRef::from_slice(&data);
let depths = hierarchy_ref.get_parents_sorted_by_depth();
let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
assert_eq!(out.len(), CHILDREN + 1);
for i in 1..=CHILDREN {
let expected = info(u32::try_from(i - 1).unwrap(), i == CHILDREN);
assert_eq!(out.internal[i], expected, "child {i}");
}
}
#[test]
fn construct_html_cascade_tree_handles_a_deep_chain() {
const DEPTH: usize = 1_000;
let mut hierarchy = Vec::with_capacity(DEPTH);
let mut data = Vec::with_capacity(DEPTH);
for i in 0..DEPTH {
hierarchy.push(node(
if i == 0 { None } else { Some(i - 1) },
None,
None,
if i + 1 == DEPTH { None } else { Some(i + 1) },
));
data.push(NodeData::create_div());
}
let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
let data_ref = NodeDataContainerRef::from_slice(&data);
let depths = hierarchy_ref.get_parents_sorted_by_depth();
let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
assert_eq!(out.len(), DEPTH);
for i in 0..DEPTH {
assert_eq!(
out.internal[i],
info(0, true),
"every node in a chain is an only child"
);
}
}
#[test]
fn matches_html_element_empty_path_never_matches() {
assert!(!matches(Vec::new(), 1, None));
assert!(!matches(Vec::new(), 0, Some(CssPathPseudoSelector::Hover)));
}
#[test]
fn matches_html_element_matches_type_class_and_id_on_the_subject() {
assert!(matches(vec![CssPathSelector::Global], 1, None));
assert!(matches(vec![CssPathSelector::Class("a".into())], 1, None));
assert!(!matches(vec![CssPathSelector::Class("a".into())], 3, None));
assert!(matches(vec![CssPathSelector::Id("first".into())], 1, None));
assert!(matches(
vec![CssPathSelector::Type(NodeTypeTag::Div)],
1,
None
));
assert!(!matches(
vec![CssPathSelector::Type(NodeTypeTag::Div)],
0,
None
));
assert!(matches(
vec![CssPathSelector::Type(NodeTypeTag::Body)],
0,
None
));
let div_a = vec![
CssPathSelector::Type(NodeTypeTag::Div),
CssPathSelector::Class("a".into()),
];
assert!(matches(div_a.clone(), 1, None));
assert!(!matches(div_a, 3, None));
}
#[test]
fn matches_html_element_never_matches_an_anonymous_node() {
let (hierarchy, data) = anonymous_fixture();
let hier_items = items(&hierarchy);
let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
let data_ref = NodeDataContainerRef::from_slice(&data);
let depths = hierarchy_ref.get_parents_sorted_by_depth();
let cascade = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
for id in [1usize, 2, 4] {
assert!(
!matches_html_element(
&CssPath::new(vec![CssPathSelector::Global]),
NodeId::new(id),
&NodeDataContainerRef::from_slice(&hier_items),
&data_ref,
&cascade.as_ref(),
None,
),
"anonymous node {id} must not be styled"
);
}
for id in [0usize, 3, 5] {
assert!(matches_html_element(
&CssPath::new(vec![CssPathSelector::Global]),
NodeId::new(id),
&NodeDataContainerRef::from_slice(&hier_items),
&data_ref,
&cascade.as_ref(),
None,
));
}
}
#[test]
fn matches_html_element_child_combinator_is_stricter_than_the_descendant_one() {
let child = vec![
CssPathSelector::Type(NodeTypeTag::Body),
CssPathSelector::DirectChildren,
CssPathSelector::Type(NodeTypeTag::P),
];
assert!(!matches(child, 4, None));
let descendant = vec![
CssPathSelector::Type(NodeTypeTag::Body),
CssPathSelector::Children,
CssPathSelector::Type(NodeTypeTag::P),
];
assert!(matches(descendant, 4, None));
let direct = vec![
CssPathSelector::Type(NodeTypeTag::Body),
CssPathSelector::DirectChildren,
CssPathSelector::Type(NodeTypeTag::Div),
CssPathSelector::Class("b".into()),
];
assert!(matches(direct, 3, None));
}
#[test]
fn matches_html_element_descendant_combinator_walks_the_whole_ancestor_chain() {
let close = vec![
CssPathSelector::Class("b".into()),
CssPathSelector::Children,
CssPathSelector::Class("inner".into()),
];
assert!(matches(close, 4, None));
let unrelated = vec![
CssPathSelector::Class("c".into()),
CssPathSelector::Children,
CssPathSelector::Class("inner".into()),
];
assert!(!matches(unrelated, 4, None));
}
#[test]
fn matches_html_element_general_sibling_scans_all_previous_siblings() {
let general = vec![
CssPathSelector::Class("a".into()),
CssPathSelector::GeneralSibling,
CssPathSelector::Class("c".into()),
];
assert!(matches(general, 5, None));
let backwards = vec![
CssPathSelector::Class("c".into()),
CssPathSelector::GeneralSibling,
CssPathSelector::Class("a".into()),
];
assert!(!matches(backwards, 1, None));
}
#[test]
fn matches_html_element_adjacent_sibling_matches_the_immediate_element_sibling() {
let adjacent = vec![
CssPathSelector::Class("b".into()),
CssPathSelector::AdjacentSibling,
CssPathSelector::Class("c".into()),
];
assert!(matches(adjacent, 5, None));
let not_adjacent = vec![
CssPathSelector::Class("a".into()),
CssPathSelector::AdjacentSibling,
CssPathSelector::Class("c".into()),
];
assert!(!matches(not_adjacent, 5, None));
}
#[test]
fn matches_html_element_adjacent_sibling_skips_text_nodes() {
let adjacent = vec![
CssPathSelector::Class("a".into()),
CssPathSelector::AdjacentSibling,
CssPathSelector::Class("b".into()),
];
assert!(
matches(adjacent, 3, None),
"the `+` combinator must ignore non-element (text) siblings"
);
}
#[test]
fn matches_html_element_hover_on_a_single_group_path_needs_the_expected_ending() {
let hover = vec![
CssPathSelector::Class("a".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
];
assert!(matches(
hover.clone(),
1,
Some(CssPathPseudoSelector::Hover)
));
assert!(!matches(hover.clone(), 1, None));
assert!(!matches(
hover.clone(),
1,
Some(CssPathPseudoSelector::Focus)
));
assert!(!matches(hover, 3, Some(CssPathPseudoSelector::Hover)));
}
#[test]
fn matches_html_element_hover_on_a_descendant_path_still_matches() {
let hover_descendant = vec![
CssPathSelector::Type(NodeTypeTag::Body),
CssPathSelector::Children,
CssPathSelector::Class("a".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
];
assert!(
matches(hover_descendant, 1, Some(CssPathPseudoSelector::Hover)),
":hover on the subject of a multi-group selector must still match"
);
}
#[test]
fn matches_html_element_structural_pseudos_work_on_the_subject() {
let first = vec![
CssPathSelector::Class("a".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::First),
];
assert!(matches(first, 1, None));
let last = vec![
CssPathSelector::Class("c".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::Last),
];
assert!(matches(last, 5, None));
let nth = vec![
CssPathSelector::Class("b".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
CssNthChildSelector::Number(2),
)),
];
assert!(matches(nth, 3, None));
let wrong_nth = vec![
CssPathSelector::Class("b".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
CssNthChildSelector::Number(3),
)),
];
assert!(!matches(wrong_nth, 3, None));
}
#[test]
fn matches_html_element_root_scope_confines_a_rule_to_its_subtree() {
let scoped = vec![
CssPathSelector::Root(CssScopeRange { start: 3, end: 4 }),
CssPathSelector::Global,
];
assert!(matches(scoped.clone(), 3, None));
assert!(matches(scoped.clone(), 4, None));
assert!(
!matches(scoped.clone(), 1, None),
"must not leak to a sibling"
);
assert!(
!matches(scoped.clone(), 5, None),
"must not leak to a sibling"
);
assert!(!matches(scoped, 0, None), "must not leak to the parent");
let node_only = vec![
CssPathSelector::Root(CssScopeRange { start: 3, end: 3 }),
CssPathSelector::Global,
];
assert!(matches(node_only.clone(), 3, None));
assert!(
!matches(node_only, 4, None),
"a node-only scope must not reach children"
);
}
#[test]
fn matches_html_element_with_a_dangling_combinator_does_not_panic() {
let dangling = vec![
CssPathSelector::Class("a".into()),
CssPathSelector::DirectChildren,
];
assert!(!matches(dangling.clone(), 4, None));
for id in 0..6 {
let _ = matches(dangling.clone(), id, None);
}
}
#[test]
fn matches_html_element_on_a_very_long_selector_chain_terminates() {
let mut path = Vec::new();
for _ in 0..500 {
path.push(CssPathSelector::Type(NodeTypeTag::Body));
path.push(CssPathSelector::Children);
}
path.push(CssPathSelector::Class("inner".into()));
assert!(!matches(path, 4, None));
}
}