diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index db39028..fb4b90c 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -663,6 +663,11 @@ class Node { get previousSibling() { return _wrap(+_dom("prev_sibling", this._nid)); } appendChild(c) { if (!c) return c; + if (c instanceof DocumentFragment) { + const children = Array.from(c.childNodes); + for (const child of children) this.appendChild(child); + return c; + } _dom("append_child", this._nid, c._nid); if (globalThis.__mutationObservers?.length) globalThis.__notifyMutation('childList', this._nid, [c._nid], []); if (c instanceof Element && c.tagName === 'SCRIPT') { @@ -745,6 +750,12 @@ class Node { } replaceChild(newChild, oldChild) { if (!oldChild || !newChild) return oldChild; + if (newChild instanceof DocumentFragment) { + const children = Array.from(newChild.childNodes); + for (const child of children) this.insertBefore(child, oldChild); + this.removeChild(oldChild); + return oldChild; + } _dom("insert_before", newChild._nid, oldChild._nid); _dom("remove_child", oldChild._nid); return oldChild; @@ -752,6 +763,11 @@ class Node { insertBefore(n, ref) { if (!n) return n; if (!ref) { this.appendChild(n); return n; } + if (n instanceof DocumentFragment) { + const children = Array.from(n.childNodes); + for (const child of children) this.insertBefore(child, ref); + return n; + } _dom("insert_before", n._nid, ref._nid); return n; } diff --git a/crates/obscura-js/src/runtime.rs b/crates/obscura-js/src/runtime.rs index eb9ecde..1f6e766 100644 --- a/crates/obscura-js/src/runtime.rs +++ b/crates/obscura-js/src/runtime.rs @@ -1377,6 +1377,112 @@ mod tests { ); } + #[test] + fn append_child_flattens_document_fragment() { + let mut rt = setup_runtime(r#"
"#); + rt.run_page_init(); + let result = rt + .evaluate( + r#" + const host = document.getElementById('host'); + const fragment = document.createDocumentFragment(); + const first = document.createElement('article'); + const second = document.createElement('article'); + first.id = 'first'; + second.id = 'second'; + first.className = second.className = 'quote'; + fragment.appendChild(first); + fragment.appendChild(second); + + const returned = host.appendChild(fragment); + return [ + returned === fragment, + Array.from(host.children).map(node => node.id), + host.querySelectorAll('.quote').length, + fragment.childNodes.length, + first.parentNode === host, + first.parentElement === host, + ]; + "#, + ) + .unwrap(); + assert_eq!( + result, + serde_json::json!([true, ["first", "second"], 2, 0, true, true]) + ); + } + + #[test] + fn insert_before_flattens_document_fragment_in_order() { + let mut rt = setup_runtime(r#"
"#); + rt.run_page_init(); + let result = rt + .evaluate( + r#" + const host = document.getElementById('host'); + const last = document.getElementById('last'); + const fragment = document.createDocumentFragment(); + const first = document.createElement('article'); + const second = document.createElement('article'); + first.id = 'first'; + second.id = 'second'; + fragment.appendChild(first); + fragment.appendChild(second); + + const returned = host.insertBefore(fragment, last); + return [ + returned === fragment, + Array.from(host.children).map(node => node.id), + fragment.childNodes.length, + first.parentElement === host, + second.parentElement === host, + ]; + "#, + ) + .unwrap(); + assert_eq!( + result, + serde_json::json!([true, ["first", "second", "last"], 0, true, true]) + ); + } + + #[test] + fn replace_child_flattens_document_fragment_and_removes_old_child() { + let mut rt = setup_runtime( + r#"
"#, + ); + rt.run_page_init(); + let result = rt + .evaluate( + r#" + const host = document.getElementById('host'); + const old = document.getElementById('old'); + const fragment = document.createDocumentFragment(); + const first = document.createElement('article'); + const second = document.createElement('article'); + first.id = 'first'; + second.id = 'second'; + fragment.appendChild(first); + fragment.appendChild(second); + + const returned = host.replaceChild(fragment, old); + return [ + returned === old, + Array.from(host.children).map(node => node.id), + fragment.childNodes.length, + old.parentNode === null, + first.parentElement === host, + second.parentElement === host, + ]; + "#, + ) + .unwrap(); + assert_eq!( + result, + serde_json::json!([true, ["first", "second", "tail"], 0, true, true, true]) + ); + } + #[test] fn test_inner_html() { let mut rt = setup_runtime(r#"

Hello

"#);