@marimo-team/islands 0.19.8-dev40 → 0.19.8-dev48

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 (35) hide show
  1. package/dist/assets/__vite-browser-external-WSlCcXn_.js +1 -0
  2. package/dist/assets/worker-DUYMdbtA.js +73 -0
  3. package/dist/main.js +1740 -1691
  4. package/dist/style.css +1 -1
  5. package/dist/{useDeepCompareMemoize-CMGprt3H.js → useDeepCompareMemoize-BhZZsis0.js} +7 -3
  6. package/dist/{vega-component-DU3aSp4m.js → vega-component-DCxUyPnb.js} +1 -1
  7. package/package.json +1 -1
  8. package/src/components/app-config/optional-features.tsx +1 -1
  9. package/src/components/chat/__tests__/useFileState.test.tsx +93 -0
  10. package/src/components/chat/acp/agent-panel.tsx +26 -77
  11. package/src/components/chat/chat-components.tsx +114 -1
  12. package/src/components/chat/chat-panel.tsx +32 -104
  13. package/src/components/chat/chat-utils.ts +42 -0
  14. package/src/components/editor/ai/add-cell-with-ai.tsx +85 -53
  15. package/src/components/editor/ai/ai-completion-editor.tsx +15 -38
  16. package/src/components/editor/chrome/panels/packages-panel.tsx +12 -9
  17. package/src/core/islands/__tests__/bridge.test.ts +7 -2
  18. package/src/core/islands/bridge.ts +1 -1
  19. package/src/core/islands/main.ts +7 -0
  20. package/src/core/network/types.ts +2 -2
  21. package/src/core/wasm/bridge.ts +1 -1
  22. package/src/core/websocket/useMarimoKernelConnection.tsx +5 -15
  23. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +86 -167
  24. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +37 -123
  25. package/src/plugins/impl/anywidget/__tests__/model.test.ts +128 -122
  26. package/src/{utils/__tests__/data-views.test.ts → plugins/impl/anywidget/__tests__/serialization.test.ts} +42 -96
  27. package/src/plugins/impl/anywidget/model.ts +348 -223
  28. package/src/plugins/impl/anywidget/schemas.ts +32 -0
  29. package/src/{utils/data-views.ts → plugins/impl/anywidget/serialization.ts} +13 -36
  30. package/src/plugins/impl/anywidget/types.ts +27 -0
  31. package/src/plugins/impl/chat/chat-ui.tsx +22 -20
  32. package/src/utils/Deferred.ts +21 -0
  33. package/src/utils/json/base64.ts +38 -8
  34. package/dist/assets/__vite-browser-external-6-UwTyQC.js +0 -1
  35. package/dist/assets/worker-D3e5wDxM.js +0 -73
@@ -1,65 +1,17 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { act, render, waitFor } from "@testing-library/react";
3
+ import { render, waitFor } from "@testing-library/react";
4
4
  import { beforeEach, describe, expect, it, vi } from "vitest";
5
5
  import { TestUtils } from "@/__tests__/test-helpers";
6
- import type { UIElementId } from "@/core/cells/ids";
7
- import { MarimoIncomingMessageEvent } from "@/core/dom/events";
8
- import { getDirtyFields, visibleForTesting } from "../AnyWidgetPlugin";
9
- import { Model } from "../model";
6
+ import type { HTMLElementNotDerivedFromRef } from "@/hooks/useEventListener";
7
+ import { visibleForTesting } from "../AnyWidgetPlugin";
8
+ import { MODEL_MANAGER, Model } from "../model";
9
+ import type { WidgetModelId } from "../types";
10
10
 
11
11
  const { LoadedSlot } = visibleForTesting;
12
12
 
13
- describe("getDirtyFields", () => {
14
- it("should return empty set when values are equal", () => {
15
- const value = { foo: "bar", baz: 123 };
16
- const initialValue = { foo: "bar", baz: 123 };
17
-
18
- const result = getDirtyFields(value, initialValue);
19
-
20
- expect(result.size).toBe(0);
21
- });
22
-
23
- it("should return keys of changed values", () => {
24
- const value = { foo: "changed", baz: 123 };
25
- const initialValue = { foo: "bar", baz: 123 };
26
-
27
- const result = getDirtyFields(value, initialValue);
28
-
29
- expect(result.size).toBe(1);
30
- expect(result.has("foo")).toBe(true);
31
- });
32
-
33
- it("should handle multiple changed values", () => {
34
- const value = { foo: "changed", baz: 456, extra: "new" };
35
- const initialValue = { foo: "bar", baz: 123, extra: "old" };
36
-
37
- const result = getDirtyFields(value, initialValue);
38
-
39
- expect(result.size).toBe(3);
40
- expect(result.has("foo")).toBe(true);
41
- expect(result.has("baz")).toBe(true);
42
- expect(result.has("extra")).toBe(true);
43
- });
44
-
45
- it("should handle nested objects correctly", () => {
46
- const value = { foo: "bar", nested: { a: 1, b: 2 } };
47
- const initialValue = { foo: "bar", nested: { a: 1, b: 3 } };
48
-
49
- const result = getDirtyFields(value, initialValue);
50
-
51
- expect(result.size).toBe(1);
52
- expect(result.has("nested")).toBe(true);
53
- });
54
-
55
- it("should handle subset of initial fields", () => {
56
- const value = { foo: "bar", baz: 123 };
57
- const initialValue = { foo: "bar", baz: 123, full: "value" };
58
-
59
- const result = getDirtyFields(value, initialValue);
60
- expect(result.size).toBe(0);
61
- });
62
- });
13
+ // Helper to create typed model IDs for tests
14
+ const asModelId = (id: string): WidgetModelId => id as WidgetModelId;
63
15
 
64
16
  // Mock a minimal AnyWidget implementation
65
17
  const mockWidget = {
@@ -76,23 +28,32 @@ vi.mock("../AnyWidgetPlugin", async () => {
76
28
  });
77
29
 
78
30
  describe("LoadedSlot", () => {
31
+ const modelId = asModelId("test-model-id");
32
+ let mockModel: Model<{ count: number }>;
33
+
79
34
  const mockProps = {
80
- value: { count: 0 },
81
- setValue: vi.fn(),
82
35
  widget: mockWidget,
83
- functions: {
84
- send_to_widget: vi.fn().mockResolvedValue(null),
85
- },
86
36
  data: {
87
37
  jsUrl: "http://example.com/widget.js",
88
38
  jsHash: "abc123",
89
- initialValue: { count: 0 },
90
39
  },
91
- host: document.createElement("div"),
40
+ host: document.createElement(
41
+ "div",
42
+ ) as unknown as HTMLElementNotDerivedFromRef,
43
+ modelId: modelId,
92
44
  };
93
45
 
94
46
  beforeEach(() => {
95
47
  vi.clearAllMocks();
48
+ // Create and register a mock model before each test
49
+ mockModel = new Model(
50
+ { count: 0 },
51
+ {
52
+ sendUpdate: vi.fn().mockResolvedValue(undefined),
53
+ sendCustomMessage: vi.fn().mockResolvedValue(undefined),
54
+ },
55
+ );
56
+ MODEL_MANAGER.set(modelId, mockModel);
96
57
  });
97
58
 
98
59
  it("should render a div with ref", () => {
@@ -100,83 +61,36 @@ describe("LoadedSlot", () => {
100
61
  expect(container.querySelector("div")).not.toBeNull();
101
62
  });
102
63
 
103
- it("should initialize model with merged values", () => {
104
- const modelSpy = vi.spyOn(Model.prototype, "updateAndEmitDiffs");
105
- render(<LoadedSlot {...mockProps} />);
106
-
107
- expect(modelSpy).toHaveBeenCalledExactlyOnceWith({ count: 0 });
108
- });
109
-
110
- it("should update model when value prop changes", async () => {
111
- const { rerender } = render(<LoadedSlot {...mockProps} />);
112
- const modelSpy = vi.spyOn(Model.prototype, "updateAndEmitDiffs");
113
-
114
- // Update the value prop
115
- rerender(<LoadedSlot {...mockProps} value={{ count: 5 }} />);
116
-
117
- // Model should be updated with the new value
118
- expect(modelSpy).toHaveBeenCalledWith({ count: 5 });
119
- });
120
-
121
- it("should listen for incoming messages", async () => {
64
+ it("should call runAnyWidgetModule on initialization", async () => {
122
65
  render(<LoadedSlot {...mockProps} />);
123
66
 
124
- // Send a mock message
125
- const mockMessageEvent = MarimoIncomingMessageEvent.create({
126
- detail: {
127
- objectId: "test-id" as UIElementId,
128
- message: {
129
- method: "update",
130
- state: { count: 10 },
131
- buffer_paths: [],
132
- },
133
- buffers: [],
134
- },
135
- bubbles: false,
136
- composed: true,
137
- });
138
- const updateAndEmitDiffsSpy = vi.spyOn(Model.prototype, "set");
139
-
140
- // Dispatch the event on the host element
141
- act(() => {
142
- mockProps.host.dispatchEvent(mockMessageEvent);
143
- });
144
-
67
+ // Wait a render
145
68
  await waitFor(() => {
146
- expect(updateAndEmitDiffsSpy).toHaveBeenCalledWith("count", 10);
69
+ expect(mockWidget.render).toHaveBeenCalled();
147
70
  });
148
71
  });
149
72
 
150
- it("should call runAnyWidgetModule on initialization", async () => {
73
+ it("should re-run widget when widget prop changes", async () => {
151
74
  const { rerender } = render(<LoadedSlot {...mockProps} />);
152
75
 
153
- // Wait a render
76
+ // Wait for initial render
154
77
  await waitFor(() => {
155
78
  expect(mockWidget.render).toHaveBeenCalled();
156
79
  });
157
80
 
158
- // Render without any prop changes
159
- rerender(<LoadedSlot {...mockProps} />);
160
- await TestUtils.nextTick();
81
+ // Create a new widget mock
82
+ const newMockWidget = {
83
+ initialize: vi.fn(),
84
+ render: vi.fn(),
85
+ };
161
86
 
162
- // Still only called once
163
- expect(mockWidget.render).toHaveBeenCalledTimes(1);
164
-
165
- // Change the jsUrl
166
- rerender(
167
- <LoadedSlot
168
- {...mockProps}
169
- data={{
170
- ...mockProps.data,
171
- jsUrl: "http://example.com/widget-updated.js",
172
- }}
173
- />,
174
- );
87
+ // Change the widget
88
+ rerender(<LoadedSlot {...mockProps} widget={newMockWidget} />);
175
89
  await TestUtils.nextTick();
176
90
 
177
- // Wait a render
91
+ // Wait for re-render with new widget
178
92
  await waitFor(() => {
179
- expect(mockWidget.render).toHaveBeenCalledTimes(2);
93
+ expect(newMockWidget.render).toHaveBeenCalled();
180
94
  });
181
95
  });
182
96
  });
@@ -9,35 +9,73 @@ import {
9
9
  vi,
10
10
  } from "vitest";
11
11
  import { TestUtils } from "@/__tests__/test-helpers";
12
- import type { Base64String } from "@/utils/json/base64";
13
12
  import {
14
- type AnyWidgetMessage,
13
+ getMarimoInternal,
15
14
  handleWidgetMessage,
16
15
  Model,
17
16
  visibleForTesting,
18
17
  } from "../model";
18
+ import type { WidgetModelId } from "../types";
19
19
 
20
20
  const { ModelManager } = visibleForTesting;
21
21
 
22
+ // Helper to create typed model IDs for tests
23
+ const asModelId = (id: string): WidgetModelId => id as WidgetModelId;
24
+
25
+ // Mock the request client
26
+ const mockSendModelValue = vi.fn().mockResolvedValue(null);
27
+ vi.mock("@/core/network/requests", () => ({
28
+ getRequestClient: () => ({
29
+ sendModelValue: mockSendModelValue,
30
+ }),
31
+ }));
32
+
33
+ // Helper to create a mock MarimoComm
34
+ function createMockComm<T>() {
35
+ return {
36
+ sendUpdate: vi.fn().mockResolvedValue(undefined),
37
+ sendCustomMessage: vi.fn().mockResolvedValue(undefined),
38
+ };
39
+ }
40
+
22
41
  describe("Model", () => {
23
42
  let model: Model<{ foo: string; bar: number }>;
24
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
- let onChange: (value: any) => void;
26
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
- let sendToWidget: (req: {
28
- content: unknown;
29
- buffers: Base64String[];
30
- }) => Promise<null | undefined>;
43
+ let mockComm: ReturnType<typeof createMockComm<{ foo: string; bar: number }>>;
31
44
 
32
45
  beforeEach(() => {
33
- onChange = vi.fn();
34
- sendToWidget = vi.fn().mockResolvedValue(null);
35
- model = new Model(
36
- { foo: "test", bar: 123 },
37
- onChange,
38
- sendToWidget,
39
- new Set(),
40
- );
46
+ mockComm = createMockComm();
47
+ mockSendModelValue.mockClear();
48
+ model = new Model({ foo: "test", bar: 123 }, mockComm);
49
+ });
50
+
51
+ describe("public API", () => {
52
+ it("should only expose AFM-compliant interface", () => {
53
+ // Get all enumerable own properties
54
+ const ownProperties = Object.keys(model).sort();
55
+ // Get prototype methods (excluding constructor)
56
+ const prototypeMethods = Object.getOwnPropertyNames(
57
+ Object.getPrototypeOf(model),
58
+ )
59
+ .filter((name) => name !== "constructor")
60
+ .sort();
61
+
62
+ // Snapshot the public API to catch accidental leaks of internal methods
63
+ expect({ ownProperties, prototypeMethods }).toMatchInlineSnapshot(`
64
+ {
65
+ "ownProperties": [
66
+ "widget_manager",
67
+ ],
68
+ "prototypeMethods": [
69
+ "get",
70
+ "off",
71
+ "on",
72
+ "save_changes",
73
+ "send",
74
+ "set",
75
+ ],
76
+ }
77
+ `);
78
+ });
41
79
  });
42
80
 
43
81
  describe("get/set", () => {
@@ -70,7 +108,7 @@ describe("Model", () => {
70
108
  model.set("bar", 456);
71
109
  model.save_changes();
72
110
 
73
- expect(onChange).toHaveBeenCalledWith({
111
+ expect(mockComm.sendUpdate).toHaveBeenCalledWith({
74
112
  foo: "new value",
75
113
  bar: 456,
76
114
  });
@@ -80,7 +118,7 @@ describe("Model", () => {
80
118
  model.set("foo", "new value");
81
119
  model.save_changes();
82
120
 
83
- expect(onChange).toHaveBeenCalledWith({
121
+ expect(mockComm.sendUpdate).toHaveBeenCalledWith({
84
122
  foo: "new value",
85
123
  });
86
124
 
@@ -88,17 +126,17 @@ describe("Model", () => {
88
126
  model.save_changes();
89
127
 
90
128
  // After clearing, only the newly changed field is sent
91
- expect(onChange).toHaveBeenCalledWith({
129
+ expect(mockComm.sendUpdate).toHaveBeenCalledWith({
92
130
  bar: 456,
93
131
  });
94
132
  });
95
133
 
96
- it("should not call onChange when no dirty fields", () => {
134
+ it("should not call sendUpdate when no dirty fields", () => {
97
135
  model.set("foo", "new value");
98
136
  model.save_changes();
99
- model.save_changes(); // Second save should not call onChange
137
+ model.save_changes(); // Second save should not call sendUpdate
100
138
 
101
- expect(onChange).toHaveBeenCalledTimes(1);
139
+ expect(mockComm.sendUpdate).toHaveBeenCalledTimes(1);
102
140
  });
103
141
  });
104
142
 
@@ -145,23 +183,28 @@ describe("Model", () => {
145
183
  describe("send", () => {
146
184
  it("should send message and handle callbacks", async () => {
147
185
  const callback = vi.fn();
148
- model.send({ test: true }, callback);
186
+ await model.send({ test: true }, callback);
149
187
 
150
- expect(sendToWidget).toHaveBeenCalledWith({
151
- content: {
152
- state: { test: true },
153
- bufferPaths: [],
154
- },
155
- buffers: [],
156
- });
157
- await TestUtils.nextTick(); // flush
158
- expect(callback).toHaveBeenCalledWith(null);
188
+ expect(mockComm.sendCustomMessage).toHaveBeenCalledWith(
189
+ { test: true },
190
+ [],
191
+ );
192
+ expect(callback).toHaveBeenCalled();
193
+ });
194
+
195
+ it("should convert buffers to DataViews", async () => {
196
+ const buffer = new ArrayBuffer(8);
197
+ await model.send({ test: true }, undefined, [buffer]);
198
+
199
+ expect(mockComm.sendCustomMessage).toHaveBeenCalledWith({ test: true }, [
200
+ expect.any(DataView),
201
+ ]);
159
202
  });
160
203
  });
161
204
 
162
205
  describe("widget_manager", () => {
163
- const childModelId = "test-id";
164
- const childModel = new Model({ foo: "test" }, vi.fn(), vi.fn(), new Set());
206
+ const childModelId = asModelId("test-id");
207
+ const childModel = new Model({ foo: "test" }, createMockComm());
165
208
  const manager = new ModelManager(10);
166
209
  let previousModelManager = Model._modelManager;
167
210
 
@@ -177,9 +220,9 @@ describe("Model", () => {
177
220
  });
178
221
 
179
222
  it("should throw error when accessing a model that is not registered", async () => {
180
- await expect(model.widget_manager.get_model("random-id")).rejects.toThrow(
181
- "Model not found for key: random-id",
182
- );
223
+ await expect(
224
+ model.widget_manager.get_model(asModelId("random-id")),
225
+ ).rejects.toThrow("Model not found for key: random-id");
183
226
  });
184
227
 
185
228
  it("should return the registered model", async () => {
@@ -194,7 +237,7 @@ describe("Model", () => {
194
237
  const callback = vi.fn();
195
238
  model.on("change:foo", callback);
196
239
 
197
- model.updateAndEmitDiffs({ foo: "test", bar: 456 });
240
+ getMarimoInternal(model).updateAndEmitDiffs({ foo: "test", bar: 456 });
198
241
  expect(callback).not.toHaveBeenCalled(); // foo didn't change
199
242
  expect(model.get("bar")).toBe(456);
200
243
  });
@@ -202,47 +245,33 @@ describe("Model", () => {
202
245
  it("should update and emit for deep changes", () => {
203
246
  const modelWithObject = new Model<{ foo: { nested: string } }>(
204
247
  { foo: { nested: "test" } },
205
- onChange,
206
- sendToWidget,
207
- new Set(),
248
+ createMockComm(),
208
249
  );
209
250
  const callback = vi.fn();
210
251
  modelWithObject.on("change:foo", callback);
211
252
 
212
- modelWithObject.updateAndEmitDiffs({ foo: { nested: "changed" } });
253
+ getMarimoInternal(modelWithObject).updateAndEmitDiffs({
254
+ foo: { nested: "changed" },
255
+ });
213
256
  expect(callback).toHaveBeenCalledTimes(1);
214
257
  });
215
258
 
216
259
  it("should emit change event for any changes", async () => {
217
260
  const callback = vi.fn();
218
261
  model.on("change", callback);
219
- model.updateAndEmitDiffs({ foo: "changed", bar: 456 });
262
+ getMarimoInternal(model).updateAndEmitDiffs({ foo: "changed", bar: 456 });
220
263
  await TestUtils.nextTick(); // flush
221
264
  expect(callback).toHaveBeenCalledTimes(1);
222
265
  });
223
266
  });
224
267
 
225
- describe("receiveCustomMessage", () => {
226
- it("should handle update messages", () => {
227
- model.receiveCustomMessage({
228
- method: "update",
229
- state: {
230
- foo: "updated",
231
- bar: 789,
232
- },
233
- buffer_paths: [],
234
- });
235
-
236
- expect(model.get("foo")).toBe("updated");
237
- expect(model.get("bar")).toBe(789);
238
- });
239
-
268
+ describe("emitCustomMessage", () => {
240
269
  it("should handle custom messages", () => {
241
270
  const callback = vi.fn();
242
271
  model.on("msg:custom", callback);
243
272
 
244
273
  const content = { type: "test" };
245
- model.receiveCustomMessage({
274
+ getMarimoInternal(model).emitCustomMessage({
246
275
  method: "custom",
247
276
  content,
248
277
  });
@@ -256,7 +285,7 @@ describe("Model", () => {
256
285
 
257
286
  const content = { type: "test" };
258
287
  const buffer = new DataView(new ArrayBuffer(8));
259
- model.receiveCustomMessage(
288
+ getMarimoInternal(model).emitCustomMessage(
260
289
  {
261
290
  method: "custom",
262
291
  content,
@@ -266,108 +295,85 @@ describe("Model", () => {
266
295
 
267
296
  expect(callback).toHaveBeenCalledWith(content, [buffer]);
268
297
  });
269
-
270
- it("should log error for invalid messages", () => {
271
- const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {
272
- // noop
273
- });
274
- model.receiveCustomMessage({ invalid: "message" });
275
-
276
- expect(consoleSpy).toHaveBeenCalledTimes(2);
277
- });
278
298
  });
279
299
  });
280
300
 
281
301
  describe("ModelManager", () => {
282
302
  let modelManager = new ModelManager(50);
283
- const handle = ({
284
- modelId,
285
- message,
286
- buffers,
287
- }: {
288
- modelId: string;
289
- message: AnyWidgetMessage;
290
- buffers: readonly DataView[];
291
- }) => {
292
- return handleWidgetMessage({
293
- modelId,
294
- msg: message,
295
- buffers,
296
- modelManager,
297
- });
298
- };
303
+ const testId = asModelId("test-id");
299
304
 
300
305
  beforeEach(() => {
301
306
  // Clear the model manager before each test
302
307
  modelManager = new ModelManager(50);
308
+ mockSendModelValue.mockClear();
303
309
  });
304
310
 
305
311
  it("should set and get models", async () => {
306
- const model = new Model({ count: 0 }, vi.fn(), vi.fn(), new Set());
307
- modelManager.set("test-id", model);
308
- const retrievedModel = await modelManager.get("test-id");
312
+ const model = new Model({ count: 0 }, createMockComm());
313
+ modelManager.set(testId, model);
314
+ const retrievedModel = await modelManager.get(testId);
309
315
  expect(retrievedModel).toBe(model);
310
316
  });
311
317
 
312
318
  it("should handle model not found", async () => {
313
- await expect(modelManager.get("non-existent")).rejects.toThrow(
319
+ await expect(modelManager.get(asModelId("non-existent"))).rejects.toThrow(
314
320
  "Model not found for key: non-existent",
315
321
  );
316
322
  });
317
323
 
318
324
  it("should delete models", async () => {
319
- const model = new Model({ count: 0 }, vi.fn(), vi.fn(), new Set());
320
- modelManager.set("test-id", model);
321
- modelManager.delete("test-id");
322
- await expect(modelManager.get("test-id")).rejects.toThrow();
325
+ const model = new Model({ count: 0 }, createMockComm());
326
+ modelManager.set(testId, model);
327
+ modelManager.delete(testId);
328
+ await expect(modelManager.get(testId)).rejects.toThrow();
323
329
  });
324
330
 
325
331
  it("should handle widget messages", async () => {
326
- const openMessage: AnyWidgetMessage = {
327
- method: "open",
328
- state: { count: 0 },
329
- buffer_paths: [],
330
- };
331
-
332
- await handle({ modelId: "test-id", message: openMessage, buffers: [] });
333
- const model = await modelManager.get("test-id");
332
+ await handleWidgetMessage(modelManager, {
333
+ model_id: testId,
334
+ message: {
335
+ method: "open",
336
+ state: { count: 0 },
337
+ buffer_paths: [],
338
+ buffers: [],
339
+ },
340
+ });
341
+ const model = await modelManager.get(testId);
334
342
  expect(model.get("count")).toBe(0);
335
343
 
336
- const updateMessage: AnyWidgetMessage = {
337
- method: "update",
338
- state: {
339
- count: 1,
344
+ await handleWidgetMessage(modelManager, {
345
+ model_id: testId,
346
+ message: {
347
+ method: "update",
348
+ state: { count: 1 },
349
+ buffer_paths: [],
350
+ buffers: [],
340
351
  },
341
- buffer_paths: [],
342
- };
343
-
344
- await handle({ modelId: "test-id", message: updateMessage, buffers: [] });
352
+ });
345
353
  expect(model.get("count")).toBe(1);
346
354
  });
347
355
 
348
356
  it("should handle custom messages", async () => {
349
- const model = new Model({ count: 0 }, vi.fn(), vi.fn(), new Set());
357
+ const model = new Model({ count: 0 }, createMockComm());
350
358
  const callback = vi.fn();
351
359
  model.on("msg:custom", callback);
352
- modelManager.set("test-id", model);
360
+ modelManager.set(testId, model);
353
361
 
354
- await handle({
355
- modelId: "test-id",
356
- message: { method: "custom", content: { count: 1 } },
357
- buffers: [],
362
+ await handleWidgetMessage(modelManager, {
363
+ model_id: testId,
364
+ message: { method: "custom", content: { count: 1 }, buffers: [] },
358
365
  });
359
366
  expect(callback).toHaveBeenCalledWith({ count: 1 }, []);
360
367
  });
361
368
 
362
369
  it("should handle close messages", async () => {
363
- const model = new Model({ count: 0 }, vi.fn(), vi.fn(), new Set());
364
- modelManager.set("test-id", model);
370
+ const model = new Model({ count: 0 }, createMockComm());
371
+ modelManager.set(testId, model);
365
372
 
366
- await handle({
367
- modelId: "test-id",
373
+ await handleWidgetMessage(modelManager, {
374
+ model_id: testId,
368
375
  message: { method: "close" },
369
- buffers: [],
370
376
  });
371
- await expect(modelManager.get("test-id")).rejects.toThrow();
377
+ await expect(modelManager.get(testId)).rejects.toThrow();
372
378
  });
373
379
  });