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>
125 lines
3.4 KiB
TypeScript
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);
|
|
});
|