From 867f00e091e0fe2a0e2ad9599c0f0a740c34be77 Mon Sep 17 00:00:00 2001 From: egrs Date: Fri, 20 Feb 2026 09:06:00 +0100 Subject: [PATCH] fix ChildNode after() and replaceWith() sibling ordering after() captured node.nextSibling() once, which went stale when that sibling was one of the nodes being inserted. Use viableNextSibling() to find the first following sibling not in the nodes list per the DOM spec. replaceWith() in CData had the same stale-reference problem and also removed self before inserting, unlike Element.replaceWith() which keeps self as the insertion anchor. Adopt the same anchor pattern: insert before self, then remove self at the end. Flips ChildNode-after.html from 33/45 to 45/45 and ChildNode-replaceWith.html from 27/33 to 33/33. --- src/browser/webapi/CData.zig | 22 ++++++++++++++-------- src/browser/webapi/Element.zig | 4 ++-- src/browser/webapi/Node.zig | 19 +++++++++++++++++++ 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/browser/webapi/CData.zig b/src/browser/webapi/CData.zig index b7b6d371..dbe59fe0 100644 --- a/src/browser/webapi/CData.zig +++ b/src/browser/webapi/CData.zig @@ -227,24 +227,30 @@ pub fn before(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void { pub fn after(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void { const node = self.asNode(); const parent = node.parentNode() orelse return; - const next = node.nextSibling(); + const viable_next = Node.NodeOrText.viableNextSibling(node, nodes); for (nodes) |node_or_text| { const child = try node_or_text.toNode(page); - _ = try parent.insertBefore(child, next, page); + _ = try parent.insertBefore(child, viable_next, page); } } pub fn replaceWith(self: *CData, nodes: []const Node.NodeOrText, page: *Page) !void { - const node = self.asNode(); - const parent = node.parentNode() orelse return; - const next = node.nextSibling(); - - _ = try parent.removeChild(node, page); + const ref_node = self.asNode(); + const parent = ref_node.parentNode() orelse return; + var rm_ref_node = true; for (nodes) |node_or_text| { const child = try node_or_text.toNode(page); - _ = try parent.insertBefore(child, next, page); + if (child == ref_node) { + rm_ref_node = false; + continue; + } + _ = try parent.insertBefore(child, ref_node, page); + } + + if (rm_ref_node) { + _ = try parent.removeChild(ref_node, page); } } diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index b8732e09..82826e23 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -870,11 +870,11 @@ pub fn before(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void pub fn after(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void { const node = self.asNode(); const parent = node.parentNode() orelse return; - const next = node.nextSibling(); + const viable_next = Node.NodeOrText.viableNextSibling(node, nodes); for (nodes) |node_or_text| { const child = try node_or_text.toNode(page); - _ = try parent.insertBefore(child, next, page); + _ = try parent.insertBefore(child, viable_next, page); } } diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 124da937..d72f93e5 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -1069,6 +1069,25 @@ pub const NodeOrText = union(enum) { .text => |txt| page.createTextNode(txt), }; } + + /// DOM spec: first following sibling of `node` that is not in `nodes`. + pub fn viableNextSibling(node: *Node, nodes: []const NodeOrText) ?*Node { + var sibling = node.nextSibling() orelse return null; + blk: while (true) { + for (nodes) |n| { + switch (n) { + .node => |nn| if (sibling == nn) { + sibling = sibling.nextSibling() orelse return null; + continue :blk; + }, + .text => {}, + } + } else { + return sibling; + } + } + return null; + } }; const testing = @import("../../testing.zig");