/* Copyright 2026 Marimo. All rights reserved. */ import { afterEach, describe, expect, it } from "vitest"; import { ISLAND_DATA_ATTRIBUTES } from "@/core/islands/constants"; import { extractIslandCodeFromEmbed, parseIslandElement, parseIslandElementsIntoApps, parseMarimoIslandApps, } from "../parse"; import { buildIslandHTML, createIslandHarness, type IslandHarness, } from "./test-utils.tsx"; let harness: IslandHarness; afterEach(() => { harness?.cleanup(); }); // ============================================================================ // Reactive vs Non-Reactive Parsing // ============================================================================ describe("reactive vs non-reactive islands", () => { it("should parse reactive islands into apps with code", () => { harness = createIslandHarness( buildIslandHTML([ { reactive: true, code: "x = 1", output: "
1
" }, { reactive: true, code: "y = 2", output: "
2
" }, ]), ); const apps = parseMarimoIslandApps(harness.container); expect(apps).toHaveLength(1); expect(apps[0].cells).toHaveLength(2); expect(apps[0].cells[0].code).toBe("x = 1"); expect(apps[0].cells[1].code).toBe("y = 2"); }); it("should skip non-reactive islands during parsing (no code sent to kernel)", () => { harness = createIslandHarness( buildIslandHTML([ { reactive: true, code: "x = 1", output: "
1
" }, { reactive: false, output: "
static content
" }, { reactive: true, code: "y = 2", output: "
2
" }, ]), ); const apps = parseMarimoIslandApps(harness.container); expect(apps).toHaveLength(1); // Only the 2 reactive islands become cells expect(apps[0].cells).toHaveLength(2); expect(apps[0].cells[0].code).toBe("x = 1"); expect(apps[0].cells[1].code).toBe("y = 2"); }); it("should not set data-cell-idx on non-reactive islands", () => { harness = createIslandHarness( buildIslandHTML([ { reactive: true, code: "x = 1", output: "
1
" }, { reactive: false, output: "
static
" }, ]), ); parseMarimoIslandApps(harness.container); const [reactiveIsland, nonReactiveIsland] = harness.islands; // Reactive island gets a cell index expect(reactiveIsland.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe( "0", ); // Non-reactive island does NOT get a cell index expect( nonReactiveIsland.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX), ).toBeNull(); }); it("should handle all-non-reactive islands (empty app list)", () => { harness = createIslandHarness( buildIslandHTML([ { reactive: false, output: "
static 1
" }, { reactive: false, output: "
static 2
" }, ]), ); const apps = parseMarimoIslandApps(harness.container); expect(apps).toHaveLength(0); }); }); // ============================================================================ // extractIslandCodeFromEmbed // ============================================================================ describe("extractIslandCodeFromEmbed with harness", () => { it("should return code for reactive islands", () => { harness = createIslandHarness( buildIslandHTML([ { reactive: true, code: 'mo.md("hello")', output: "
hello
" }, ]), ); const code = extractIslandCodeFromEmbed(harness.islands[0]); expect(code).toBe('mo.md("hello")'); }); it("should return empty string for non-reactive islands", () => { harness = createIslandHarness( buildIslandHTML([ { reactive: false, code: 'mo.md("hello")', output: "
hello
", }, ]), ); const code = extractIslandCodeFromEmbed(harness.islands[0]); expect(code).toBe(""); }); }); // ============================================================================ // parseIslandElement // ============================================================================ describe("parseIslandElement with harness", () => { it("should return cell data for reactive island with output and code", () => { harness = createIslandHarness( buildIslandHTML([ { reactive: true, code: "x = 1", output: "
1
" }, ]), ); const result = parseIslandElement(harness.islands[0]); expect(result).not.toBeNull(); expect(result!.code).toBe("x = 1"); expect(result!.output).toBe("
1
"); }); it("should return null for non-reactive island (code is empty)", () => { harness = createIslandHarness( buildIslandHTML([{ reactive: false, output: "
static
" }]), ); const result = parseIslandElement(harness.islands[0]); expect(result).toBeNull(); }); }); // ============================================================================ // Multi-app parsing // ============================================================================ describe("multi-app parsing with harness", () => { it("should group islands by app-id", () => { harness = createIslandHarness( buildIslandHTML([ { appId: "app-1", reactive: true, code: "a = 1", output: "
" }, { appId: "app-2", reactive: true, code: "b = 2", output: "
" }, { appId: "app-1", reactive: true, code: "c = 3", output: "
" }, ]), ); const apps = parseMarimoIslandApps(harness.container); expect(apps).toHaveLength(2); const app1 = apps.find((a) => a.id === "app-1")!; const app2 = apps.find((a) => a.id === "app-2")!; expect(app1.cells).toHaveLength(2); expect(app1.cells[0].code).toBe("a = 1"); expect(app1.cells[1].code).toBe("c = 3"); expect(app2.cells).toHaveLength(1); expect(app2.cells[0].code).toBe("b = 2"); }); it("should assign sequential cell indices within each app", () => { harness = createIslandHarness( buildIslandHTML([ { appId: "app-1", reactive: true, code: "a", output: "
" }, { appId: "app-1", reactive: true, code: "b", output: "
" }, { appId: "app-1", reactive: true, code: "c", output: "
" }, ]), ); const apps = parseMarimoIslandApps(harness.container); expect(apps[0].cells.map((c) => c.idx)).toEqual([0, 1, 2]); }); it("should skip non-reactive islands in cell index assignment", () => { harness = createIslandHarness( buildIslandHTML([ { reactive: true, code: "a = 1", output: "
" }, { reactive: false, output: "
static
" }, { reactive: true, code: "b = 2", output: "
" }, ]), ); const apps = parseMarimoIslandApps(harness.container); expect(apps[0].cells).toHaveLength(2); expect(apps[0].cells[0].idx).toBe(0); expect(apps[0].cells[1].idx).toBe(1); // Verify DOM: reactive islands get indices, non-reactive does not expect( harness.islands[0].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX), ).toBe("0"); expect( harness.islands[1].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX), ).toBeNull(); expect( harness.islands[2].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX), ).toBe("1"); }); }); // ============================================================================ // Mixed reactive/non-reactive scenarios (regression tests) // ============================================================================ describe("mixed reactive/non-reactive island scenarios", () => { it("should handle the generate.py demo pattern: reactive + non-reactive + display_code", () => { // Mirrors the "Island Features" section of generate.py harness = createIslandHarness( buildIslandHTML([ // Section header (reactive) { reactive: true, code: 'mo.md("## Display Code")', output: "

Display Code

", }, // display_code island (reactive) { reactive: true, code: 'mo.md("You can show the code")', output: "
You can show the code
", displayCode: true, }, // Non-reactive section header { reactive: true, code: 'mo.md("## Non-Reactive Islands")', output: "

Non-Reactive Islands

", }, // Non-reactive island — the one that was crashing { reactive: false, code: 'mo.md("This island is non-reactive")', output: "
This island is non-reactive - it runs once and doesn't update
", }, ]), ); const apps = parseMarimoIslandApps(harness.container); // Only 3 reactive islands become cells expect(apps).toHaveLength(1); expect(apps[0].cells).toHaveLength(3); // Non-reactive island (index 3) has no cell-idx expect( harness.islands[3].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX), ).toBeNull(); expect( harness.islands[3].getAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE), ).toBe("false"); }); it("should handle non-reactive island at the start", () => { harness = createIslandHarness( buildIslandHTML([ { reactive: false, output: "
static header
" }, { reactive: true, code: "x = 1", output: "
1
" }, ]), ); const apps = parseMarimoIslandApps(harness.container); expect(apps).toHaveLength(1); expect(apps[0].cells).toHaveLength(1); expect(apps[0].cells[0].code).toBe("x = 1"); expect(apps[0].cells[0].idx).toBe(0); // First island (non-reactive) has no index expect( harness.islands[0].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX), ).toBeNull(); // Second island (reactive) gets index 0 expect( harness.islands[1].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX), ).toBe("0"); }); it("should handle alternating reactive and non-reactive islands", () => { harness = createIslandHarness( buildIslandHTML([ { reactive: true, code: "a = 1", output: "
" }, { reactive: false, output: "
static
" }, { reactive: true, code: "b = 2", output: "
" }, { reactive: false, output: "
static
" }, { reactive: true, code: "c = 3", output: "
" }, ]), ); const apps = parseMarimoIslandApps(harness.container); expect(apps[0].cells).toHaveLength(3); expect(apps[0].cells.map((c) => c.code)).toEqual([ "a = 1", "b = 2", "c = 3", ]); expect(apps[0].cells.map((c) => c.idx)).toEqual([0, 1, 2]); }); }); // ============================================================================ // parseIslandElementsIntoApps (direct element-level tests) // ============================================================================ describe("parseIslandElementsIntoApps with mixed elements", () => { it("should preserve DOM order for cell indices", () => { harness = createIslandHarness( buildIslandHTML([ { reactive: true, code: "first", output: "
" }, { reactive: true, code: "second", output: "
" }, { reactive: true, code: "third", output: "
" }, ]), ); const apps = parseIslandElementsIntoApps(harness.islands); expect(apps[0].cells.map((c) => c.code)).toEqual([ "first", "second", "third", ]); }); it("should handle empty container", () => { harness = createIslandHarness(""); const apps = parseMarimoIslandApps(harness.container); expect(apps).toHaveLength(0); }); });