fix(cssom): invalidate nested declaration mutations

This commit is contained in:
ldm0
2026-09-08 02:00:31 +08:00
parent df08c94744
commit 14ada1a7c2
2 changed files with 92 additions and 2 deletions
@@ -912,7 +912,7 @@ impl LiveStylesheet {
return None;
};
let ancestor_path_len = rule_path.len().saturating_sub(1);
Some(LiveStylesheetRuleMutation::new(
Some(Self::rule_mutation_for_cascade(
rule,
self.css_rule_ancestors_at_path(&rule_path[..ancestor_path_len])?,
change_kind,
@@ -925,13 +925,36 @@ impl LiveStylesheet {
parent_path: &[usize],
change_kind: RuleChangeKind,
) -> Option<LiveStylesheetRuleMutation> {
Some(LiveStylesheetRuleMutation::new(
Some(Self::rule_mutation_for_cascade(
rule,
self.css_rule_ancestors_at_path(parent_path)?,
change_kind,
))
}
fn rule_mutation_for_cascade(
rule: CssRule,
ancestors: Vec<CssRule>,
change_kind: RuleChangeKind,
) -> LiveStylesheetRuleMutation {
if matches!(&rule, CssRule::NestedDeclarations(_))
&& let Some(style_index) = ancestors
.iter()
.rposition(|ancestor| matches!(ancestor, CssRule::Style(_)))
{
// Nested declarations inherit their selector from the nearest
// enclosing style rule. Stylo therefore does not collect ordinary
// element invalidations from the declaration rule itself. Report
// the effective declaration change on that selector-bearing rule.
return LiveStylesheetRuleMutation::new(
ancestors[style_index].clone(),
ancestors[..style_index].to_vec(),
RuleChangeKind::StyleRuleDeclarations,
);
}
LiveStylesheetRuleMutation::new(rule, ancestors, change_kind)
}
fn css_rule_ancestors_at_path(&self, path: &[usize]) -> Option<Vec<CssRule>> {
let mut ancestors = Vec::with_capacity(path.len());
for depth in 1..=path.len() {
@@ -3254,6 +3254,73 @@ getComputedStyle(document.querySelector('.nested-journal-target')).color;
);
}
#[test]
fn nested_declaration_mutations_invalidate_their_ancestor_style_selector() {
let mut vm = new_storage_test_vm("https://nested-declaration-invalidation.test/");
assert_eq!(
vm.eval(
r#"
const style = document.createElement('style');
style.textContent = 'div { z-index: 1; &.test { } }';
(document.head || document.documentElement || document).appendChild(style);
const target = document.createElement('div');
target.className = 'test';
(document.body || document.documentElement || document).appendChild(target);
globalThis.__nestedDeclarationRule = style.sheet.cssRules[0];
globalThis.__nestedDeclarationTarget = target;
getComputedStyle(target).zIndex;
"#,
)
.expect("nested declaration invalidation setup should evaluate"),
"1"
);
crate::style_engine::reset_live_stylesheet_update_counts_for_test();
assert_eq!(
vm.eval(
r#"
const rule = globalThis.__nestedDeclarationRule;
const mutationTarget = globalThis.__nestedDeclarationTarget;
const states = [];
rule.insertRule('z-index: 3;', 0);
const declarations = rule.cssRules[0];
states.push(declarations instanceof CSSNestedDeclarations);
states.push(getComputedStyle(mutationTarget).zIndex);
declarations.style.zIndex = '4';
states.push(getComputedStyle(mutationTarget).zIndex);
rule.deleteRule(0);
states.push(getComputedStyle(mutationTarget).zIndex);
rule.insertRule('@media all { a { } }', 1);
const media = rule.cssRules[1];
media.insertRule('z-index: 5;', 0);
states.push(media.cssRules[0] instanceof CSSNestedDeclarations);
states.push(getComputedStyle(mutationTarget).zIndex);
media.deleteRule(0);
states.push(getComputedStyle(mutationTarget).zIndex);
states.join('|');
"#,
)
.expect("nested declaration mutations should invalidate ancestor selectors"),
"true|3|4|1|true|5|1"
);
assert_eq!(
crate::style_engine::exact_rule_change_notification_count_for_test(),
6,
"nested declaration changes should remain exact journal updates",
);
assert_eq!(
crate::style_engine::full_cascade_update_fallback_count_for_test(),
0,
"nested declaration changes must not rebuild an entire stylesheet",
);
}
#[test]
fn computed_style_wrapper_reflects_style_element_media_mutations() {
let mut vm = new_storage_test_vm("https://style-media-computed-wrapper-refresh.test/");