From ce06c1c5dd49c92b5dc7934adb954e00a9e443d1 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Sat, 11 Jul 2026 14:10:31 +0200 Subject: [PATCH] webapi: innerHTML/outerHTML queue one combined mutation record Fixes WPT /dom/nodes/MutationObserver-inner-outer.html (0/3 -> 3/3): setting innerHTML must queue a single "replace all" mutation record (all removed children + all parsed children), and setting outerHTML a single record replacing the element with the parsed nodes. We emitted one record per removed child plus one per inserted child. - Node.setHTML suppresses per-node records (removals via the new notify_observers opt, insertions by making fragment parsing never notify) and queues the combined record itself. Parsing still targets the element directly so html5ever keeps the right fragment context (e.g. raw-text content of diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index aeee38879..efcc7aaff 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -472,13 +472,34 @@ pub fn setOuterHTML(self: *Element, html: []const u8, frame: *Frame) !void { } frame.domChanged(); + + // Observers of the parent must see a single mutation record replacing + // this node with the parsed nodes. + const notify = Frame.observers.hasMutationObservers(frame); + const previous_sibling = node.previousSibling(); + const next_sibling = node.nextSibling(); + var added: std.ArrayList(*Node) = .empty; + if (html.len > 0) { const fragment = (try Node.DocumentFragment.init(frame)).asNode(); try frame.parseHtmlAsChildren(fragment, html); - try frame.insertAllChildrenBefore(fragment, parent, node); + const dest_connected = parent.isConnected(); + var it = fragment.childrenIterator(); + while (it.next()) |child| { + if (notify) { + try added.append(frame.call_arena, child); + } + frame.removeNode(fragment, child, .{ .will_be_reconnected = dest_connected, .notify_observers = false }); + try frame.insertNodeRelative(parent, child, .{ .before = node }, .{ .notify_observers = false }); + } } - frame.removeNode(parent, node, .{ .will_be_reconnected = false }); + frame.removeNode(parent, node, .{ .will_be_reconnected = false, .notify_observers = false }); + + if (notify) { + const removed = [_]*Node{node}; + Frame.observers.notifyChildListChange(frame, parent, added.items, &removed, previous_sibling, next_sibling); + } } pub fn getInnerHTML(self: *Element, writer: *std.Io.Writer, frame: *Frame) !void { diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index bbe5d1f9e..c5d2d3a26 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -1316,19 +1316,38 @@ pub fn replaceChildren(self: *Node, nodes: []const NodeOrText, frame: *Frame) !v /// Shared implementation in Element and DocumentFragment pub fn setHTML(self: *Node, html: []const u8, allow_declarative_shadow: bool, frame: *Frame) !void { frame.domChanged(); + + // Observers of this subtree get one combined "replace all" mutation + // record; per-node notification is suppressed for the removals here and + // for the parser insertions (fragment parsing never notifies). + const notify = Frame.observers.hasMutationObservers(frame); + var removed: std.ArrayList(*Node) = .empty; + var it = self.childrenIterator(); while (it.next()) |child| { - frame.removeNode(self, child, .{ .will_be_reconnected = false }); + if (notify) { + try removed.append(frame.call_arena, child); + } + frame.removeNode(self, child, .{ .will_be_reconnected = false, .notify_observers = false }); } - if (html.len == 0) { - return; + if (html.len > 0) { + if (allow_declarative_shadow) { + try frame.parseHtmlUnsafeAsChildren(self, html); + } else { + try frame.parseHtmlAsChildren(self, html); + } } - if (allow_declarative_shadow) { - try frame.parseHtmlUnsafeAsChildren(self, html); - } else { - try frame.parseHtmlAsChildren(self, html); + if (notify) { + var added: std.ArrayList(*Node) = .empty; + var child_it = self.childrenIterator(); + while (child_it.next()) |child| { + try added.append(frame.call_arena, child); + } + if (removed.items.len > 0 or added.items.len > 0) { + Frame.observers.notifyChildListChange(frame, self, added.items, removed.items, null, null); + } } }