Skip to content

Commit a091551

Browse files
ursmclaude
andcommitted
fix(dom): getRootNode honours the composed option (−1)
`getRootNode()` ignored its options and always walked to the topmost node, crossing shadow boundaries (a ShadowRoot's `_parent` is its host). Per DOM "get the root", the default (`composed: false`) returns the node's own root and stops at a shadow boundary; only `composed: true` returns the shadow-including root. So a node in a shadow tree now returns its ShadowRoot, not the document. The one internal consumer relying on the old shadow-crossing behaviour — `moveBefore`'s same-(shadow-including-)tree check — now passes `{composed: true}` explicitly (the symmetric WPT allowlist caught this regression on regen). Clears rootNode.html. Gate 660/0/15, gem 1586/0/35, 0 regressions (incl. moveBefore). Shadow-only public-API semantics, not a hot path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2b1792a commit a091551

3 files changed

Lines changed: 23 additions & 12 deletions

File tree

lib/capybara/simulated/js/bridge.bundle.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7071,9 +7071,13 @@
70717071
this.nodeType = NODE_ELEMENT;
70727072
this._ownerDoc = null;
70737073
}
7074-
getRootNode(_options) {
7074+
getRootNode(options) {
7075+
const composed = !!(options && options.composed);
70757076
let cur = this;
7076-
while (cur._parent) cur = cur._parent;
7077+
while (cur._parent) {
7078+
if (!composed && cur._isShadowRoot) break;
7079+
cur = cur._parent;
7080+
}
70777081
return cur;
70787082
}
70797083
// Per DOM, `nodeValue` is null for every node type except Attr (its value)
@@ -12528,7 +12532,7 @@
1252812532
assertNodeArg(node);
1252912533
if (child != null) assertNodeArg(child);
1253012534
const parent = this;
12531-
if (node.getRootNode() !== parent.getRootNode()) {
12535+
if (node.getRootNode({ composed: true }) !== parent.getRootNode({ composed: true })) {
1253212536
throw hierarchyError("moveBefore: node and new parent are not in the same tree");
1253312537
}
1253412538
if (isInclusiveAncestor(node, parent)) {

lib/capybara/simulated/js/src/dom-nodes.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -758,9 +758,18 @@ class Node {
758758
this._ownerDoc = null; // set by createElement/adopt; pre-init keeps the hidden class STABLE so the
759759
// per-element hot readers (find/visible_text/cascade) hit monomorphic ICs.
760760
}
761-
getRootNode(_options) {
761+
getRootNode(options) {
762+
// DOM "get the root": the topmost node reached by following `_parent`.
763+
// `composed: false` (the default) stops at a shadow boundary — a
764+
// ShadowRoot's `_parent` IS its host, so without the break we'd cross into
765+
// the light tree and wrongly report the document. `composed: true` returns
766+
// the shadow-INCLUDING root and keeps climbing across the boundary.
767+
const composed = !!(options && options.composed);
762768
let cur = this;
763-
while (cur._parent) cur = cur._parent;
769+
while (cur._parent) {
770+
if (!composed && cur._isShadowRoot) break;
771+
cur = cur._parent;
772+
}
764773
return cur;
765774
}
766775
// Per DOM, `nodeValue` is null for every node type except Attr (its value)
@@ -5853,10 +5862,11 @@ function moveBeforeImpl(node, child) {
58535862

58545863
const parent = this;
58555864
// Ensure pre-move validity (https://whatpr.org/dom/1307.html#concept-node-ensure-pre-move-validity).
5856-
// 1. node and parent must share a shadow-including root (getRootNode already
5857-
// crosses shadow boundaries via ShadowRoot._parent = host). This subsumes
5858-
// the "both connected", cross-document and cross-tree cases.
5859-
if (node.getRootNode() !== parent.getRootNode()) {
5865+
// 1. node and parent must share a shadow-including root. `composed: true`
5866+
// crosses shadow boundaries (ShadowRoot._parent = host) — plain
5867+
// getRootNode() now stops at a shadow root per spec. This subsumes the
5868+
// "both connected", cross-document and cross-tree cases.
5869+
if (node.getRootNode({ composed: true }) !== parent.getRootNode({ composed: true })) {
58605870
throw hierarchyError('moveBefore: node and new parent are not in the same tree');
58615871
}
58625872
// 2. node must not be a (host-including) inclusive ancestor of parent.

spec/support/wpt_expected_failures.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -337,9 +337,6 @@ dom/nodes/remove-and-adopt-thcrash.html:
337337
doesn't crash.
338338
dom/nodes/remove-next-sibling-during-replace-with.html:
339339
- remove-next-sibling-during-replace-with
340-
dom/nodes/rootNode.html:
341-
- getRootNode() must return context object's shadow-including root if options's composed
342-
is true, and context object's root otherwise
343340
dom/ranges/Range-adopt-test.html:
344341
- 'Parentless range container moved to another document with appendChild: Removing
345342
the only element in the range must collapse the range'

0 commit comments

Comments
 (0)