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); }); /** * Stand in for the browser's IntersectionObserver, reporting only targets that * actually have area. * * That is the case the real one is inconsistent about: an island placeholder * is empty until it mounts, so it is zero-height, and an engine that declines * to report it leaves the island unmounted forever. */ function installAreaOnlyObserver(window: Window, onObserve?: () => void) { const observed: Element[] = []; (globalThis as any).IntersectionObserver = class { constructor(private callback: (entries: { isIntersecting: boolean }[]) => void) {} observe(element: Element) { observed.push(element); onObserve?.(); const rect = (element as unknown as HTMLElement).getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) this.callback([{ isIntersecting: true }]); } disconnect() {} }; return observed; } /** Place the island marker at a given position with a given size. */ function positionIsland(window: Window, top: number, height: number) { const element = window.document.querySelector("[data-wrn-island]") as unknown as HTMLElement; element.getBoundingClientRect = () => ({ top, bottom: top + height, left: 0, right: 800, width: 800, height }) as DOMRect; return element; } test("mounts a visible island whose placeholder has no height", async () => { const window = domWith(marker("{}", "visible")); installAreaOnlyObserver(window); positionIsland(window, 40, 0); await act(async () => { await mountIslands(host(window), { loader }); }); expect(islandRootCount()).toBe(1); delete (globalThis as any).IntersectionObserver; }); test("a visible island below the fold waits, then mounts once scrolled to", async () => { const window = domWith(marker("{}", "visible")); installAreaOnlyObserver(window); positionIsland(window, 5000, 0); let settled = false; // Started outside act: it stays pending until the scroll, and the render it // then performs is what act needs to wrap. const mounting = mountIslands(host(window), { loader }).then(() => { settled = true; }); await new Promise((resolve) => setTimeout(resolve, 10)); expect(settled).toBe(false); expect(islandRootCount()).toBe(0); positionIsland(window, 100, 0); await act(async () => { window.dispatchEvent(new window.Event("scroll")); await mounting; }); expect(settled).toBe(true); expect(islandRootCount()).toBe(1); delete (globalThis as any).IntersectionObserver; }); test("a placeholder with real size still goes through the observer", async () => { const window = domWith(marker("{}", "visible")); let observedCount = 0; installAreaOnlyObserver(window, () => { observedCount++; }); positionIsland(window, 5000, 300); await act(async () => { await mountIslands(host(window), { loader }); }); expect(observedCount).toBe(1); expect(islandRootCount()).toBe(1); delete (globalThis as any).IntersectionObserver; });