@marimo-team/frontend 0.23.1-dev9 → 0.23.2-dev1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/assets/{JsonOutput-BY31ccA7.js → JsonOutput-CavtrueA.js} +1 -1
  2. package/dist/assets/{MarimoErrorOutput--Yd2Aw0J.js → MarimoErrorOutput-Bmp8DLLo.js} +1 -1
  3. package/dist/assets/RenderHTML-CM3WMmA8.js +1 -0
  4. package/dist/assets/{add-connection-dialog-CjvNOKgb.js → add-connection-dialog-BGZvJkor.js} +1 -1
  5. package/dist/assets/{agent-panel-C24uwabG.js → agent-panel-BvL9Lu9c.js} +1 -1
  6. package/dist/assets/{cell-editor-zW0u82sK.js → cell-editor-B40o_zx_.js} +1 -1
  7. package/dist/assets/{chat-display-DsHMZa9F.js → chat-display-M_nvYuHH.js} +1 -1
  8. package/dist/assets/{chat-panel-o9D3upnX.js → chat-panel-BMOW93uQ.js} +1 -1
  9. package/dist/assets/{chat-ui-BYS03y86.js → chat-ui-DyeimpVh.js} +1 -1
  10. package/dist/assets/{column-preview-Dwv5a_zE.js → column-preview-AfcgbFG_.js} +1 -1
  11. package/dist/assets/{command-palette-BYbKGSF3.js → command-palette-BgvdyU3B.js} +1 -1
  12. package/dist/assets/{documentation-panel-CA2pWMgB.js → documentation-panel-DUPcsi8P.js} +1 -1
  13. package/dist/assets/{edit-page-CMUN3ESy.js → edit-page-DD4uEDmX.js} +4 -4
  14. package/dist/assets/{error-panel-CbqfK1HJ.js → error-panel-DQOeSv5-.js} +1 -1
  15. package/dist/assets/{file-explorer-panel-CbS8z-JR.js → file-explorer-panel-B67zjs2X.js} +1 -1
  16. package/dist/assets/{form-DLyXacSF.js → form-BJ6VFU8l.js} +1 -1
  17. package/dist/assets/{hooks-kZJc1iBf.js → hooks-DvwShzDb.js} +1 -1
  18. package/dist/assets/index-ThWddW3f.js +42 -0
  19. package/dist/assets/{layout-tmN-U1zs.js → layout-erv8pLIP.js} +1 -1
  20. package/dist/assets/{panels-CLfdzLPR.js → panels-1u-RE72f.js} +1 -1
  21. package/dist/assets/{run-page-DPuH6QY4.js → run-page-DfWH_1mz.js} +1 -1
  22. package/dist/assets/{scratchpad-panel-BsMm0GQP.js → scratchpad-panel-CnaiXtoJ.js} +1 -1
  23. package/dist/assets/{session-panel-CTDzGShO.js → session-panel-C68GBFwH.js} +1 -1
  24. package/dist/assets/{snippets-panel-CWof0wHk.js → snippets-panel-BmIdR0lc.js} +1 -1
  25. package/dist/assets/state-D1n-olwf.js +3 -0
  26. package/dist/assets/{useNotebookActions-DHBEqrc_.js → useNotebookActions-Ch1o32Jw.js} +1 -1
  27. package/dist/index.html +7 -7
  28. package/package.json +4 -4
  29. package/src/core/islands/__tests__/bridge.test.ts +2 -12
  30. package/src/core/islands/__tests__/islands-harness.test.ts +348 -0
  31. package/src/core/islands/__tests__/parse.test.ts +466 -24
  32. package/src/core/islands/__tests__/test-utils.tsx +263 -0
  33. package/src/core/islands/bootstrap.ts +265 -0
  34. package/src/core/islands/bridge.ts +154 -75
  35. package/src/core/islands/components/IslandControls.tsx +103 -0
  36. package/src/core/islands/components/__tests__/IslandControls.test.tsx +185 -0
  37. package/src/core/islands/components/__tests__/useIslandControls.test.ts +208 -0
  38. package/src/core/islands/components/output-wrapper.tsx +76 -93
  39. package/src/core/islands/components/useIslandControls.ts +60 -0
  40. package/src/core/islands/components/web-components.tsx +168 -40
  41. package/src/core/islands/constants.ts +28 -0
  42. package/src/core/islands/main.ts +7 -205
  43. package/src/core/islands/parse.ts +73 -26
  44. package/src/core/islands/worker-factory.ts +86 -0
  45. package/src/plugins/core/RenderHTML.tsx +9 -0
  46. package/src/plugins/core/__test__/RenderHTML.test.ts +27 -0
  47. package/src/plugins/core/__test__/trusted-url.test.ts +48 -0
  48. package/src/plugins/core/registerReactComponent.tsx +11 -8
  49. package/src/plugins/core/trusted-url.ts +20 -0
  50. package/src/plugins/impl/ButtonPlugin.tsx +4 -6
  51. package/src/plugins/impl/CodeEditorPlugin.tsx +15 -18
  52. package/src/plugins/impl/DataEditorPlugin.tsx +8 -14
  53. package/src/plugins/impl/DataTablePlugin.tsx +8 -9
  54. package/src/plugins/impl/FileUploadPlugin.tsx +39 -43
  55. package/src/plugins/impl/FormPlugin.tsx +2 -6
  56. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +27 -1
  57. package/src/plugins/impl/anywidget/widget-binding.ts +13 -0
  58. package/src/plugins/impl/chat/ChatPlugin.tsx +17 -20
  59. package/src/plugins/impl/data-explorer/DataExplorerPlugin.tsx +5 -8
  60. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +21 -0
  61. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +119 -0
  62. package/src/plugins/impl/panel/PanelPlugin.tsx +31 -10
  63. package/src/plugins/impl/panel/__tests__/PanelPlugin.test.ts +60 -0
  64. package/src/plugins/impl/plotly/__tests__/PlotlyPlugin.test.tsx +50 -0
  65. package/src/plugins/impl/plotly/__tests__/selection.test.ts +82 -0
  66. package/src/plugins/impl/plotly/selection.ts +62 -3
  67. package/src/plugins/impl/vega/VegaPlugin.tsx +5 -8
  68. package/src/plugins/layout/NavigationMenuPlugin.tsx +2 -6
  69. package/dist/assets/RenderHTML-CbuarQqA.js +0 -1
  70. package/dist/assets/index-Bm25ctN7.js +0 -42
  71. package/dist/assets/state-BvnlMKdT.js +0 -3
@@ -0,0 +1,348 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { afterEach, describe, expect, it } from "vitest";
3
+ import { ISLAND_DATA_ATTRIBUTES } from "@/core/islands/constants";
4
+ import {
5
+ extractIslandCodeFromEmbed,
6
+ parseIslandElement,
7
+ parseIslandElementsIntoApps,
8
+ parseMarimoIslandApps,
9
+ } from "../parse";
10
+ import {
11
+ buildIslandHTML,
12
+ createIslandHarness,
13
+ type IslandHarness,
14
+ } from "./test-utils.tsx";
15
+
16
+ let harness: IslandHarness;
17
+
18
+ afterEach(() => {
19
+ harness?.cleanup();
20
+ });
21
+
22
+ // ============================================================================
23
+ // Reactive vs Non-Reactive Parsing
24
+ // ============================================================================
25
+
26
+ describe("reactive vs non-reactive islands", () => {
27
+ it("should parse reactive islands into apps with code", () => {
28
+ harness = createIslandHarness(
29
+ buildIslandHTML([
30
+ { reactive: true, code: "x = 1", output: "<div>1</div>" },
31
+ { reactive: true, code: "y = 2", output: "<div>2</div>" },
32
+ ]),
33
+ );
34
+
35
+ const apps = parseMarimoIslandApps(harness.container);
36
+ expect(apps).toHaveLength(1);
37
+ expect(apps[0].cells).toHaveLength(2);
38
+ expect(apps[0].cells[0].code).toBe("x = 1");
39
+ expect(apps[0].cells[1].code).toBe("y = 2");
40
+ });
41
+
42
+ it("should skip non-reactive islands during parsing (no code sent to kernel)", () => {
43
+ harness = createIslandHarness(
44
+ buildIslandHTML([
45
+ { reactive: true, code: "x = 1", output: "<div>1</div>" },
46
+ { reactive: false, output: "<div>static content</div>" },
47
+ { reactive: true, code: "y = 2", output: "<div>2</div>" },
48
+ ]),
49
+ );
50
+
51
+ const apps = parseMarimoIslandApps(harness.container);
52
+ expect(apps).toHaveLength(1);
53
+ // Only the 2 reactive islands become cells
54
+ expect(apps[0].cells).toHaveLength(2);
55
+ expect(apps[0].cells[0].code).toBe("x = 1");
56
+ expect(apps[0].cells[1].code).toBe("y = 2");
57
+ });
58
+
59
+ it("should not set data-cell-idx on non-reactive islands", () => {
60
+ harness = createIslandHarness(
61
+ buildIslandHTML([
62
+ { reactive: true, code: "x = 1", output: "<div>1</div>" },
63
+ { reactive: false, output: "<div>static</div>" },
64
+ ]),
65
+ );
66
+
67
+ parseMarimoIslandApps(harness.container);
68
+
69
+ const [reactiveIsland, nonReactiveIsland] = harness.islands;
70
+
71
+ // Reactive island gets a cell index
72
+ expect(reactiveIsland.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe(
73
+ "0",
74
+ );
75
+ // Non-reactive island does NOT get a cell index
76
+ expect(
77
+ nonReactiveIsland.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX),
78
+ ).toBeNull();
79
+ });
80
+
81
+ it("should handle all-non-reactive islands (empty app list)", () => {
82
+ harness = createIslandHarness(
83
+ buildIslandHTML([
84
+ { reactive: false, output: "<div>static 1</div>" },
85
+ { reactive: false, output: "<div>static 2</div>" },
86
+ ]),
87
+ );
88
+
89
+ const apps = parseMarimoIslandApps(harness.container);
90
+ expect(apps).toHaveLength(0);
91
+ });
92
+ });
93
+
94
+ // ============================================================================
95
+ // extractIslandCodeFromEmbed
96
+ // ============================================================================
97
+
98
+ describe("extractIslandCodeFromEmbed with harness", () => {
99
+ it("should return code for reactive islands", () => {
100
+ harness = createIslandHarness(
101
+ buildIslandHTML([
102
+ { reactive: true, code: 'mo.md("hello")', output: "<div>hello</div>" },
103
+ ]),
104
+ );
105
+
106
+ const code = extractIslandCodeFromEmbed(harness.islands[0]);
107
+ expect(code).toBe('mo.md("hello")');
108
+ });
109
+
110
+ it("should return empty string for non-reactive islands", () => {
111
+ harness = createIslandHarness(
112
+ buildIslandHTML([
113
+ {
114
+ reactive: false,
115
+ code: 'mo.md("hello")',
116
+ output: "<div>hello</div>",
117
+ },
118
+ ]),
119
+ );
120
+
121
+ const code = extractIslandCodeFromEmbed(harness.islands[0]);
122
+ expect(code).toBe("");
123
+ });
124
+ });
125
+
126
+ // ============================================================================
127
+ // parseIslandElement
128
+ // ============================================================================
129
+
130
+ describe("parseIslandElement with harness", () => {
131
+ it("should return cell data for reactive island with output and code", () => {
132
+ harness = createIslandHarness(
133
+ buildIslandHTML([
134
+ { reactive: true, code: "x = 1", output: "<div>1</div>" },
135
+ ]),
136
+ );
137
+
138
+ const result = parseIslandElement(harness.islands[0]);
139
+ expect(result).not.toBeNull();
140
+ expect(result!.code).toBe("x = 1");
141
+ expect(result!.output).toBe("<div>1</div>");
142
+ });
143
+
144
+ it("should return null for non-reactive island (code is empty)", () => {
145
+ harness = createIslandHarness(
146
+ buildIslandHTML([{ reactive: false, output: "<div>static</div>" }]),
147
+ );
148
+
149
+ const result = parseIslandElement(harness.islands[0]);
150
+ expect(result).toBeNull();
151
+ });
152
+ });
153
+
154
+ // ============================================================================
155
+ // Multi-app parsing
156
+ // ============================================================================
157
+
158
+ describe("multi-app parsing with harness", () => {
159
+ it("should group islands by app-id", () => {
160
+ harness = createIslandHarness(
161
+ buildIslandHTML([
162
+ { appId: "app-1", reactive: true, code: "a = 1", output: "<div/>" },
163
+ { appId: "app-2", reactive: true, code: "b = 2", output: "<div/>" },
164
+ { appId: "app-1", reactive: true, code: "c = 3", output: "<div/>" },
165
+ ]),
166
+ );
167
+
168
+ const apps = parseMarimoIslandApps(harness.container);
169
+ expect(apps).toHaveLength(2);
170
+
171
+ const app1 = apps.find((a) => a.id === "app-1")!;
172
+ const app2 = apps.find((a) => a.id === "app-2")!;
173
+
174
+ expect(app1.cells).toHaveLength(2);
175
+ expect(app1.cells[0].code).toBe("a = 1");
176
+ expect(app1.cells[1].code).toBe("c = 3");
177
+
178
+ expect(app2.cells).toHaveLength(1);
179
+ expect(app2.cells[0].code).toBe("b = 2");
180
+ });
181
+
182
+ it("should assign sequential cell indices within each app", () => {
183
+ harness = createIslandHarness(
184
+ buildIslandHTML([
185
+ { appId: "app-1", reactive: true, code: "a", output: "<div/>" },
186
+ { appId: "app-1", reactive: true, code: "b", output: "<div/>" },
187
+ { appId: "app-1", reactive: true, code: "c", output: "<div/>" },
188
+ ]),
189
+ );
190
+
191
+ const apps = parseMarimoIslandApps(harness.container);
192
+ expect(apps[0].cells.map((c) => c.idx)).toEqual([0, 1, 2]);
193
+ });
194
+
195
+ it("should skip non-reactive islands in cell index assignment", () => {
196
+ harness = createIslandHarness(
197
+ buildIslandHTML([
198
+ { reactive: true, code: "a = 1", output: "<div/>" },
199
+ { reactive: false, output: "<div>static</div>" },
200
+ { reactive: true, code: "b = 2", output: "<div/>" },
201
+ ]),
202
+ );
203
+
204
+ const apps = parseMarimoIslandApps(harness.container);
205
+ expect(apps[0].cells).toHaveLength(2);
206
+ expect(apps[0].cells[0].idx).toBe(0);
207
+ expect(apps[0].cells[1].idx).toBe(1);
208
+
209
+ // Verify DOM: reactive islands get indices, non-reactive does not
210
+ expect(
211
+ harness.islands[0].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX),
212
+ ).toBe("0");
213
+ expect(
214
+ harness.islands[1].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX),
215
+ ).toBeNull();
216
+ expect(
217
+ harness.islands[2].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX),
218
+ ).toBe("1");
219
+ });
220
+ });
221
+
222
+ // ============================================================================
223
+ // Mixed reactive/non-reactive scenarios (regression tests)
224
+ // ============================================================================
225
+
226
+ describe("mixed reactive/non-reactive island scenarios", () => {
227
+ it("should handle the generate.py demo pattern: reactive + non-reactive + display_code", () => {
228
+ // Mirrors the "Island Features" section of generate.py
229
+ harness = createIslandHarness(
230
+ buildIslandHTML([
231
+ // Section header (reactive)
232
+ {
233
+ reactive: true,
234
+ code: 'mo.md("## Display Code")',
235
+ output: "<div><h2>Display Code</h2></div>",
236
+ },
237
+ // display_code island (reactive)
238
+ {
239
+ reactive: true,
240
+ code: 'mo.md("You can show the code")',
241
+ output: "<div>You can show the code</div>",
242
+ displayCode: true,
243
+ },
244
+ // Non-reactive section header
245
+ {
246
+ reactive: true,
247
+ code: 'mo.md("## Non-Reactive Islands")',
248
+ output: "<div><h2>Non-Reactive Islands</h2></div>",
249
+ },
250
+ // Non-reactive island — the one that was crashing
251
+ {
252
+ reactive: false,
253
+ code: 'mo.md("This island is non-reactive")',
254
+ output:
255
+ "<div>This island is non-reactive - it runs once and doesn't update</div>",
256
+ },
257
+ ]),
258
+ );
259
+
260
+ const apps = parseMarimoIslandApps(harness.container);
261
+
262
+ // Only 3 reactive islands become cells
263
+ expect(apps).toHaveLength(1);
264
+ expect(apps[0].cells).toHaveLength(3);
265
+
266
+ // Non-reactive island (index 3) has no cell-idx
267
+ expect(
268
+ harness.islands[3].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX),
269
+ ).toBeNull();
270
+ expect(
271
+ harness.islands[3].getAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE),
272
+ ).toBe("false");
273
+ });
274
+
275
+ it("should handle non-reactive island at the start", () => {
276
+ harness = createIslandHarness(
277
+ buildIslandHTML([
278
+ { reactive: false, output: "<div>static header</div>" },
279
+ { reactive: true, code: "x = 1", output: "<div>1</div>" },
280
+ ]),
281
+ );
282
+
283
+ const apps = parseMarimoIslandApps(harness.container);
284
+ expect(apps).toHaveLength(1);
285
+ expect(apps[0].cells).toHaveLength(1);
286
+ expect(apps[0].cells[0].code).toBe("x = 1");
287
+ expect(apps[0].cells[0].idx).toBe(0);
288
+
289
+ // First island (non-reactive) has no index
290
+ expect(
291
+ harness.islands[0].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX),
292
+ ).toBeNull();
293
+ // Second island (reactive) gets index 0
294
+ expect(
295
+ harness.islands[1].getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX),
296
+ ).toBe("0");
297
+ });
298
+
299
+ it("should handle alternating reactive and non-reactive islands", () => {
300
+ harness = createIslandHarness(
301
+ buildIslandHTML([
302
+ { reactive: true, code: "a = 1", output: "<div/>" },
303
+ { reactive: false, output: "<div>static</div>" },
304
+ { reactive: true, code: "b = 2", output: "<div/>" },
305
+ { reactive: false, output: "<div>static</div>" },
306
+ { reactive: true, code: "c = 3", output: "<div/>" },
307
+ ]),
308
+ );
309
+
310
+ const apps = parseMarimoIslandApps(harness.container);
311
+ expect(apps[0].cells).toHaveLength(3);
312
+ expect(apps[0].cells.map((c) => c.code)).toEqual([
313
+ "a = 1",
314
+ "b = 2",
315
+ "c = 3",
316
+ ]);
317
+ expect(apps[0].cells.map((c) => c.idx)).toEqual([0, 1, 2]);
318
+ });
319
+ });
320
+
321
+ // ============================================================================
322
+ // parseIslandElementsIntoApps (direct element-level tests)
323
+ // ============================================================================
324
+
325
+ describe("parseIslandElementsIntoApps with mixed elements", () => {
326
+ it("should preserve DOM order for cell indices", () => {
327
+ harness = createIslandHarness(
328
+ buildIslandHTML([
329
+ { reactive: true, code: "first", output: "<div/>" },
330
+ { reactive: true, code: "second", output: "<div/>" },
331
+ { reactive: true, code: "third", output: "<div/>" },
332
+ ]),
333
+ );
334
+
335
+ const apps = parseIslandElementsIntoApps(harness.islands);
336
+ expect(apps[0].cells.map((c) => c.code)).toEqual([
337
+ "first",
338
+ "second",
339
+ "third",
340
+ ]);
341
+ });
342
+
343
+ it("should handle empty container", () => {
344
+ harness = createIslandHarness("");
345
+ const apps = parseMarimoIslandApps(harness.container);
346
+ expect(apps).toHaveLength(0);
347
+ });
348
+ });