From 90fc4ae931e7b3a653862e3be19bd90078cbca0c Mon Sep 17 00:00:00 2001 From: unknown <1784931579@qq.com> Date: Sat, 22 Aug 2026 13:45:35 +0800 Subject: [PATCH] Fix Node#contains infinite loop for indirect descendants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/polyfill/source/Node.ts | 2 +- packages/polyfill/source/tests/node.test.ts | 60 +++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 packages/polyfill/source/tests/node.test.ts diff --git a/packages/polyfill/source/Node.ts b/packages/polyfill/source/Node.ts index af1a5705..864f4922 100644 --- a/packages/polyfill/source/Node.ts +++ b/packages/polyfill/source/Node.ts @@ -160,7 +160,7 @@ export class Node extends EventTarget { while (true) { if (currentNode == null) return false; if (currentNode === this) return true; - currentNode = node!.parentNode; + currentNode = currentNode.parentNode; } } } diff --git a/packages/polyfill/source/tests/node.test.ts b/packages/polyfill/source/tests/node.test.ts new file mode 100644 index 00000000..b557dcac --- /dev/null +++ b/packages/polyfill/source/tests/node.test.ts @@ -0,0 +1,60 @@ +import {Window} from '../index.ts'; + +import {describe, it, expect, beforeEach} from 'vitest'; + +describe('Node#contains', () => { + beforeEach(() => { + const window = new Window(); + Window.setGlobalThis(window); + }); + + it('returns true for a direct child', () => { + const parent = document.createElement('div'); + const child = document.createElement('span'); + parent.append(child); + + expect(parent.contains(child)).toBe(true); + }); + + it('returns true for an indirect descendant', () => { + const ancestor = document.createElement('div'); + const middle = document.createElement('div'); + const descendant = document.createElement('span'); + middle.append(descendant); + ancestor.append(middle); + + // The walk used to re-read the original node's parentNode instead of + // advancing, so any indirect descendant looped forever. + expect(ancestor.contains(descendant)).toBe(true); + }); + + it('returns true for the node itself', () => { + const element = document.createElement('div'); + + expect(element.contains(element)).toBe(true); + }); + + it('returns false for a detached node', () => { + const element = document.createElement('div'); + const detached = document.createElement('span'); + + expect(element.contains(detached)).toBe(false); + }); + + it('returns false for sibling subtrees', () => { + const root = document.createElement('div'); + const left = document.createElement('div'); + const right = document.createElement('div'); + const rightChild = document.createElement('span'); + right.append(rightChild); + root.append(left, right); + + expect(left.contains(rightChild)).toBe(false); + }); + + it('returns false for null', () => { + const element = document.createElement('div'); + + expect(element.contains(null)).toBe(false); + }); +});