import { expect, test } from "bun:test"; import { islandNamesFrom, islandPropValue, parseIslandStrategy, renderIslandMarker, serializeIslandProps, } from "../src/island-codegen.ts"; test("defaults to the client-only strategy", () => { expect(parseIslandStrategy([])).toBe("only"); expect(parseIslandStrategy(["client:visible"])).toBe("visible"); expect(parseIslandStrategy(["client:idle"])).toBe("idle"); expect(parseIslandStrategy(["client:load"])).toBe("load"); }); test("serializes JSON-safe props", () => { const result = serializeIslandProps("Chart", { title: "Revenue", points: [1, 2] }); expect(result).toEqual({ json: '{"title":"Revenue","points":[1,2]}' }); }); test("rejects non-serializable props with WRN-ISLAND-PROPS", () => { const result = serializeIslandProps("Chart", { onClick: () => {} }); expect(result).toHaveProperty("diagnostic"); const { diagnostic } = result as { diagnostic: { code: string; message: string } }; expect(diagnostic.code).toBe("WRN-ISLAND-PROPS"); expect(diagnostic.message).toContain("Chart"); expect(diagnostic.message).toContain("onClick"); }); test("rejects class instances and nested offenders", () => { class Point { constructor(public x = 1) {} } expect(serializeIslandProps("Chart", { origin: new Point() })).toHaveProperty("diagnostic"); expect(serializeIslandProps("Chart", { nested: { deep: () => {} } })).toHaveProperty( "diagnostic", ); expect(serializeIslandProps("Chart", { list: [1, () => {}] })).toHaveProperty("diagnostic"); }); test("accepts null and nested plain data", () => { const result = serializeIslandProps("Chart", { empty: null, nested: { rows: [{ id: 1 }], flag: false }, }); expect(result).toHaveProperty("json"); }); test("rejects island names that are unsafe as URL path segments", () => { // The name is fetched as /__wrnexus/island/.js, so traversal and // separators must be refused rather than merely escaped. expect(() => renderIslandMarker({ name: "../secret", strategy: "only", propsJson: "{}" }), ).toThrow(/not a safe identifier/); expect(() => renderIslandMarker({ name: "a/b", strategy: "only", propsJson: "{}" })).toThrow( /not a safe identifier/, ); expect(() => renderIslandMarker({ name: "Chart", strategy: "only", propsJson: "{}" }), ).not.toThrow(); }); test("renders a marker with escaped props", () => { const html = renderIslandMarker({ name: "Chart", strategy: "visible", propsJson: '{"title":"a { const names = islandNamesFrom([ { kind: "island", declaration: { defaultImport: "Chart" } }, { declaration: { defaultImport: "Card" } }, { kind: "island", declaration: {} }, ]); expect([...names]).toEqual(["Chart"]); }); test("island prop values follow JSX semantics, not raw attribute strings", () => { // Without this, start={3} arrives as the string "3" and arithmetic inside the // island concatenates: 3 -> "31" -> "311". expect(islandPropValue("{3}")).toEqual({ value: 3 }); expect(islandPropValue("{true}")).toEqual({ value: true }); expect(islandPropValue("{[1,2]}")).toEqual({ value: [1, 2] }); expect(islandPropValue('{"a"}')).toEqual({ value: "a" }); expect(islandPropValue("Revenue")).toEqual({ value: "Revenue" }); expect(islandPropValue(undefined)).toEqual({ value: true }); }); test("a runtime expression prop is reported as dynamic", () => { expect(islandPropValue("{someVariable}")).toEqual({ dynamic: "someVariable" }); expect(islandPropValue("{fn()}")).toEqual({ dynamic: "fn()" }); });