diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index eae6b38..f58c78c 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -9004,6 +9004,17 @@ Element.prototype.attachShadow = function attachShadow(opts) { } const host = this; const children = []; + const assertInsertable = (node, operation) => { + const createsComposedCycle = node instanceof ShadowRoot + || node === host + || !!(node?.contains && node.contains(host)); + if (createsComposedCycle) { + throw new DOMException( + `Failed to execute '${operation}' on 'Node': The new child would contain the parent.`, + "HierarchyRequestError" + ); + } + }; const shadow = { mode: opts.mode, host: host, @@ -9022,44 +9033,72 @@ Element.prototype.attachShadow = function attachShadow(opts) { get firstElementChild() { return children.find(c => c.nodeType === 1) || null; }, get children() { return children.filter(c => c.nodeType === 1); }, appendChild(c) { - if (c) { - if (c._shadowParent) c._shadowParent.removeChild(c); - else if (c.parentNode) c.parentNode.removeChild(c); - children.push(c); - c._shadowParent = shadow; + if (!c) return c; + assertInsertable(c, "appendChild"); + if (c instanceof DocumentFragment) { + for (const child of Array.from(c.childNodes)) shadow.appendChild(child); + return c; } + if (c._shadowParent) c._shadowParent.removeChild(c); + else if (c.parentNode) c.parentNode.removeChild(c); + children.push(c); + c._shadowParent = shadow; return c; }, insertBefore(n, ref) { if (!n) return n; if (!ref) { shadow.appendChild(n); return n; } - const idx = children.indexOf(ref); - if (idx >= 0) { - if (n._shadowParent) n._shadowParent.removeChild(n); - else if (n.parentNode) n.parentNode.removeChild(n); - children.splice(idx, 0, n); - n._shadowParent = shadow; + if (!children.includes(ref)) { + throw new DOMException( + "Failed to execute 'insertBefore' on 'Node': The reference node is not a child of this node.", + "NotFoundError" + ); } - else shadow.appendChild(n); + if (n === ref) return n; + assertInsertable(n, "insertBefore"); + if (n instanceof DocumentFragment) { + for (const child of Array.from(n.childNodes)) shadow.insertBefore(child, ref); + return n; + } + if (n._shadowParent) n._shadowParent.removeChild(n); + else if (n.parentNode) n.parentNode.removeChild(n); + const idx = children.indexOf(ref); + children.splice(idx, 0, n); + n._shadowParent = shadow; return n; }, removeChild(c) { const idx = children.indexOf(c); - if (idx >= 0) { - children.splice(idx, 1); - if (c._shadowParent === shadow) c._shadowParent = null; + if (idx < 0) { + throw new DOMException( + "Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.", + "NotFoundError" + ); } + children.splice(idx, 1); + if (c._shadowParent === shadow) c._shadowParent = null; return c; }, replaceChild(n, o) { - const idx = children.indexOf(o); - if (idx >= 0) { - if (n._shadowParent) n._shadowParent.removeChild(n); - else if (n.parentNode) n.parentNode.removeChild(n); - children[idx] = n; - n._shadowParent = shadow; - if (o._shadowParent === shadow) o._shadowParent = null; + if (!children.includes(o)) { + throw new DOMException( + "Failed to execute 'replaceChild' on 'Node': The node to be replaced is not a child of this node.", + "NotFoundError" + ); } + if (n === o) return o; + assertInsertable(n, "replaceChild"); + if (n instanceof DocumentFragment) { + for (const child of Array.from(n.childNodes)) shadow.insertBefore(child, o); + shadow.removeChild(o); + return o; + } + if (n._shadowParent) n._shadowParent.removeChild(n); + else if (n.parentNode) n.parentNode.removeChild(n); + const idx = children.indexOf(o); + children[idx] = n; + n._shadowParent = shadow; + if (o._shadowParent === shadow) o._shadowParent = null; return o; }, querySelector(s) { diff --git a/crates/obscura-js/src/runtime.rs b/crates/obscura-js/src/runtime.rs index a966ebd..f5ea935 100644 --- a/crates/obscura-js/src/runtime.rs +++ b/crates/obscura-js/src/runtime.rs @@ -1993,7 +1993,50 @@ mod tests { inside.getRootNode() === root, root.textContent ]; - return [inserted, removed, moved, parsed]; + + const a = document.createElement("a"); + const b = document.createElement("b"); + const c = document.createElement("i"); + root.replaceChildren(a, b, c); + root.insertBefore(a, c); + const movedWithin = Array.from(root.children, el => el.localName); + + const fragment = document.createDocumentFragment(); + const x = document.createElement("x-one"); + const y = document.createElement("x-two"); + fragment.append(x, y); + root.insertBefore(fragment, c); + const flattened = [ + Array.from(root.children, el => el.localName), + fragment.childNodes.length, + x.parentNode === root, + y.parentNode === root + ]; + + root.replaceChild(b, c); + const replaced = [ + Array.from(root.children, el => el.localName), + c.parentNode === null, + b.parentNode === root + ]; + + const detached = document.createElement("detached-node"); + const errors = []; + for (const operation of [ + () => root.insertBefore(detached, c), + () => root.removeChild(c), + () => root.replaceChild(detached, c), + () => root.appendChild(root), + () => root.appendChild(host) + ]) { + try { + operation(); + errors.push("none"); + } catch (error) { + errors.push(error.name); + } + } + return [inserted, removed, moved, parsed, movedWithin, flattened, replaced, errors]; "#, ) .unwrap(); @@ -2016,7 +2059,17 @@ mod tests { ], [true, true, true], [true, true, true], - [true, true, "inside"] + [true, true, "inside"], + ["b", "a", "i"], + [["b", "a", "x-one", "x-two", "i"], 0, true, true], + [["a", "x-one", "x-two", "b"], true, true], + [ + "NotFoundError", + "NotFoundError", + "NotFoundError", + "HierarchyRequestError", + "HierarchyRequestError" + ] ]) ); }