Bug: infinite synchronous loop in getParentHydrationBoundary during event dispatch when <html> is rendered inside a container div (jsdom + portaled focus content)#37065
Summary
react-dom enters an infinite synchronous loop inside the event-dispatch path — getClosestInstanceFromNode → getParentHydrationBoundary → repeated previousSibling walking — when:
- the rendered tree contains an
<html>element nested inside a container<div>(the default React Testing Library mount, under jsdom), and - an event is dispatched that involves portaled content with focus guards (e.g. opening a Radix UI dialog, or pointer/focus events after such content has existed in the document).
Because the loop is synchronous microtask/dispatch work, it never yields: the test runner's timeouts never fire and the process spins at 100% CPU indefinitely. Where it strikes is nondeterministic across runs (whichever event dispatch first hits the walk), which makes it look like flaky "hanging tests".
Reproduction (18 lines, vitest + jsdom)
import * as Dialog from "@radix-ui/react-dialog";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
describe("nested-html repro", () => {
it("hangs forever on the click", async () => {
const user = userEvent.setup();
render(
<html lang="en">
<body>
<Dialog.Root>
<Dialog.Trigger>open</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>Hello</Dialog.Title>
<button>inside</button>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</body>
</html>,
);
await user.click(screen.getByText("open")); // ← never returns
expect(await screen.findByRole("dialog")).toBeInTheDocument();
});
});