mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-30 17:25:58 -04:00
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 <script>). - Fragment parsing no longer notifies observers per inserted child: every other fragment-parse target is a detached DocumentFragment where notification is a no-op. - Element.setOuterHTML moves the parsed nodes in with notification suppressed and queues one record with removedNodes=[the element], addedNodes=parsed children and the element's siblings. - The unit test mutation_observer/childlist.html expected the old 6-record innerHTML behavior; updated to the spec's single record (matches Chrome). Coverage: /dom/nodes/MutationObserver-inner-outer.html 0/3 -> 3/3 (fully green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2558,14 +2558,13 @@ pub fn _insertNodeRelative(self: *Frame, comptime from_parser: bool, parent: *No
|
||||
}
|
||||
}
|
||||
|
||||
// The parser path does its own (limited) notification and connected-callback
|
||||
// work, then returns.
|
||||
// The parser path does its own (limited) connected-callback work, then
|
||||
// returns.
|
||||
if (comptime from_parser) {
|
||||
// Of the parser insertions, only fragment parses (innerHTML) mutate a
|
||||
// live tree; the initial document parse suppresses notifications.
|
||||
if (self._parse_mode == .fragment) {
|
||||
self.notifyChildInserted(parent, child);
|
||||
}
|
||||
// No mutation records from parser insertions: the initial document
|
||||
// parse never notifies, and fragment parses (innerHTML et al.) queue
|
||||
// one combined "replace all" record at the call site (Node.setHTML)
|
||||
// instead of one per inserted child.
|
||||
|
||||
if (child.is(Element)) |el| {
|
||||
// Invoke connectedCallback for custom elements during parsing.
|
||||
@@ -2821,21 +2820,11 @@ fn parseHtmlAsChildrenInner(self: *Frame, node: *Node, html: []const u8, opts: F
|
||||
}
|
||||
node._children = first._children;
|
||||
|
||||
if (observers.hasMutationObservers(self)) {
|
||||
var it = node.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
child._parent = node;
|
||||
// Notify mutation observers for each unwrapped child
|
||||
const previous_sibling = child.previousSibling();
|
||||
const next_sibling = child.nextSibling();
|
||||
const added = [_]*Node{child};
|
||||
observers.notifyChildListChange(self, node, &added, &.{}, previous_sibling, next_sibling);
|
||||
}
|
||||
} else {
|
||||
var it = node.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
child._parent = node;
|
||||
}
|
||||
// No mutation records for the unwrapped children either; see the comment
|
||||
// about fragment parses in _insertNodeRelative.
|
||||
var it = node.childrenIterator();
|
||||
while (it.next()) |child| {
|
||||
child._parent = node;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -269,41 +269,18 @@
|
||||
innerHtmlParent.innerHTML = '<span>New 1</span><span>New 2</span><span>New 3</span>';
|
||||
|
||||
await state.done((mutations) => {
|
||||
// innerHTML triggers mutations for both removals and additions
|
||||
// With tri-state: from_parser=true + parse_mode=fragment -> mutations fire
|
||||
// HTML wrapper element is filtered out, so: 3 removals + 3 additions = 6
|
||||
testing.expectEqual(6, mutations.length);
|
||||
// Per the "replace all" algorithm, innerHTML queues a single mutation
|
||||
// record combining all removed and added nodes.
|
||||
testing.expectEqual(1, mutations.length);
|
||||
|
||||
// First 3: removals
|
||||
testing.expectEqual('childList', mutations[0].type);
|
||||
testing.expectEqual(1, mutations[0].removedNodes.length);
|
||||
testing.expectEqual(3, mutations[0].removedNodes.length);
|
||||
testing.expectEqual(oldChildren[0], mutations[0].removedNodes[0]);
|
||||
testing.expectEqual(0, mutations[0].addedNodes.length);
|
||||
|
||||
testing.expectEqual('childList', mutations[1].type);
|
||||
testing.expectEqual(1, mutations[1].removedNodes.length);
|
||||
testing.expectEqual(oldChildren[1], mutations[1].removedNodes[0]);
|
||||
testing.expectEqual(0, mutations[1].addedNodes.length);
|
||||
|
||||
testing.expectEqual('childList', mutations[2].type);
|
||||
testing.expectEqual(1, mutations[2].removedNodes.length);
|
||||
testing.expectEqual(oldChildren[2], mutations[2].removedNodes[0]);
|
||||
testing.expectEqual(0, mutations[2].addedNodes.length);
|
||||
|
||||
// Last 3: additions (unwrapped span elements)
|
||||
testing.expectEqual('childList', mutations[3].type);
|
||||
testing.expectEqual(0, mutations[3].removedNodes.length);
|
||||
testing.expectEqual(1, mutations[3].addedNodes.length);
|
||||
testing.expectEqual('SPAN', mutations[3].addedNodes[0].nodeName);
|
||||
|
||||
testing.expectEqual('childList', mutations[4].type);
|
||||
testing.expectEqual(0, mutations[4].removedNodes.length);
|
||||
testing.expectEqual(1, mutations[4].addedNodes.length);
|
||||
testing.expectEqual('SPAN', mutations[4].addedNodes[0].nodeName);
|
||||
|
||||
testing.expectEqual('childList', mutations[5].type);
|
||||
testing.expectEqual(0, mutations[5].removedNodes.length);
|
||||
testing.expectEqual(1, mutations[5].addedNodes.length);
|
||||
testing.expectEqual('SPAN', mutations[5].addedNodes[0].nodeName);
|
||||
testing.expectEqual(oldChildren[1], mutations[0].removedNodes[1]);
|
||||
testing.expectEqual(oldChildren[2], mutations[0].removedNodes[2]);
|
||||
testing.expectEqual(3, mutations[0].addedNodes.length);
|
||||
testing.expectEqual('SPAN', mutations[0].addedNodes[0].nodeName);
|
||||
testing.expectEqual('SPAN', mutations[0].addedNodes[1].nodeName);
|
||||
testing.expectEqual('SPAN', mutations[0].addedNodes[2].nodeName);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user