@marimo-team/islands 0.19.8-dev41 → 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.
- package/dist/assets/__vite-browser-external-WSlCcXn_.js +1 -0
- package/dist/assets/worker-DUYMdbtA.js +73 -0
- package/dist/main.js +1740 -1691
- package/dist/style.css +1 -1
- package/dist/{useDeepCompareMemoize-CMGprt3H.js → useDeepCompareMemoize-BhZZsis0.js} +7 -3
- package/dist/{vega-component-DU3aSp4m.js → vega-component-DCxUyPnb.js} +1 -1
- package/package.json +1 -1
- package/src/components/app-config/optional-features.tsx +1 -1
- package/src/components/chat/__tests__/useFileState.test.tsx +93 -0
- package/src/components/chat/acp/agent-panel.tsx +26 -77
- package/src/components/chat/chat-components.tsx +114 -1
- package/src/components/chat/chat-panel.tsx +32 -104
- package/src/components/chat/chat-utils.ts +42 -0
- package/src/components/editor/ai/add-cell-with-ai.tsx +85 -53
- package/src/components/editor/ai/ai-completion-editor.tsx +15 -38
- package/src/components/editor/chrome/panels/packages-panel.tsx +12 -9
- package/src/core/islands/__tests__/bridge.test.ts +7 -2
- package/src/core/islands/bridge.ts +1 -1
- package/src/core/islands/main.ts +7 -0
- package/src/core/network/types.ts +2 -2
- package/src/core/wasm/bridge.ts +1 -1
- package/src/core/websocket/useMarimoKernelConnection.tsx +5 -15
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +86 -167
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +37 -123
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +128 -122
- package/src/{utils/__tests__/data-views.test.ts → plugins/impl/anywidget/__tests__/serialization.test.ts} +42 -96
- package/src/plugins/impl/anywidget/model.ts +348 -223
- package/src/plugins/impl/anywidget/schemas.ts +32 -0
- package/src/{utils/data-views.ts → plugins/impl/anywidget/serialization.ts} +13 -36
- package/src/plugins/impl/anywidget/types.ts +27 -0
- package/src/plugins/impl/chat/chat-ui.tsx +22 -20
- package/src/utils/Deferred.ts +21 -0
- package/src/utils/json/base64.ts +38 -8
- package/dist/assets/__vite-browser-external-6-UwTyQC.js +0 -1
- package/dist/assets/worker-D3e5wDxM.js +0 -73
|
@@ -1,65 +1,17 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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
|
-
|
|
14
|
-
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
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(
|
|
69
|
+
expect(mockWidget.render).toHaveBeenCalled();
|
|
147
70
|
});
|
|
148
71
|
});
|
|
149
72
|
|
|
150
|
-
it("should
|
|
73
|
+
it("should re-run widget when widget prop changes", async () => {
|
|
151
74
|
const { rerender } = render(<LoadedSlot {...mockProps} />);
|
|
152
75
|
|
|
153
|
-
// Wait
|
|
76
|
+
// Wait for initial render
|
|
154
77
|
await waitFor(() => {
|
|
155
78
|
expect(mockWidget.render).toHaveBeenCalled();
|
|
156
79
|
});
|
|
157
80
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
81
|
+
// Create a new widget mock
|
|
82
|
+
const newMockWidget = {
|
|
83
|
+
initialize: vi.fn(),
|
|
84
|
+
render: vi.fn(),
|
|
85
|
+
};
|
|
161
86
|
|
|
162
|
-
//
|
|
163
|
-
|
|
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
|
|
91
|
+
// Wait for re-render with new widget
|
|
178
92
|
await waitFor(() => {
|
|
179
|
-
expect(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
model = new Model(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
129
|
+
expect(mockComm.sendUpdate).toHaveBeenCalledWith({
|
|
92
130
|
bar: 456,
|
|
93
131
|
});
|
|
94
132
|
});
|
|
95
133
|
|
|
96
|
-
it("should not call
|
|
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
|
|
137
|
+
model.save_changes(); // Second save should not call sendUpdate
|
|
100
138
|
|
|
101
|
-
expect(
|
|
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(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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" },
|
|
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(
|
|
181
|
-
"
|
|
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
|
-
|
|
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({
|
|
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("
|
|
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.
|
|
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.
|
|
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
|
|
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 },
|
|
307
|
-
modelManager.set(
|
|
308
|
-
const retrievedModel = await modelManager.get(
|
|
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 },
|
|
320
|
-
modelManager.set(
|
|
321
|
-
modelManager.delete(
|
|
322
|
-
await expect(modelManager.get(
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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 },
|
|
357
|
+
const model = new Model({ count: 0 }, createMockComm());
|
|
350
358
|
const callback = vi.fn();
|
|
351
359
|
model.on("msg:custom", callback);
|
|
352
|
-
modelManager.set(
|
|
360
|
+
modelManager.set(testId, model);
|
|
353
361
|
|
|
354
|
-
await
|
|
355
|
-
|
|
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 },
|
|
364
|
-
modelManager.set(
|
|
370
|
+
const model = new Model({ count: 0 }, createMockComm());
|
|
371
|
+
modelManager.set(testId, model);
|
|
365
372
|
|
|
366
|
-
await
|
|
367
|
-
|
|
373
|
+
await handleWidgetMessage(modelManager, {
|
|
374
|
+
model_id: testId,
|
|
368
375
|
message: { method: "close" },
|
|
369
|
-
buffers: [],
|
|
370
376
|
});
|
|
371
|
-
await expect(modelManager.get(
|
|
377
|
+
await expect(modelManager.get(testId)).rejects.toThrow();
|
|
372
378
|
});
|
|
373
379
|
});
|