Fix ChildNode.replaceWith throwing instead of replacing the node#623
Open
airhorns wants to merge 2 commits into
Open
Fix ChildNode.replaceWith throwing instead of replacing the node#623airhorns wants to merge 2 commits into
airhorns wants to merge 2 commits into
Conversation
…ted node ParentNode.insertBefore relinked the new child's NEXT/PREV and the reference node's PREV, but only repointed the previous sibling's NEXT when the reference node was the first child. Inserting before any later child therefore left the forward sibling chain skipping the new node. That chain is what querySelector, querySelectorAll, and firstChild/nextSibling traversal walk, so an affected node was unreachable from every selector query and sibling walk, while childNodes, parentNode, previousSibling, and the host mirror all still reported it as present. The node rendered correctly on the host and was invisible to the remote environment's own lookups, with no error raised. replaceChild, prepend of more than one node, and ChildNode.after all route through the same branch and were affected too. Hoist the reference node's previous sibling and link it forward to the new child, keeping the insert-at-front case that the old branch already handled. Co-Authored-By: Claude <noreply@anthropic.com>
replaceWith passed its arguments to replaceChild in the wrong order. replaceChild(newChild, oldChild) was called as parent.replaceChild(this, node), naming the incoming node as the child to replace. That node is usually fresh and has no parent, so the reference check rejected it and every call on a node with a parent threw "reference node is not a child of this parent". It also read the following sibling off the incoming node rather than off this, so the remaining arguments had no correct insertion point to anchor to, and a call with no arguments stringified undefined into a text node instead of removing the node. Remove this and insert the given nodes at its position in argument order, anchored on the first following sibling that is not itself being moved so that replacing a node with one of its own siblings still has a reference node left. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
ChildNode.replaceWith()passes its arguments toreplaceChild()in the wrong order. The signature isreplaceChild(newChild, oldChild), but it is called as:replaceChildrejects anoldChildthat isn't already a child of the parent. The incoming node is normally fresh with no parent, so every call on a node that has a parent throws:Two further consequences of the same mix-up:
nextis read off the incoming (detached) node, so it isnull, and the 2nd..nth arguments have no correct insertion point to anchor to.replaceWith()with no arguments takesnodes[0] === undefinedthroughtoNode, which stringifies it — so instead of removing the node it tries to insert the literal textundefined.Current behaviour on
main, every case with a parent:mainel.replaceWith(fresh)(middle child)el.replaceWith(fresh)(last child)el.replaceWith(a, b)el.replaceWith('text')el.replaceWith()el.replaceWith(fresh)Only the no-parent early return worked, so the method is entirely non-functional.
The fix
Remove
thisand insert the given nodes at its position in argument order:This matches the spec: strings become text nodes, no arguments removes the node (same as
remove()), and a node with no parent is left alone.The
whileloop is the spec's viable next sibling. Anchoring onthis[NEXT]alone breaks when a node is replaced with one of its own siblings —b.replaceWith(c)wherecisb's next sibling would movecout of the parent and then try to insert before it, throwing again. Skipping anchors that are themselves being moved keepsb.replaceWith(c)andb.replaceWith(b)correct.Why it stacks on #622
Replacing a middle child means inserting before a non-first child, which is exactly the sibling-chain bug #622 fixes. With this fix alone on
main, the replacement lands inchildNodesbut never enters the forward sibling chain:So
replaceWithcannot be observably fixed without #622, and 4 of the tests below fail without it.Tests
packages/polyfill/source/tests/ChildNode.test.ts— 12 tests covering single-node replacement at first/middle/last position, multiple nodes in argument order, strings, mixed nodes and strings, no arguments, a detached node, moving a node already in the tree, replacing a node with its own next sibling, replacing a node with itself, and sibling-chain/childNodesagreement.10 of the 12 fail on
main. The 2 that pass are the paths that happened to work — the detached early return, and self-replacement, where the reversed arguments coincidentally refer to the same node — kept as guards.Full suite (195 tests),
type-check, andlintare green.🤖 Generated with Claude Code