@marimo-team/frontend 0.19.8-dev3 → 0.19.8-dev5
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/package.json
CHANGED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
// Mock browser APIs before any imports
|
|
5
|
+
vi.stubGlobal(
|
|
6
|
+
"Worker",
|
|
7
|
+
vi.fn(() => ({
|
|
8
|
+
addEventListener: vi.fn(),
|
|
9
|
+
postMessage: vi.fn(),
|
|
10
|
+
terminate: vi.fn(),
|
|
11
|
+
})),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
// Create a mock URL class that works as a constructor
|
|
15
|
+
class MockURL {
|
|
16
|
+
href: string;
|
|
17
|
+
constructor(url: string, base?: string | URL) {
|
|
18
|
+
this.href = base ? `${base}/${url}` : url;
|
|
19
|
+
}
|
|
20
|
+
static createObjectURL = vi.fn(() => "blob:mock-url");
|
|
21
|
+
static revokeObjectURL = vi.fn();
|
|
22
|
+
}
|
|
23
|
+
vi.stubGlobal("URL", MockURL);
|
|
24
|
+
|
|
25
|
+
// Mock the worker RPC before importing the bridge
|
|
26
|
+
const mockBridge = vi.fn();
|
|
27
|
+
const mockLoadPackages = vi.fn();
|
|
28
|
+
|
|
29
|
+
vi.mock("@/core/wasm/rpc", () => ({
|
|
30
|
+
getWorkerRPC: () => ({
|
|
31
|
+
proxy: {
|
|
32
|
+
request: {
|
|
33
|
+
bridge: mockBridge,
|
|
34
|
+
loadPackages: mockLoadPackages,
|
|
35
|
+
startSession: vi.fn(),
|
|
36
|
+
},
|
|
37
|
+
send: {
|
|
38
|
+
consumerReady: vi.fn(),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
addMessageListener: vi.fn(),
|
|
42
|
+
}),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// Mock the parse module to avoid DOM dependencies
|
|
46
|
+
vi.mock("../parse", () => ({
|
|
47
|
+
parseMarimoIslandApps: () => [],
|
|
48
|
+
createMarimoFile: vi.fn(),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// Mock uuid to have predictable tokens
|
|
52
|
+
vi.mock("@/utils/uuid", () => ({
|
|
53
|
+
generateUUID: () => "test-uuid-12345",
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
// Mock getMarimoVersion
|
|
57
|
+
vi.mock("@/core/meta/globals", () => ({
|
|
58
|
+
getMarimoVersion: () => "0.0.0-test",
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
// Mock the jotai store
|
|
62
|
+
vi.mock("@/core/state/jotai", () => ({
|
|
63
|
+
store: {
|
|
64
|
+
set: vi.fn(),
|
|
65
|
+
},
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
// Now import the bridge class
|
|
69
|
+
import { IslandsPyodideBridge } from "../bridge";
|
|
70
|
+
|
|
71
|
+
describe("IslandsPyodideBridge", () => {
|
|
72
|
+
let bridge: IslandsPyodideBridge;
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
vi.clearAllMocks();
|
|
76
|
+
// Reset the singleton by clearing the window property
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
delete (window as any)._marimo_private_IslandsPyodideBridge;
|
|
79
|
+
// Access the singleton - creates a fresh instance
|
|
80
|
+
bridge = IslandsPyodideBridge.INSTANCE;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
// Clean up singleton
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
delete (window as any)._marimo_private_IslandsPyodideBridge;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("sendComponentValues", () => {
|
|
90
|
+
it("should include type field and token in control request", async () => {
|
|
91
|
+
const request = {
|
|
92
|
+
objectIds: ["Hbol-0"],
|
|
93
|
+
values: [58],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
await bridge.sendComponentValues(request);
|
|
97
|
+
|
|
98
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
99
|
+
functionName: "put_control_request",
|
|
100
|
+
payload: {
|
|
101
|
+
type: "update-ui-element",
|
|
102
|
+
objectIds: ["Hbol-0"],
|
|
103
|
+
values: [58],
|
|
104
|
+
token: "test-uuid-12345",
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should preserve all request properties", async () => {
|
|
110
|
+
const request = {
|
|
111
|
+
objectIds: ["slider-1", "slider-2"],
|
|
112
|
+
values: [10, 20],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
await bridge.sendComponentValues(request);
|
|
116
|
+
|
|
117
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
118
|
+
functionName: "put_control_request",
|
|
119
|
+
payload: expect.objectContaining({
|
|
120
|
+
type: "update-ui-element",
|
|
121
|
+
objectIds: ["slider-1", "slider-2"],
|
|
122
|
+
values: [10, 20],
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("sendFunctionRequest", () => {
|
|
129
|
+
it("should include type field in control request", async () => {
|
|
130
|
+
const request = {
|
|
131
|
+
functionCallId: "call-123",
|
|
132
|
+
namespace: "test_namespace",
|
|
133
|
+
functionName: "my_function",
|
|
134
|
+
args: { x: 1, y: 2 },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
await bridge.sendFunctionRequest(request);
|
|
138
|
+
|
|
139
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
140
|
+
functionName: "put_control_request",
|
|
141
|
+
payload: {
|
|
142
|
+
type: "invoke-function",
|
|
143
|
+
functionCallId: "call-123",
|
|
144
|
+
namespace: "test_namespace",
|
|
145
|
+
functionName: "my_function",
|
|
146
|
+
args: { x: 1, y: 2 },
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("sendRun", () => {
|
|
153
|
+
it("should include type field in control request", async () => {
|
|
154
|
+
const request = {
|
|
155
|
+
cellIds: ["cell-1", "cell-2"],
|
|
156
|
+
codes: ["print('hello')", "print('world')"],
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
await bridge.sendRun(request);
|
|
160
|
+
|
|
161
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
162
|
+
functionName: "put_control_request",
|
|
163
|
+
payload: {
|
|
164
|
+
type: "execute-cells",
|
|
165
|
+
cellIds: ["cell-1", "cell-2"],
|
|
166
|
+
codes: ["print('hello')", "print('world')"],
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should call loadPackages before putControlRequest", async () => {
|
|
172
|
+
const request = {
|
|
173
|
+
cellIds: ["cell-1"],
|
|
174
|
+
codes: ["import pandas"],
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
await bridge.sendRun(request);
|
|
178
|
+
|
|
179
|
+
// Verify loadPackages was called with joined codes
|
|
180
|
+
expect(mockLoadPackages).toHaveBeenCalledWith("import pandas");
|
|
181
|
+
|
|
182
|
+
// Verify order: loadPackages should be called before bridge
|
|
183
|
+
const loadPackagesCallOrder =
|
|
184
|
+
mockLoadPackages.mock.invocationCallOrder[0];
|
|
185
|
+
const bridgeCallOrder = mockBridge.mock.invocationCallOrder[0];
|
|
186
|
+
expect(loadPackagesCallOrder).toBeLessThan(bridgeCallOrder);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("sendModelValue", () => {
|
|
191
|
+
it("should include type field in control request", async () => {
|
|
192
|
+
const request = {
|
|
193
|
+
modelId: "widget-1",
|
|
194
|
+
message: {
|
|
195
|
+
state: { value: 42 },
|
|
196
|
+
bufferPaths: [],
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
await bridge.sendModelValue(request);
|
|
201
|
+
|
|
202
|
+
expect(mockBridge).toHaveBeenCalledWith({
|
|
203
|
+
functionName: "put_control_request",
|
|
204
|
+
payload: {
|
|
205
|
+
type: "update-widget-model",
|
|
206
|
+
modelId: "widget-1",
|
|
207
|
+
message: {
|
|
208
|
+
state: { value: 42 },
|
|
209
|
+
bufferPaths: [],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("control request message format", () => {
|
|
217
|
+
it("should always include the type field required by msgspec", async () => {
|
|
218
|
+
// Test all methods to ensure they include the type field
|
|
219
|
+
await bridge.sendComponentValues({ objectIds: [], values: [] });
|
|
220
|
+
await bridge.sendFunctionRequest({
|
|
221
|
+
functionCallId: "",
|
|
222
|
+
namespace: "",
|
|
223
|
+
functionName: "",
|
|
224
|
+
args: {},
|
|
225
|
+
});
|
|
226
|
+
await bridge.sendRun({ cellIds: [], codes: [] });
|
|
227
|
+
await bridge.sendModelValue({
|
|
228
|
+
modelId: "",
|
|
229
|
+
message: { state: {}, bufferPaths: [] },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// All calls should have the type field
|
|
233
|
+
const allCalls = mockBridge.mock.calls;
|
|
234
|
+
for (const call of allCalls) {
|
|
235
|
+
const payload = call[0].payload;
|
|
236
|
+
expect(payload).toHaveProperty("type");
|
|
237
|
+
expect(typeof payload.type).toBe("string");
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -6,7 +6,8 @@ import { Deferred } from "@/utils/Deferred";
|
|
|
6
6
|
import { throwNotImplemented } from "@/utils/functions";
|
|
7
7
|
import type { JsonString } from "@/utils/json/base64";
|
|
8
8
|
import { Logger } from "@/utils/Logger";
|
|
9
|
-
import
|
|
9
|
+
import { generateUUID } from "@/utils/uuid";
|
|
10
|
+
import type { CommandMessage, NotificationPayload } from "../kernel/messages";
|
|
10
11
|
import { getMarimoVersion } from "../meta/globals";
|
|
11
12
|
import type { EditRequests, RunRequests } from "../network/types";
|
|
12
13
|
import { store } from "../state/jotai";
|
|
@@ -104,7 +105,11 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
104
105
|
sendComponentValues: RunRequests["sendComponentValues"] = async (
|
|
105
106
|
request,
|
|
106
107
|
): Promise<null> => {
|
|
107
|
-
await this.putControlRequest(
|
|
108
|
+
await this.putControlRequest({
|
|
109
|
+
type: "update-ui-element",
|
|
110
|
+
...request,
|
|
111
|
+
token: generateUUID(),
|
|
112
|
+
});
|
|
108
113
|
return null;
|
|
109
114
|
};
|
|
110
115
|
|
|
@@ -117,18 +122,27 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
117
122
|
sendFunctionRequest: RunRequests["sendFunctionRequest"] = async (
|
|
118
123
|
request,
|
|
119
124
|
): Promise<null> => {
|
|
120
|
-
await this.putControlRequest(
|
|
125
|
+
await this.putControlRequest({
|
|
126
|
+
type: "invoke-function",
|
|
127
|
+
...request,
|
|
128
|
+
});
|
|
121
129
|
return null;
|
|
122
130
|
};
|
|
123
131
|
|
|
124
132
|
sendRun: EditRequests["sendRun"] = async (request): Promise<null> => {
|
|
125
133
|
await this.rpc.proxy.request.loadPackages(request.codes.join("\n"));
|
|
126
|
-
await this.putControlRequest(
|
|
134
|
+
await this.putControlRequest({
|
|
135
|
+
type: "execute-cells",
|
|
136
|
+
...request,
|
|
137
|
+
});
|
|
127
138
|
return null;
|
|
128
139
|
};
|
|
129
140
|
|
|
130
141
|
sendModelValue: RunRequests["sendModelValue"] = async (request) => {
|
|
131
|
-
await this.putControlRequest(
|
|
142
|
+
await this.putControlRequest({
|
|
143
|
+
type: "update-widget-model",
|
|
144
|
+
...request,
|
|
145
|
+
});
|
|
132
146
|
return null;
|
|
133
147
|
};
|
|
134
148
|
|
|
@@ -187,7 +201,9 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
187
201
|
clearCache = throwNotImplemented;
|
|
188
202
|
getCacheInfo = throwNotImplemented;
|
|
189
203
|
|
|
190
|
-
|
|
204
|
+
// The kernel uses msgspec to parse control requests, which requires a 'type'
|
|
205
|
+
// field for discriminated union deserialization.
|
|
206
|
+
private async putControlRequest(operation: CommandMessage) {
|
|
191
207
|
await this.rpc.proxy.request.bridge({
|
|
192
208
|
functionName: "put_control_request",
|
|
193
209
|
payload: operation,
|