From e0df01a2cda28b753f24bc4edb70d88024a83a6a Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 15 Oct 2026 15:38:29 +0800 Subject: [PATCH] fix(dom): retarget adopted shadow trees before reinsertion Retarget ShadowRoot ownership and descendants along with the adopted host, and collect stylesheet owners from nested shadow trees. Cover closed and nested roots, inactive documents, and deep iterative adoption. --- moli-dom/src/native/host/clone.rs | 59 +++++++++++++++++++++++++++- moli-dom/src/native/host/document.rs | 2 +- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/moli-dom/src/native/host/clone.rs b/moli-dom/src/native/host/clone.rs index 49ea83dcf7..77d9ef6b6a 100644 --- a/moli-dom/src/native/host/clone.rs +++ b/moli-dom/src/native/host/clone.rs @@ -170,7 +170,7 @@ impl DomHost { .is_some_and(Node::is_document) .then_some(())?; self.node(handle).filter(|node| !node.is_document())?; - let owners = self + let mut owners = self .dom .mark_subtree_tree_scope_collecting_stylesheet_owners( handle, @@ -178,6 +178,7 @@ impl DomHost { false, false, ); + self.mark_shadow_tree_scope_for_subtree(handle, Some(document_handle), false, &mut owners); let owner_changes = owners .into_iter() .map(|owner| { @@ -783,6 +784,54 @@ mod tests { ); } + #[test] + fn adoption_retargets_shadow_roots_and_descendants_before_reinsertion() { + let mut host = test_host(); + let source_document = host.document_handle(); + let target_document = host.create_detached_html_document(); + assert!(!host.is_connected(target_document)); + let root = host.create_element("section"); + let shadow = host + .attach_shadow_root(root, "closed") + .expect("closed root"); + let nested_host = host.create_element("div"); + assert!(host.append_child(shadow, nested_host)); + let nested_shadow = host + .attach_shadow_root(nested_host, "open") + .expect("nested root"); + let media = host.create_element("video"); + assert!(host.append_child(nested_shadow, media)); + let text = host.create_text_node("fallback"); + assert!(host.append_child(media, text)); + assert!(host.append_child(source_document, root)); + assert!(host.remove_child(source_document, root)); + + assert_eq!(host.adopt_node(target_document, root), Some(root)); + for node in [root, shadow, nested_host, nested_shadow, media, text] { + assert_eq!( + host.node(node).and_then(Node::owner_document), + Some(target_document) + ); + assert!( + !host.is_connected(node), + "adoption must not reconnect a removed subtree" + ); + } + assert_eq!(host.shadow_root_handle(root), Some(shadow)); + assert_eq!(host.shadow_root_mode(shadow).as_deref(), Some("closed")); + assert_eq!(host.shadow_root_handle(nested_host), Some(nested_shadow)); + assert!(host.append_child(target_document, root)); + assert_eq!(host.parent_node(root), Some(target_document)); + assert!( + !host.is_connected(media), + "reinsertion must retain the inactive native Document's connection state" + ); + assert_eq!( + host.node(media).and_then(Node::owner_document), + Some(target_document) + ); + } + #[test] fn deep_shadow_tree_scope_retarget_walks_iteratively() { let mut host = test_host(); @@ -804,5 +853,13 @@ mod tests { Some(document) ); assert!(host.is_connected(shadow_host)); + let target_document = host.create_detached_html_document(); + assert!(host.remove_child(document, root)); + assert_eq!(host.adopt_node(target_document, root), Some(root)); + assert_eq!( + host.node(shadow_host).and_then(Node::owner_document), + Some(target_document) + ); + assert!(!host.is_connected(shadow_host)); } } diff --git a/moli-dom/src/native/host/document.rs b/moli-dom/src/native/host/document.rs index c687626a1b..517b786300 100644 --- a/moli-dom/src/native/host/document.rs +++ b/moli-dom/src/native/host/document.rs @@ -1960,7 +1960,7 @@ impl DomHost { true } - fn mark_shadow_tree_scope_for_subtree( + pub(super) fn mark_shadow_tree_scope_for_subtree( &mut self, root: DomHandle, owner_document: Option,