diff --git a/moli-renderer-v8/src/live_stylesheet/mutation.rs b/moli-renderer-v8/src/live_stylesheet/mutation.rs index 8e8d73a9c4..b84dff0652 100644 --- a/moli-renderer-v8/src/live_stylesheet/mutation.rs +++ b/moli-renderer-v8/src/live_stylesheet/mutation.rs @@ -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 { - 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, + 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> { let mut ancestors = Vec::with_capacity(path.len()); for depth in 1..=path.len() { diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs index 4606cfe481..0fe37d6171 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/computed_style.rs @@ -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/");