@marimo-team/islands 0.23.15-dev4 → 0.23.15-dev41
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/{ConnectedDataExplorerComponent-CU4fZJzG.js → ConnectedDataExplorerComponent-7p7rt9iw.js} +4 -4
- package/dist/{ErrorBoundary-DE6tzZf-.js → ErrorBoundary-B_CAG7_e.js} +1 -1
- package/dist/{ImageComparisonComponent-CHrI72em.js → ImageComparisonComponent-1i5IBhon.js} +173 -129
- package/dist/assets/__vite-browser-external-CjNAy01L.js +1 -0
- package/dist/assets/worker-B3XPCb6y.js +98 -0
- package/dist/{chat-ui-DinOvbWu.js → chat-ui-C0z13gJB.js} +3111 -3111
- package/dist/{code-visibility-Dk8cVOny.js → code-visibility-COokQS5X.js} +946 -864
- package/dist/{constants-T20xxyNf.js → debounce-BOD3DbfP.js} +1 -24
- package/dist/{formats-Dzx4J_z1.js → formats-CitsMtUm.js} +1 -1
- package/dist/{glide-data-editor-CjTu7ukN.js → glide-data-editor-CwZz71BD.js} +3 -3
- package/dist/{html-to-image-_wGfk8V-.js → html-to-image-CEo5pRYw.js} +2291 -2250
- package/dist/{input-DtsN7xm-.js → input-BGPrFH3g.js} +2 -2
- package/dist/main.js +1297 -1201
- package/dist/{mermaid-BYqXy_NE.js → mermaid-UdmxG2PZ.js} +2 -2
- package/dist/{process-output-Mh4UrjwM.js → process-output-BOvvilRG.js} +1 -1
- package/dist/{reveal-component-B5DXLyO7.js → reveal-component-D3ySPwH5.js} +618 -605
- package/dist/{spec-Cz-Bj1JI.js → spec-DSs9v0xx.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-CWNNlFEX.js → toDate-BRJgtOGm.js} +14 -5
- package/dist/{useAsyncData-KfHB8wQR.js → useAsyncData-BSOyAbac.js} +1 -1
- package/dist/{useDeepCompareMemoize-nJMtxhm4.js → useDeepCompareMemoize-BTLeWzuR.js} +1 -1
- package/dist/{useLifecycle-DegSo0lV.js → useLifecycle-C6wHjkhW.js} +1 -1
- package/dist/{useTheme-6eZ3GOTS.js → useTheme-Df_vGflw.js} +62 -27
- package/dist/{vega-component-DzyyM9fc.js → vega-component-BAvmvTjO.js} +6 -6
- package/package.json +1 -1
- package/src/components/chat/__tests__/chat-utils.test.ts +244 -1
- package/src/components/chat/__tests__/message-queue.test.tsx +121 -0
- package/src/components/chat/chat-panel.tsx +196 -67
- package/src/components/chat/chat-utils.ts +111 -2
- package/src/components/editor/SortableCell.tsx +6 -2
- package/src/components/editor/__tests__/output-persistence.test.tsx +241 -0
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +153 -0
- package/src/components/editor/actions/pair-with-agent-commands.ts +109 -0
- package/src/components/editor/actions/pair-with-agent-modal.tsx +20 -64
- package/src/components/editor/ai/add-cell-with-ai.tsx +14 -5
- package/src/components/editor/ai/ai-completion-editor.tsx +4 -1
- package/src/components/editor/cell/cell-context-menu.tsx +7 -0
- package/src/components/editor/chrome/panels/packages-panel.tsx +11 -1
- package/src/components/editor/columns/__tests__/cell-column.test.tsx +105 -0
- package/src/components/editor/columns/cell-column.tsx +34 -4
- package/src/components/editor/columns/sortable-column.tsx +24 -4
- package/src/components/editor/file-tree/download.ts +46 -0
- package/src/components/editor/file-tree/file-explorer.tsx +3 -21
- package/src/components/editor/file-tree/file-viewer.tsx +4 -27
- package/src/components/editor/navigation/__tests__/navigation.test.ts +33 -0
- package/src/components/editor/navigation/navigation.ts +8 -1
- package/src/components/editor/notebook-cell.tsx +203 -106
- package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +66 -0
- package/src/components/editor/renderers/cell-array.tsx +26 -13
- package/src/components/editor/renderers/cells-renderer.tsx +8 -2
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +29 -0
- package/src/components/editor/renderers/slides-layout/types.ts +4 -0
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
- package/src/components/slides/reveal-component.tsx +37 -4
- package/src/components/slides/slide-form.tsx +72 -0
- package/src/components/ui/toast.tsx +11 -3
- package/src/components/ui/toaster.tsx +65 -9
- package/src/core/cells/__tests__/utils.test.ts +59 -0
- package/src/core/cells/utils.ts +51 -0
- package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +97 -3
- package/src/core/codemirror/completion/signature-hint.ts +78 -11
- package/src/core/constants.ts +6 -0
- package/src/core/islands/__tests__/bridge.test.ts +341 -46
- package/src/core/islands/__tests__/main.test.ts +176 -0
- package/src/core/islands/__tests__/parse.test.ts +105 -0
- package/src/core/islands/bootstrap.ts +8 -3
- package/src/core/islands/bridge.ts +116 -23
- package/src/core/islands/components/__tests__/web-components.test.tsx +214 -0
- package/src/core/islands/components/web-components.tsx +76 -15
- package/src/core/islands/constants.ts +1 -0
- package/src/core/islands/main.ts +69 -3
- package/src/core/islands/parse.ts +70 -23
- package/src/core/islands/worker/__tests__/controller.test.ts +173 -0
- package/src/core/islands/worker/worker.tsx +145 -57
- package/src/core/runtime/__tests__/runtime.test.ts +64 -0
- package/src/core/runtime/runtime.ts +30 -10
- package/src/core/wasm/worker/bootstrap.ts +113 -20
- package/src/css/app/Cell.css +10 -0
- package/src/hooks/__tests__/useHotkey.test.tsx +88 -0
- package/src/hooks/useHotkey.ts +29 -4
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +6 -9
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +22 -61
- package/src/plugins/impl/anywidget/model-proxy.ts +0 -13
- package/src/plugins/impl/anywidget/widget-binding.ts +4 -34
- package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +53 -2
- package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +71 -0
- package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +71 -3
- package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +1 -0
- package/src/theme/__tests__/useTheme.test.ts +68 -0
- package/src/theme/useTheme.ts +16 -1
- package/dist/assets/__vite-browser-external-BQCLNwri.js +0 -1
- package/dist/assets/worker-DAWRHcPq.js +0 -73
|
@@ -45,14 +45,22 @@ vi.stubGlobal("URL", MockURL);
|
|
|
45
45
|
const {
|
|
46
46
|
mockBridge,
|
|
47
47
|
mockLoadPackages,
|
|
48
|
+
mockReplaceSessionRequest,
|
|
49
|
+
mockStopSessionRequest,
|
|
48
50
|
mockStartSessionRequest,
|
|
51
|
+
mockMessageListeners,
|
|
52
|
+
mockStoreSet,
|
|
49
53
|
mockParseMarimoIslandApps,
|
|
50
54
|
mockCreateMarimoFile,
|
|
51
55
|
mockGetMarimoExportContext,
|
|
52
56
|
} = vi.hoisted(() => ({
|
|
53
57
|
mockBridge: vi.fn(),
|
|
54
58
|
mockLoadPackages: vi.fn(),
|
|
59
|
+
mockReplaceSessionRequest: vi.fn(),
|
|
60
|
+
mockStopSessionRequest: vi.fn(),
|
|
55
61
|
mockStartSessionRequest: vi.fn(),
|
|
62
|
+
mockMessageListeners: new Map<string, (payload: never) => void>(),
|
|
63
|
+
mockStoreSet: vi.fn(),
|
|
56
64
|
mockParseMarimoIslandApps: vi.fn<() => TestIslandApp[]>(() => []),
|
|
57
65
|
mockCreateMarimoFile: vi.fn(),
|
|
58
66
|
mockGetMarimoExportContext: vi.fn<() => TestExportContext | undefined>(
|
|
@@ -66,13 +74,19 @@ vi.mock("@/core/wasm/rpc", () => ({
|
|
|
66
74
|
request: {
|
|
67
75
|
bridge: mockBridge,
|
|
68
76
|
loadPackages: mockLoadPackages,
|
|
77
|
+
replaceSession: mockReplaceSessionRequest,
|
|
69
78
|
startSession: mockStartSessionRequest,
|
|
79
|
+
stopSession: mockStopSessionRequest,
|
|
70
80
|
},
|
|
71
81
|
send: {
|
|
72
82
|
consumerReady: vi.fn(),
|
|
73
83
|
},
|
|
74
84
|
},
|
|
75
|
-
addMessageListener: vi.fn(
|
|
85
|
+
addMessageListener: vi.fn(
|
|
86
|
+
(name: string, listener: (payload: never) => void) => {
|
|
87
|
+
mockMessageListeners.set(name, listener);
|
|
88
|
+
},
|
|
89
|
+
),
|
|
76
90
|
}),
|
|
77
91
|
}));
|
|
78
92
|
|
|
@@ -97,10 +111,11 @@ vi.mock("@/core/meta/globals", () => ({
|
|
|
97
111
|
}));
|
|
98
112
|
|
|
99
113
|
// Mock the jotai store
|
|
100
|
-
vi.mock("@/core/state/jotai", () => ({
|
|
114
|
+
vi.mock("@/core/state/jotai", async (importOriginal) => ({
|
|
115
|
+
...(await importOriginal<typeof import("@/core/state/jotai")>()),
|
|
101
116
|
store: {
|
|
102
117
|
get: vi.fn(),
|
|
103
|
-
set:
|
|
118
|
+
set: mockStoreSet,
|
|
104
119
|
},
|
|
105
120
|
}));
|
|
106
121
|
|
|
@@ -115,40 +130,69 @@ describe("IslandsPyodideBridge", () => {
|
|
|
115
130
|
mockParseMarimoIslandApps.mockReturnValue([]);
|
|
116
131
|
mockCreateMarimoFile.mockReset();
|
|
117
132
|
mockGetMarimoExportContext.mockReturnValue(undefined);
|
|
118
|
-
|
|
133
|
+
mockMessageListeners.clear();
|
|
134
|
+
bridge = new IslandsPyodideBridge();
|
|
119
135
|
});
|
|
120
136
|
|
|
121
|
-
|
|
137
|
+
function signalWorkerReady() {
|
|
138
|
+
const listener = mockMessageListeners.get("ready");
|
|
139
|
+
if (!listener) {
|
|
140
|
+
throw new Error("Missing worker ready listener");
|
|
141
|
+
}
|
|
142
|
+
listener({} as never);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function app(id = "app-1"): TestIslandApp {
|
|
146
|
+
return {
|
|
147
|
+
id,
|
|
148
|
+
cells: [{ code: "x = 1", idx: 0, output: "<div>1</div>" }],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function setSingleApp(file = "generated app 1", appId = "app-1") {
|
|
153
|
+
mockParseMarimoIslandApps.mockReturnValue([app(appId)]);
|
|
154
|
+
mockCreateMarimoFile.mockReturnValue(file);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function mockSingleApp(file = "generated app 1", appId = "app-1") {
|
|
158
|
+
setSingleApp(file, appId);
|
|
159
|
+
signalWorkerReady();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function sendSlider() {
|
|
163
|
+
return bridge.sendComponentValues({
|
|
164
|
+
objectIds: [uiElementId("slider-1")],
|
|
165
|
+
values: [2],
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function initializeSingleApp() {
|
|
170
|
+
mockSingleApp();
|
|
171
|
+
await bridge.initializeApps();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
describe("app initialization", () => {
|
|
122
175
|
it("should prefer trusted export notebook code when there is exactly one reactive app", async () => {
|
|
123
|
-
mockParseMarimoIslandApps.mockReturnValue([
|
|
124
|
-
{
|
|
125
|
-
id: "app-1",
|
|
126
|
-
cells: [{ code: "x = 1", idx: 0, output: "<div>1</div>" }],
|
|
127
|
-
},
|
|
128
|
-
]);
|
|
176
|
+
mockParseMarimoIslandApps.mockReturnValue([app()]);
|
|
129
177
|
mockGetMarimoExportContext.mockReturnValue({
|
|
130
178
|
trusted: true,
|
|
131
179
|
notebookCode:
|
|
132
180
|
"import marimo\napp = marimo.App()\n@app.cell\ndef __():\n x = 1\n return",
|
|
133
181
|
});
|
|
134
182
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
).startSessionsForAllApps();
|
|
183
|
+
signalWorkerReady();
|
|
184
|
+
await bridge.initializeApps();
|
|
138
185
|
|
|
139
186
|
expect(mockCreateMarimoFile).not.toHaveBeenCalled();
|
|
140
187
|
expect(mockStartSessionRequest).toHaveBeenCalledWith({
|
|
141
188
|
appId: "app-1",
|
|
142
189
|
code: "import marimo\napp = marimo.App()\n@app.cell\ndef __():\n x = 1\n return",
|
|
190
|
+
sessionGeneration: 1,
|
|
143
191
|
});
|
|
144
192
|
});
|
|
145
193
|
|
|
146
194
|
it("should ignore trusted export notebook code for a payload-backed app", async () => {
|
|
147
|
-
const payloadApp = {
|
|
148
|
-
id: "app-1",
|
|
149
|
-
payloadBacked: true,
|
|
150
|
-
cells: [{ code: "x = 1", idx: 0, output: "<div>1</div>" }],
|
|
151
|
-
};
|
|
195
|
+
const payloadApp = { ...app(), payloadBacked: true };
|
|
152
196
|
mockParseMarimoIslandApps.mockReturnValue([payloadApp]);
|
|
153
197
|
mockGetMarimoExportContext.mockReturnValue({
|
|
154
198
|
trusted: true,
|
|
@@ -156,28 +200,19 @@ describe("IslandsPyodideBridge", () => {
|
|
|
156
200
|
});
|
|
157
201
|
mockCreateMarimoFile.mockReturnValue("generated payload app");
|
|
158
202
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
).startSessionsForAllApps();
|
|
203
|
+
signalWorkerReady();
|
|
204
|
+
await bridge.initializeApps();
|
|
162
205
|
|
|
163
206
|
expect(mockCreateMarimoFile).toHaveBeenCalledWith(payloadApp);
|
|
164
207
|
expect(mockStartSessionRequest).toHaveBeenCalledWith({
|
|
165
208
|
appId: "app-1",
|
|
166
209
|
code: "generated payload app",
|
|
210
|
+
sessionGeneration: 1,
|
|
167
211
|
});
|
|
168
212
|
});
|
|
169
213
|
|
|
170
214
|
it("should keep synthesized per-app files for multiple reactive apps even when export context exists", async () => {
|
|
171
|
-
mockParseMarimoIslandApps.mockReturnValue([
|
|
172
|
-
{
|
|
173
|
-
id: "app-1",
|
|
174
|
-
cells: [{ code: "x = 1", idx: 0, output: "<div>1</div>" }],
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
id: "app-2",
|
|
178
|
-
cells: [{ code: "y = 2", idx: 0, output: "<div>2</div>" }],
|
|
179
|
-
},
|
|
180
|
-
]);
|
|
215
|
+
mockParseMarimoIslandApps.mockReturnValue([app(), app("app-2")]);
|
|
181
216
|
mockGetMarimoExportContext.mockReturnValue({
|
|
182
217
|
trusted: true,
|
|
183
218
|
notebookCode: "full notebook should be ignored",
|
|
@@ -186,43 +221,281 @@ describe("IslandsPyodideBridge", () => {
|
|
|
186
221
|
.mockReturnValueOnce("generated app 1")
|
|
187
222
|
.mockReturnValueOnce("generated app 2");
|
|
188
223
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
).startSessionsForAllApps();
|
|
224
|
+
signalWorkerReady();
|
|
225
|
+
await bridge.initializeApps();
|
|
192
226
|
|
|
193
227
|
expect(mockCreateMarimoFile).toHaveBeenCalledTimes(2);
|
|
194
228
|
expect(mockStartSessionRequest).toHaveBeenNthCalledWith(1, {
|
|
195
229
|
appId: "app-1",
|
|
196
230
|
code: "generated app 1",
|
|
231
|
+
sessionGeneration: 1,
|
|
197
232
|
});
|
|
198
233
|
expect(mockStartSessionRequest).toHaveBeenNthCalledWith(2, {
|
|
199
234
|
appId: "app-2",
|
|
200
235
|
code: "generated app 2",
|
|
236
|
+
sessionGeneration: 2,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await sendSlider();
|
|
240
|
+
await bridge.stopSession();
|
|
241
|
+
|
|
242
|
+
expect(mockReplaceSessionRequest).not.toHaveBeenCalled();
|
|
243
|
+
expect(mockStopSessionRequest).not.toHaveBeenCalled();
|
|
244
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
245
|
+
appId: "app-1",
|
|
246
|
+
functionName: "put_control_request",
|
|
247
|
+
payload: {
|
|
248
|
+
objectIds: ["slider-1"],
|
|
249
|
+
token: "test-uuid-12345",
|
|
250
|
+
type: "update-ui-element",
|
|
251
|
+
values: [2],
|
|
252
|
+
},
|
|
253
|
+
sessionGeneration: 1,
|
|
201
254
|
});
|
|
202
255
|
});
|
|
203
256
|
|
|
204
257
|
it("should synthesize a file for a single app when no trusted export context is present", async () => {
|
|
205
|
-
|
|
206
|
-
{
|
|
207
|
-
id: "app-1",
|
|
208
|
-
cells: [{ code: "x = 1", idx: 0, output: "<div>1</div>" }],
|
|
209
|
-
},
|
|
210
|
-
]);
|
|
211
|
-
mockCreateMarimoFile.mockReturnValue("generated app 1");
|
|
258
|
+
setSingleApp();
|
|
212
259
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
).startSessionsForAllApps();
|
|
260
|
+
signalWorkerReady();
|
|
261
|
+
await bridge.initializeApps();
|
|
216
262
|
|
|
217
263
|
expect(mockCreateMarimoFile).toHaveBeenCalledTimes(1);
|
|
218
264
|
expect(mockStartSessionRequest).toHaveBeenCalledWith({
|
|
219
265
|
appId: "app-1",
|
|
220
266
|
code: "generated app 1",
|
|
267
|
+
sessionGeneration: 1,
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("app lifecycle", () => {
|
|
273
|
+
it("holds controls until the first app session is ready", async () => {
|
|
274
|
+
setSingleApp();
|
|
275
|
+
|
|
276
|
+
const initialization = bridge.initializeApps();
|
|
277
|
+
const control = sendSlider();
|
|
278
|
+
await Promise.resolve();
|
|
279
|
+
|
|
280
|
+
expect(mockBridge).not.toHaveBeenCalled();
|
|
281
|
+
|
|
282
|
+
signalWorkerReady();
|
|
283
|
+
await Promise.all([initialization, control]);
|
|
284
|
+
expect(mockBridge).toHaveBeenCalledWith(
|
|
285
|
+
expect.objectContaining({
|
|
286
|
+
appId: "app-1",
|
|
287
|
+
sessionGeneration: 1,
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("starts the first app, skips duplicates, and replaces changed source", async () => {
|
|
293
|
+
mockSingleApp();
|
|
294
|
+
|
|
295
|
+
await bridge.initializeApps();
|
|
296
|
+
await bridge.initializeApps();
|
|
297
|
+
await sendSlider();
|
|
298
|
+
|
|
299
|
+
expect(mockStartSessionRequest).toHaveBeenCalledOnce();
|
|
300
|
+
expect(mockStartSessionRequest).toHaveBeenCalledWith({
|
|
301
|
+
appId: "app-1",
|
|
302
|
+
code: "generated app 1",
|
|
303
|
+
sessionGeneration: 1,
|
|
304
|
+
});
|
|
305
|
+
expect(mockReplaceSessionRequest).not.toHaveBeenCalled();
|
|
306
|
+
expect(mockBridge).toHaveBeenCalledWith(
|
|
307
|
+
expect.objectContaining({
|
|
308
|
+
appId: "app-1",
|
|
309
|
+
sessionGeneration: 1,
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
mockCreateMarimoFile.mockReturnValue("generated app 2");
|
|
313
|
+
await bridge.initializeApps();
|
|
314
|
+
|
|
315
|
+
expect(mockReplaceSessionRequest).toHaveBeenCalledOnce();
|
|
316
|
+
expect(mockReplaceSessionRequest).toHaveBeenCalledWith({
|
|
317
|
+
appId: "app-1",
|
|
318
|
+
code: "generated app 2",
|
|
319
|
+
sessionGeneration: 2,
|
|
320
|
+
});
|
|
321
|
+
expect(mockStoreSet).toHaveBeenCalledOnce();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("stops the matching active app", async () => {
|
|
325
|
+
mockSingleApp();
|
|
326
|
+
await bridge.initializeApps();
|
|
327
|
+
|
|
328
|
+
await bridge.stopSession("other-app");
|
|
329
|
+
await bridge.stopSession("app-1");
|
|
330
|
+
|
|
331
|
+
expect(mockStopSessionRequest).toHaveBeenCalledOnce();
|
|
332
|
+
expect(mockStopSessionRequest).toHaveBeenCalledWith({
|
|
333
|
+
appId: "app-1",
|
|
334
|
+
sessionGeneration: 1,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
mockBridge.mockClear();
|
|
338
|
+
const control = sendSlider();
|
|
339
|
+
await Promise.resolve();
|
|
340
|
+
expect(mockBridge).not.toHaveBeenCalled();
|
|
341
|
+
|
|
342
|
+
mockSingleApp("generated app 2");
|
|
343
|
+
await bridge.initializeApps();
|
|
344
|
+
await control;
|
|
345
|
+
|
|
346
|
+
expect(mockBridge).toHaveBeenCalledWith(
|
|
347
|
+
expect.objectContaining({ sessionGeneration: 2 }),
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("replaces the active session after stop fails", async () => {
|
|
352
|
+
mockSingleApp();
|
|
353
|
+
await bridge.initializeApps();
|
|
354
|
+
mockStopSessionRequest.mockRejectedValueOnce(new Error("stop failed"));
|
|
355
|
+
|
|
356
|
+
await expect(bridge.stopSession("app-1")).rejects.toThrow("stop failed");
|
|
357
|
+
|
|
358
|
+
mockSingleApp("generated app 2", "app-2");
|
|
359
|
+
|
|
360
|
+
await bridge.initializeApps();
|
|
361
|
+
|
|
362
|
+
expect(mockStartSessionRequest).toHaveBeenCalledOnce();
|
|
363
|
+
expect(mockReplaceSessionRequest).toHaveBeenCalledWith({
|
|
364
|
+
appId: "app-2",
|
|
365
|
+
code: "generated app 2",
|
|
366
|
+
sessionGeneration: 2,
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("keeps controls sent during stop scoped to the stopped app", async () => {
|
|
371
|
+
mockSingleApp();
|
|
372
|
+
await bridge.initializeApps();
|
|
373
|
+
mockBridge.mockClear();
|
|
374
|
+
let finishStop!: () => void;
|
|
375
|
+
mockStopSessionRequest.mockReturnValueOnce(
|
|
376
|
+
new Promise<void>((resolve) => {
|
|
377
|
+
finishStop = resolve;
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const stop = bridge.stopSession("app-1");
|
|
382
|
+
await vi.waitFor(() =>
|
|
383
|
+
expect(mockStopSessionRequest).toHaveBeenCalledOnce(),
|
|
384
|
+
);
|
|
385
|
+
const control = sendSlider();
|
|
386
|
+
mockSingleApp("generated app 2", "app-2");
|
|
387
|
+
const nextInitialization = bridge.initializeApps();
|
|
388
|
+
|
|
389
|
+
finishStop();
|
|
390
|
+
await Promise.all([stop, control, nextInitialization]);
|
|
391
|
+
|
|
392
|
+
expect(mockBridge).toHaveBeenCalledWith(
|
|
393
|
+
expect.objectContaining({
|
|
394
|
+
appId: "app-1",
|
|
395
|
+
sessionGeneration: 1,
|
|
396
|
+
}),
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("allows initialization to retry after replacement fails", async () => {
|
|
401
|
+
mockSingleApp();
|
|
402
|
+
await bridge.initializeApps();
|
|
403
|
+
mockCreateMarimoFile.mockReturnValue("generated app 2");
|
|
404
|
+
mockReplaceSessionRequest.mockRejectedValueOnce(
|
|
405
|
+
new Error("replacement failed"),
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
await expect(bridge.initializeApps()).rejects.toThrow(
|
|
409
|
+
"replacement failed",
|
|
410
|
+
);
|
|
411
|
+
await bridge.initializeApps();
|
|
412
|
+
|
|
413
|
+
expect(mockStartSessionRequest).toHaveBeenCalledOnce();
|
|
414
|
+
expect(mockReplaceSessionRequest).toHaveBeenCalledTimes(2);
|
|
415
|
+
expect(mockReplaceSessionRequest).toHaveBeenLastCalledWith({
|
|
416
|
+
appId: "app-1",
|
|
417
|
+
code: "generated app 2",
|
|
418
|
+
sessionGeneration: 3,
|
|
221
419
|
});
|
|
222
420
|
});
|
|
421
|
+
|
|
422
|
+
it("scopes controls to the replacement active when they were sent", async () => {
|
|
423
|
+
mockSingleApp();
|
|
424
|
+
await bridge.initializeApps();
|
|
425
|
+
mockBridge.mockClear();
|
|
426
|
+
mockCreateMarimoFile.mockReturnValue("generated app 2");
|
|
427
|
+
let finishSecondApp!: () => void;
|
|
428
|
+
let finishThirdApp!: () => void;
|
|
429
|
+
mockReplaceSessionRequest
|
|
430
|
+
.mockReturnValueOnce(
|
|
431
|
+
new Promise<void>((resolve) => {
|
|
432
|
+
finishSecondApp = resolve;
|
|
433
|
+
}),
|
|
434
|
+
)
|
|
435
|
+
.mockReturnValueOnce(
|
|
436
|
+
new Promise<void>((resolve) => {
|
|
437
|
+
finishThirdApp = resolve;
|
|
438
|
+
}),
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const secondInitialization = bridge.initializeApps();
|
|
442
|
+
await vi.waitFor(() =>
|
|
443
|
+
expect(mockReplaceSessionRequest).toHaveBeenCalledOnce(),
|
|
444
|
+
);
|
|
445
|
+
const control = sendSlider();
|
|
446
|
+
mockSingleApp("generated app 3", "app-2");
|
|
447
|
+
const thirdInitialization = bridge.initializeApps();
|
|
448
|
+
|
|
449
|
+
expect(mockBridge).not.toHaveBeenCalled();
|
|
450
|
+
|
|
451
|
+
finishSecondApp();
|
|
452
|
+
await vi.waitFor(() => expect(mockBridge).toHaveBeenCalledOnce());
|
|
453
|
+
expect(mockBridge).toHaveBeenCalledWith(
|
|
454
|
+
expect.objectContaining({
|
|
455
|
+
appId: "app-1",
|
|
456
|
+
sessionGeneration: 2,
|
|
457
|
+
}),
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
finishThirdApp();
|
|
461
|
+
await Promise.all([secondInitialization, thirdInitialization, control]);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("keeps controls from a failed replacement out of the next app", async () => {
|
|
465
|
+
mockSingleApp();
|
|
466
|
+
await bridge.initializeApps();
|
|
467
|
+
mockBridge.mockClear();
|
|
468
|
+
mockCreateMarimoFile.mockReturnValue("generated app 2");
|
|
469
|
+
let failReplacement!: (error: Error) => void;
|
|
470
|
+
mockReplaceSessionRequest.mockReturnValueOnce(
|
|
471
|
+
new Promise<void>((_resolve, reject) => {
|
|
472
|
+
failReplacement = reject;
|
|
473
|
+
}),
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
const failedInitialization = bridge.initializeApps();
|
|
477
|
+
await vi.waitFor(() =>
|
|
478
|
+
expect(mockReplaceSessionRequest).toHaveBeenCalledOnce(),
|
|
479
|
+
);
|
|
480
|
+
const control = sendSlider();
|
|
481
|
+
failReplacement(new Error("replacement failed"));
|
|
482
|
+
await expect(failedInitialization).rejects.toThrow("replacement failed");
|
|
483
|
+
|
|
484
|
+
mockSingleApp("generated app 3", "app-2");
|
|
485
|
+
await Promise.all([bridge.initializeApps(), control]);
|
|
486
|
+
|
|
487
|
+
expect(mockBridge).toHaveBeenCalledWith(
|
|
488
|
+
expect.objectContaining({
|
|
489
|
+
appId: "app-1",
|
|
490
|
+
sessionGeneration: 2,
|
|
491
|
+
}),
|
|
492
|
+
);
|
|
493
|
+
});
|
|
223
494
|
});
|
|
224
495
|
|
|
225
496
|
describe("sendComponentValues", () => {
|
|
497
|
+
beforeEach(initializeSingleApp);
|
|
498
|
+
|
|
226
499
|
it("should include type field and token in control request", async () => {
|
|
227
500
|
const request = {
|
|
228
501
|
objectIds: [uiElementId("Hbol-0")],
|
|
@@ -232,6 +505,7 @@ describe("IslandsPyodideBridge", () => {
|
|
|
232
505
|
await bridge.sendComponentValues(request);
|
|
233
506
|
|
|
234
507
|
expect(mockBridge).toHaveBeenCalledWith({
|
|
508
|
+
appId: "app-1",
|
|
235
509
|
functionName: "put_control_request",
|
|
236
510
|
payload: {
|
|
237
511
|
type: "update-ui-element",
|
|
@@ -239,6 +513,7 @@ describe("IslandsPyodideBridge", () => {
|
|
|
239
513
|
values: [58],
|
|
240
514
|
token: "test-uuid-12345",
|
|
241
515
|
},
|
|
516
|
+
sessionGeneration: 1,
|
|
242
517
|
});
|
|
243
518
|
});
|
|
244
519
|
|
|
@@ -251,17 +526,21 @@ describe("IslandsPyodideBridge", () => {
|
|
|
251
526
|
await bridge.sendComponentValues(request);
|
|
252
527
|
|
|
253
528
|
expect(mockBridge).toHaveBeenCalledWith({
|
|
529
|
+
appId: "app-1",
|
|
254
530
|
functionName: "put_control_request",
|
|
255
531
|
payload: expect.objectContaining({
|
|
256
532
|
type: "update-ui-element",
|
|
257
533
|
objectIds: ["slider-1", "slider-2"],
|
|
258
534
|
values: [10, 20],
|
|
259
535
|
}),
|
|
536
|
+
sessionGeneration: 1,
|
|
260
537
|
});
|
|
261
538
|
});
|
|
262
539
|
});
|
|
263
540
|
|
|
264
541
|
describe("sendFunctionRequest", () => {
|
|
542
|
+
beforeEach(initializeSingleApp);
|
|
543
|
+
|
|
265
544
|
it("should include type field in control request", async () => {
|
|
266
545
|
const request = {
|
|
267
546
|
functionCallId: requestId("call-123"),
|
|
@@ -273,6 +552,7 @@ describe("IslandsPyodideBridge", () => {
|
|
|
273
552
|
await bridge.sendFunctionRequest(request);
|
|
274
553
|
|
|
275
554
|
expect(mockBridge).toHaveBeenCalledWith({
|
|
555
|
+
appId: "app-1",
|
|
276
556
|
functionName: "put_control_request",
|
|
277
557
|
payload: {
|
|
278
558
|
type: "invoke-function",
|
|
@@ -281,11 +561,14 @@ describe("IslandsPyodideBridge", () => {
|
|
|
281
561
|
functionName: "my_function",
|
|
282
562
|
args: { x: 1, y: 2 },
|
|
283
563
|
},
|
|
564
|
+
sessionGeneration: 1,
|
|
284
565
|
});
|
|
285
566
|
});
|
|
286
567
|
});
|
|
287
568
|
|
|
288
569
|
describe("sendRun", () => {
|
|
570
|
+
beforeEach(initializeSingleApp);
|
|
571
|
+
|
|
289
572
|
it("should include type field in control request", async () => {
|
|
290
573
|
const request = {
|
|
291
574
|
cellIds: [cellId("cell-1"), cellId("cell-2")],
|
|
@@ -295,12 +578,14 @@ describe("IslandsPyodideBridge", () => {
|
|
|
295
578
|
await bridge.sendRun(request);
|
|
296
579
|
|
|
297
580
|
expect(mockBridge).toHaveBeenCalledWith({
|
|
581
|
+
appId: "app-1",
|
|
298
582
|
functionName: "put_control_request",
|
|
299
583
|
payload: {
|
|
300
584
|
type: "execute-cells",
|
|
301
585
|
cellIds: ["cell-1", "cell-2"],
|
|
302
586
|
codes: ["print('hello')", "print('world')"],
|
|
303
587
|
},
|
|
588
|
+
sessionGeneration: 1,
|
|
304
589
|
});
|
|
305
590
|
});
|
|
306
591
|
|
|
@@ -313,7 +598,11 @@ describe("IslandsPyodideBridge", () => {
|
|
|
313
598
|
await bridge.sendRun(request);
|
|
314
599
|
|
|
315
600
|
// Verify loadPackages was called with joined codes
|
|
316
|
-
expect(mockLoadPackages).toHaveBeenCalledWith(
|
|
601
|
+
expect(mockLoadPackages).toHaveBeenCalledWith({
|
|
602
|
+
appId: "app-1",
|
|
603
|
+
code: "import pandas",
|
|
604
|
+
sessionGeneration: 1,
|
|
605
|
+
});
|
|
317
606
|
|
|
318
607
|
// Verify order: loadPackages should be called before bridge
|
|
319
608
|
const loadPackagesCallOrder =
|
|
@@ -324,6 +613,8 @@ describe("IslandsPyodideBridge", () => {
|
|
|
324
613
|
});
|
|
325
614
|
|
|
326
615
|
describe("sendModelValue", () => {
|
|
616
|
+
beforeEach(initializeSingleApp);
|
|
617
|
+
|
|
327
618
|
it("should include type field in control request", async () => {
|
|
328
619
|
const request = {
|
|
329
620
|
modelId: widgetModelId("widget-1"),
|
|
@@ -338,6 +629,7 @@ describe("IslandsPyodideBridge", () => {
|
|
|
338
629
|
await bridge.sendModelValue(request);
|
|
339
630
|
|
|
340
631
|
expect(mockBridge).toHaveBeenCalledWith({
|
|
632
|
+
appId: "app-1",
|
|
341
633
|
functionName: "put_control_request",
|
|
342
634
|
payload: {
|
|
343
635
|
type: "model",
|
|
@@ -349,11 +641,14 @@ describe("IslandsPyodideBridge", () => {
|
|
|
349
641
|
},
|
|
350
642
|
buffers: [],
|
|
351
643
|
},
|
|
644
|
+
sessionGeneration: 1,
|
|
352
645
|
});
|
|
353
646
|
});
|
|
354
647
|
});
|
|
355
648
|
|
|
356
649
|
describe("control request message format", () => {
|
|
650
|
+
beforeEach(initializeSingleApp);
|
|
651
|
+
|
|
357
652
|
it("should always include the type field required by msgspec", async () => {
|
|
358
653
|
// Test all methods to ensure they include the type field
|
|
359
654
|
await bridge.sendComponentValues({ objectIds: [], values: [] });
|