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; // happy-dom's element types do not structurally match lib.dom's ParentNode; // this cast is a test-environment concern, not a runtime one. 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; } 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 ( `
` ); } test("mounts an island and passes deserialized props", async () => { const window = domWith(marker('{"title":"Revenue"}')); await act(async () => { await mountIslands(host(window), { 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(host(window), { loader }); }); expect(islandRootCount()).toBe(1); act(() => { unmountIslands(host(window)); }); 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(host(window), { loader }); }); act(() => { unmountIslands(host(window)); }); } expect(islandRootCount()).toBe(0); }); test("does nothing when no island markers are present", async () => { const window = domWith(`

plain server html

`); await act(async () => { await mountIslands(host(window), { 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(host(window), { loader }); await mountIslands(host(window), { 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(host(window), { 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(host(window), { loader }); }); console.error = original; expect(window.document.body.textContent).toContain("none"); expect(islandRootCount()).toBe(1); });