@marimo-team/frontend 0.23.1-dev20 → 0.23.1-dev22
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.
- package/dist/assets/{index-C9DyCFTe.js → index-C7hH7rgL.js} +14 -14
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/core/islands/__tests__/bridge.test.ts +2 -12
- package/src/core/islands/__tests__/islands-harness.test.ts +348 -0
- package/src/core/islands/__tests__/parse.test.ts +466 -24
- package/src/core/islands/__tests__/test-utils.tsx +263 -0
- package/src/core/islands/bootstrap.ts +265 -0
- package/src/core/islands/bridge.ts +154 -75
- package/src/core/islands/components/IslandControls.tsx +103 -0
- package/src/core/islands/components/__tests__/IslandControls.test.tsx +185 -0
- package/src/core/islands/components/__tests__/useIslandControls.test.ts +208 -0
- package/src/core/islands/components/output-wrapper.tsx +76 -93
- package/src/core/islands/components/useIslandControls.ts +60 -0
- package/src/core/islands/components/web-components.tsx +168 -40
- package/src/core/islands/constants.ts +28 -0
- package/src/core/islands/main.ts +7 -205
- package/src/core/islands/parse.ts +73 -26
- package/src/core/islands/worker-factory.ts +86 -0
- package/src/plugins/impl/DataTablePlugin.tsx +7 -3
package/dist/index.html
CHANGED
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
<marimo-server-token data-token="{{ server_token }}" hidden></marimo-server-token>
|
|
67
67
|
<!-- /TODO -->
|
|
68
68
|
<title>{{ title }}</title>
|
|
69
|
-
<script type="module" crossorigin src="./assets/index-
|
|
69
|
+
<script type="module" crossorigin src="./assets/index-C7hH7rgL.js"></script>
|
|
70
70
|
<link rel="modulepreload" crossorigin href="./assets/preload-helper-D2MJg03u.js">
|
|
71
71
|
<link rel="modulepreload" crossorigin href="./assets/chunk-LvLJmgfZ.js">
|
|
72
72
|
<link rel="modulepreload" crossorigin href="./assets/react-Bj1aDYRI.js">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marimo-team/frontend",
|
|
3
|
-
"version": "0.23.1-
|
|
3
|
+
"version": "0.23.1-dev22",
|
|
4
4
|
"main": "dist/main.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -123,6 +123,7 @@
|
|
|
123
123
|
"path-to-regexp": "^8.4.0",
|
|
124
124
|
"plotly.js": "^3.3.1",
|
|
125
125
|
"pyodide": "0.27.7",
|
|
126
|
+
"radix-ui": "1.4.3",
|
|
126
127
|
"react-arborist": "^3.4.3",
|
|
127
128
|
"react-aria": "3.47.0",
|
|
128
129
|
"react-aria-components": "1.16.0",
|
|
@@ -159,8 +160,7 @@
|
|
|
159
160
|
"vscode-jsonrpc": "^8.2.1",
|
|
160
161
|
"vscode-languageserver-protocol": "^3.17.5",
|
|
161
162
|
"web-vitals": "^4.2.4",
|
|
162
|
-
"zod": "^4.3.6"
|
|
163
|
-
"radix-ui": "1.4.3"
|
|
163
|
+
"zod": "^4.3.6"
|
|
164
164
|
},
|
|
165
165
|
"scripts": {
|
|
166
166
|
"preinstall": "npx only-allow pnpm",
|
|
@@ -217,7 +217,7 @@
|
|
|
217
217
|
"oxfmt": "^0.42.0",
|
|
218
218
|
"oxlint": "^1.58.0",
|
|
219
219
|
"postcss": "^8.5.6",
|
|
220
|
-
"postcss-
|
|
220
|
+
"postcss-prefix-selector": "^2.1.1",
|
|
221
221
|
"react": "^19.2.4",
|
|
222
222
|
"react-compiler-runtime": "19.1.0-rc.3",
|
|
223
223
|
"react-dom": "^19.2.4",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import type { components } from "@marimo-team/marimo-api";
|
|
4
|
-
import {
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import {
|
|
6
6
|
cellId,
|
|
7
7
|
requestId,
|
|
@@ -83,17 +83,7 @@ describe("IslandsPyodideBridge", () => {
|
|
|
83
83
|
|
|
84
84
|
beforeEach(() => {
|
|
85
85
|
vi.clearAllMocks();
|
|
86
|
-
|
|
87
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
88
|
-
delete (window as any)._marimo_private_IslandsPyodideBridge;
|
|
89
|
-
// Access the singleton - creates a fresh instance
|
|
90
|
-
bridge = IslandsPyodideBridge.INSTANCE;
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
afterEach(() => {
|
|
94
|
-
// Clean up singleton
|
|
95
|
-
// oxlint-disable-next-line typescript/no-explicit-any
|
|
96
|
-
delete (window as any)._marimo_private_IslandsPyodideBridge;
|
|
86
|
+
bridge = new IslandsPyodideBridge({ autoStartSessions: false });
|
|
97
87
|
});
|
|
98
88
|
|
|
99
89
|
describe("sendComponentValues", () => {
|
|
@@ -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
|
+
});
|