Skip to content

Commit 90fc4ae

Browse files
committed
Fix Node#contains infinite loop for indirect descendants
The contains() walk re-read the ORIGINAL node's parentNode on every iteration (currentNode = node.parentNode instead of currentNode.parentNode), so it never advanced past the first step: direct children happened to return true (the first step hits the receiver), but any indirect descendant re-read the same middle node forever. In a synchronous environment such as a Web Worker running the remote DOM polyfill, that single call deadlocks the entire event loop with no exception — reported downstream as a silent permanent freeze in twenty's front component sandbox (twentyhq/twenty#24573). Advance the walk with currentNode.parentNode, per the Node.prototype.contains contract. New node.test.ts covers direct and indirect descendants, self, detached nodes, sibling subtrees and null. Differential: on the previous code the indirect-descendant case does not merely fail — it hangs the test runner indefinitely (verified by killing the run after several minutes and by an isolated child-process repro that never terminates); with this change the whole suite passes in under five seconds.
1 parent 1780f15 commit 90fc4ae

2 files changed

Lines changed: 61 additions & 1 deletion

File tree

packages/polyfill/source/Node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export class Node extends EventTarget {
160160
while (true) {
161161
if (currentNode == null) return false;
162162
if (currentNode === this) return true;
163-
currentNode = node!.parentNode;
163+
currentNode = currentNode.parentNode;
164164
}
165165
}
166166
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {Window} from '../index.ts';
2+
3+
import {describe, it, expect, beforeEach} from 'vitest';
4+
5+
describe('Node#contains', () => {
6+
beforeEach(() => {
7+
const window = new Window();
8+
Window.setGlobalThis(window);
9+
});
10+
11+
it('returns true for a direct child', () => {
12+
const parent = document.createElement('div');
13+
const child = document.createElement('span');
14+
parent.append(child);
15+
16+
expect(parent.contains(child)).toBe(true);
17+
});
18+
19+
it('returns true for an indirect descendant', () => {
20+
const ancestor = document.createElement('div');
21+
const middle = document.createElement('div');
22+
const descendant = document.createElement('span');
23+
middle.append(descendant);
24+
ancestor.append(middle);
25+
26+
// The walk used to re-read the original node's parentNode instead of
27+
// advancing, so any indirect descendant looped forever.
28+
expect(ancestor.contains(descendant)).toBe(true);
29+
});
30+
31+
it('returns true for the node itself', () => {
32+
const element = document.createElement('div');
33+
34+
expect(element.contains(element)).toBe(true);
35+
});
36+
37+
it('returns false for a detached node', () => {
38+
const element = document.createElement('div');
39+
const detached = document.createElement('span');
40+
41+
expect(element.contains(detached)).toBe(false);
42+
});
43+
44+
it('returns false for sibling subtrees', () => {
45+
const root = document.createElement('div');
46+
const left = document.createElement('div');
47+
const right = document.createElement('div');
48+
const rightChild = document.createElement('span');
49+
right.append(rightChild);
50+
root.append(left, right);
51+
52+
expect(left.contains(rightChild)).toBe(false);
53+
});
54+
55+
it('returns false for null', () => {
56+
const element = document.createElement('div');
57+
58+
expect(element.contains(null)).toBe(false);
59+
});
60+
});

0 commit comments

Comments
 (0)