import { afterEach, expect, test } from "bun:test"; import { Window } from "happy-dom"; import { act, createElement } from "react"; import { islandRootCount, mountIslands, remountIslands, unmountIslands, } from "../src/island-runtime.ts"; (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; function host(window: Window): ParentNode { return window.document.body as unknown as ParentNode; } 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; } afterEach(() => { const doc = (globalThis as any).document; if (!doc) return; act(() => { unmountIslands(doc); }); }); const marker = `
`; test("remount replaces island output without leaking roots", async () => { const window = domWith(marker); const first = async () => ({ default: () => createElement("span", null, "v1") }); const second = async () => ({ default: () => createElement("span", null, "v2") }); await act(async () => { await mountIslands(host(window), { loader: first }); }); expect(window.document.body.textContent).toContain("v1"); expect(islandRootCount()).toBe(1); await act(async () => { await remountIslands(host(window), { loader: second }); }); expect(window.document.body.textContent).toContain("v2"); expect(window.document.body.textContent).not.toContain("v1"); expect(islandRootCount()).toBe(1); }); test("repeated remounts stay at one root", async () => { const window = domWith(marker); const loader = async () => ({ default: () => createElement("span", null, "x") }); await act(async () => { await mountIslands(host(window), { loader }); }); for (let i = 0; i < 4; i += 1) { await act(async () => { await remountIslands(host(window), { loader }); }); } expect(islandRootCount()).toBe(1); }); test("remount re-renders in place instead of creating a second root", async () => { const window = domWith(marker); const loader = async () => ({ default: () => createElement("span", null, "v1") }); await act(async () => { await mountIslands(host(window), { loader }); }); expect(islandRootCount()).toBe(1); // Simulate HMR morphing server markup back over the mounted island: React's // rendered DOM is gone, so unmounting it would throw. const el = window.document.querySelector("[data-wrn-island]")!; el.innerHTML = ""; await act(async () => { await remountIslands(host(window), { loader }); }); expect(islandRootCount()).toBe(1); expect(window.document.body.textContent).toContain("v1"); }); test("a remount asks the loader for a newer generation than the mount did", async () => { // The rebuilt island keeps its URL. Without a changing generation the dev // loader re-imports the cached module and the page keeps the old code -- // silently, because the island still mounts and still works. const window = domWith(marker); const generations: number[] = []; const loader = async (_name: string, generation: number) => { generations.push(generation); return { default: () => createElement("span", null, `gen ${generation}`) }; }; await act(async () => { await mountIslands(host(window), { loader }); }); await act(async () => { await remountIslands(host(window), { loader }); }); expect(generations.length).toBe(2); expect(generations[1]).toBeGreaterThan(generations[0]!); expect(window.document.body.textContent).toContain(`gen ${generations[1]}`); });