Files
WRNexusJS/packages/react/test/island-runtime.test.ts
T
ClintchizandClaude Opus 5 d269772a79 feat(react): add island mount strategies and navigation-safe unmount
Mounts markers with client:only/load/visible/idle, and disposes roots on
route change so React roots, detached DOM, and store subscriptions do
not leak across client-side navigation.

Bundle load failures and malformed props JSON degrade to a warning and
leave the server markup intact rather than taking down the page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:20:05 +05:30

125 lines
3.4 KiB
TypeScript

import { afterEach, expect, test } from "bun:test";
import { Window } from "happy-dom";
import { act, createElement } from "react";
import { islandRootCount, mountIslands, unmountIslands } from "../src/island-runtime.ts";
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
function domWith(html: string) {
const window = new Window();
window.document.body.innerHTML = html;
(globalThis as any).window = window;
(globalThis as any).document = window.document;
return window;
}
const loader = async () => ({
default: (props: { title?: string }) => createElement("span", null, props.title ?? "none"),
});
afterEach(() => {
const doc = (globalThis as any).document;
if (!doc) return;
act(() => {
unmountIslands(doc);
});
});
function marker(props = "{}", strategy = "only") {
return (
`<div data-wrn-island="Chart" data-wrn-island-strategy="${strategy}"` +
` data-wrn-island-props='${props}'></div>`
);
}
test("mounts an island and passes deserialized props", async () => {
const window = domWith(marker('{"title":"Revenue"}'));
await act(async () => {
await mountIslands(window.document.body, { loader });
});
expect(window.document.body.textContent).toContain("Revenue");
expect(islandRootCount()).toBe(1);
});
test("unmounts roots and leaves no leaked roots behind", async () => {
const window = domWith(marker('{"title":"A"}'));
await act(async () => {
await mountIslands(window.document.body, { loader });
});
expect(islandRootCount()).toBe(1);
act(() => {
unmountIslands(window.document.body);
});
expect(islandRootCount()).toBe(0);
});
test("repeated mount/unmount cycles do not accumulate roots", async () => {
const window = domWith(marker());
for (let i = 0; i < 5; i += 1) {
await act(async () => {
await mountIslands(window.document.body, { loader });
});
act(() => {
unmountIslands(window.document.body);
});
}
expect(islandRootCount()).toBe(0);
});
test("does nothing when no island markers are present", async () => {
const window = domWith(`<p>plain server html</p>`);
await act(async () => {
await mountIslands(window.document.body, { loader });
});
expect(islandRootCount()).toBe(0);
});
test("mounting twice does not create a second root for the same element", async () => {
const window = domWith(marker());
await act(async () => {
await mountIslands(window.document.body, { loader });
await mountIslands(window.document.body, { loader });
});
expect(islandRootCount()).toBe(1);
});
test("a failing bundle load leaves the placeholder and mounts no root", async () => {
const window = domWith(marker());
const original = console.error;
console.error = () => {};
await act(async () => {
await mountIslands(window.document.body, {
loader: async () => {
throw new Error("network down");
},
});
});
console.error = original;
expect(islandRootCount()).toBe(0);
expect(window.document.querySelector("[data-wrn-island]")).not.toBeNull();
});
test("malformed props JSON falls back to empty props instead of throwing", async () => {
const window = domWith(marker("not-json"));
const original = console.error;
console.error = () => {};
await act(async () => {
await mountIslands(window.document.body, { loader });
});
console.error = original;
expect(window.document.body.textContent).toContain("none");
expect(islandRootCount()).toBe(1);
});