@marimo-team/islands 0.23.14-dev6 → 0.23.14-dev61
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-Du3_nUzI.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
- package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
- package/dist/assets/{worker-DEDLIQQV.js → worker-DAWRHcPq.js} +2 -2
- package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DinOvbWu.js} +3248 -3044
- package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
- package/dist/{code-visibility-C90Am97q.js → code-visibility-EvXXzjfW.js} +1169 -960
- package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
- package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
- package/dist/{formats-d6MhLuQ9.js → formats-Dzx4J_z1.js} +1 -1
- package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
- package/dist/{html-to-image-CGp_08St.js → html-to-image-_wGfk8V-.js} +2389 -2322
- package/dist/{input-CbEz_aj_.js → input-DtsN7xm-.js} +1 -1
- package/dist/main.js +3630 -1801
- package/dist/{mermaid-CJW9vIyO.js → mermaid-BYqXy_NE.js} +2 -2
- package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
- package/dist/{process-output-R6JsYrv3.js → process-output-Mh4UrjwM.js} +1 -1
- package/dist/{reveal-component-C_u9FY4_.js → reveal-component-CKfV5wlk.js} +600 -596
- package/dist/{spec-Bv-XlYiv.js → spec-Cz-Bj1JI.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-D-l5s8nn.js → toDate-CWNNlFEX.js} +15 -7
- package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-KfHB8wQR.js} +1 -1
- package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
- package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-DegSo0lV.js} +1 -1
- package/dist/{useTheme-BrYvK-_A.js → useTheme-6eZ3GOTS.js} +31 -26
- package/dist/{vega-component-Pk6lyc_a.js → vega-component-DzyyM9fc.js} +5 -5
- package/package.json +4 -4
- package/src/__mocks__/requests.ts +1 -0
- package/src/__tests__/CellStatus.test.tsx +1 -2
- package/src/components/app-config/user-config-form.tsx +52 -0
- package/src/components/chat/acp/agent-panel.tsx +35 -1
- package/src/components/chat/chat-panel.tsx +68 -29
- package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
- package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
- package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
- package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
- package/src/components/data-table/charts/charts.tsx +23 -1
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
- package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
- package/src/components/debugger/debugger-code.tsx +7 -2
- package/src/components/editor/actions/useNotebookActions.tsx +2 -2
- package/src/components/editor/cell/CellStatus.tsx +1 -20
- package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
- package/src/components/editor/cell/cell-context-menu.tsx +2 -2
- package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
- package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
- package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
- package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
- package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
- package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
- package/src/components/editor/code/readonly-python-code.tsx +60 -33
- package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
- package/src/components/editor/errors/auto-fix.tsx +88 -34
- package/src/components/editor/errors/fix-mode.ts +1 -1
- package/src/components/editor/notebook-cell.tsx +7 -0
- package/src/components/editor/output/ImageOutput.tsx +20 -6
- package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
- package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
- package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
- package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
- package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
- package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
- package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
- package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
- package/src/components/slides/__tests__/slide.test.tsx +33 -0
- package/src/components/slides/minimap.tsx +7 -1
- package/src/components/slides/reveal-component.tsx +3 -0
- package/src/components/slides/slide-cell-view.tsx +10 -6
- package/src/components/slides/slide.tsx +16 -13
- package/src/components/tracing/tracing.tsx +2 -1
- package/src/core/ai/config.ts +2 -2
- package/src/core/ai/context/__tests__/registry.test.ts +133 -3
- package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
- package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
- package/src/core/ai/context/providers/datasource.ts +27 -2
- package/src/core/ai/context/providers/error.ts +226 -36
- package/src/core/ai/context/registry.ts +77 -43
- package/src/core/ai/state.ts +11 -0
- package/src/core/cells/__tests__/cell.test.ts +39 -0
- package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
- package/src/core/cells/cell.ts +5 -3
- package/src/core/cells/readonly-code-display.ts +35 -0
- package/src/core/codemirror/__tests__/setup.test.ts +33 -1
- package/src/core/codemirror/ai/resources.ts +15 -10
- package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
- package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +224 -0
- package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
- package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
- package/src/core/codemirror/cells/debugger-state.ts +133 -0
- package/src/core/codemirror/cells/extensions.ts +32 -4
- package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
- package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
- package/src/core/codemirror/go-to-definition/extension.ts +3 -3
- package/src/core/codemirror/go-to-definition/underline.ts +5 -14
- package/src/core/codemirror/go-to-definition/utils.ts +49 -1
- package/src/core/codemirror/utils.ts +15 -0
- package/src/core/config/__tests__/config-schema.test.ts +17 -0
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/config/config.ts +10 -0
- package/src/core/config/feature-flag.tsx +4 -0
- package/src/core/edit-app.tsx +8 -5
- package/src/core/errors/errors.ts +2 -1
- package/src/core/islands/bootstrap.ts +4 -3
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
- package/src/core/lifecycle/render-policy.ts +125 -0
- package/src/core/mime.ts +11 -4
- package/src/core/network/__tests__/api.test.ts +17 -0
- package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
- package/src/core/network/api.ts +17 -6
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +8 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +10 -1
- package/src/core/network/types.ts +2 -0
- package/src/core/run-app.tsx +27 -23
- package/src/core/runtime/__tests__/adapter.test.ts +160 -0
- package/src/core/runtime/__tests__/runtime.test.ts +45 -0
- package/src/core/runtime/adapter.ts +182 -0
- package/src/core/runtime/runtime.ts +30 -16
- package/src/core/wasm/PyodideLoader.tsx +20 -62
- package/src/core/wasm/bridge.ts +13 -4
- package/src/core/wasm/state.ts +8 -19
- package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +4 -0
- package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
- package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
- package/src/core/websocket/transports/sse.ts +308 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
- package/src/core/websocket/useWebSocket.tsx +12 -2
- package/src/css/app/codemirror.css +14 -0
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
- package/src/hooks/useDelayElapsed.ts +27 -0
- package/src/mount.tsx +3 -3
- package/src/plugins/core/RenderHTML.tsx +35 -1
- package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
- package/src/plugins/impl/DataTablePlugin.tsx +1 -0
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -254
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
- package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
- package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
- package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
- package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
- package/src/plugins/impl/anywidget/host.ts +54 -0
- package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
- package/src/plugins/impl/anywidget/model.ts +59 -239
- package/src/plugins/impl/anywidget/registry.ts +268 -0
- package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
- package/src/plugins/impl/anywidget/runtime.ts +422 -0
- package/src/plugins/impl/anywidget/types.ts +14 -0
- package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
- package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
- package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
- package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
- package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
- package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
- package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
- package/src/utils/errors.ts +15 -0
- package/src/utils/time.ts +20 -0
- package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
- package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
- package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
- package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
- package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
- package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
- package/src/core/wasm/__tests__/state.test.ts +0 -124
- package/src/plugins/impl/anywidget/schemas.ts +0 -32
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
it,
|
|
8
|
+
type MockInstance,
|
|
9
|
+
vi,
|
|
10
|
+
} from "vitest";
|
|
11
|
+
import { getMarimoInternal, Model } from "../model";
|
|
12
|
+
import { handleWidgetMessage, WidgetRegistry } from "../registry";
|
|
13
|
+
import type { EsmSpec, ModelState, WidgetModelId } from "../types";
|
|
14
|
+
import { WIDGET_DEF_REGISTRY } from "../widget-binding";
|
|
15
|
+
|
|
16
|
+
// Helper to create typed model IDs for tests
|
|
17
|
+
const asModelId = (id: string): WidgetModelId => id as WidgetModelId;
|
|
18
|
+
|
|
19
|
+
// Mock the request client
|
|
20
|
+
const mockSendModelValue = vi.fn().mockResolvedValue(null);
|
|
21
|
+
vi.mock("@/core/network/requests", () => ({
|
|
22
|
+
getRequestClient: () => ({
|
|
23
|
+
sendModelValue: mockSendModelValue,
|
|
24
|
+
}),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// Mock isStaticNotebook — default to false (normal mode)
|
|
28
|
+
const mockIsStatic = vi.fn().mockReturnValue(false);
|
|
29
|
+
vi.mock("@/core/static/static-state", () => ({
|
|
30
|
+
isStaticNotebook: () => mockIsStatic(),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
function createMockComm() {
|
|
34
|
+
return {
|
|
35
|
+
sendUpdate: vi.fn().mockResolvedValue(undefined),
|
|
36
|
+
sendCustomMessage: vi.fn().mockResolvedValue(undefined),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const SPEC: EsmSpec = { url: "./@file/10-widget.js", hash: "hash-1" };
|
|
41
|
+
|
|
42
|
+
describe("WidgetRegistry models", () => {
|
|
43
|
+
let registry = new WidgetRegistry(50);
|
|
44
|
+
const testId = asModelId("test-id");
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
registry = new WidgetRegistry(50);
|
|
48
|
+
mockSendModelValue.mockClear();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should set and get models", async () => {
|
|
52
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
53
|
+
registry.setModel(testId, model);
|
|
54
|
+
const retrievedModel = await registry.getModel(testId);
|
|
55
|
+
expect(retrievedModel).toBe(model);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should handle model not found", async () => {
|
|
59
|
+
await expect(registry.getModel(asModelId("non-existent"))).rejects.toThrow(
|
|
60
|
+
"Model not found for key: non-existent",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("accepts a fresh model after an earlier rendezvous times out", async () => {
|
|
65
|
+
const id = asModelId("late-model");
|
|
66
|
+
await expect(registry.getModel(id)).rejects.toThrow(
|
|
67
|
+
"Model not found for key: late-model",
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
71
|
+
registry.setModel(id, model);
|
|
72
|
+
|
|
73
|
+
await expect(registry.getModel(id)).resolves.toBe(model);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("disposes a pre-open view when model rendezvous times out", async () => {
|
|
77
|
+
const id = asModelId("timed-out-view");
|
|
78
|
+
const host = document.createElement("div");
|
|
79
|
+
const root = host.attachShadow({ mode: "open" });
|
|
80
|
+
const el = document.createElement("div");
|
|
81
|
+
root.append(el);
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
|
|
84
|
+
const pending = registry.createView({
|
|
85
|
+
modelId: id,
|
|
86
|
+
el,
|
|
87
|
+
signal: controller.signal,
|
|
88
|
+
});
|
|
89
|
+
expect(root.querySelector("style")).not.toBeNull();
|
|
90
|
+
|
|
91
|
+
await expect(pending).rejects.toThrow(
|
|
92
|
+
"Model not found for key: timed-out-view",
|
|
93
|
+
);
|
|
94
|
+
expect(root.querySelector("style")).toBeNull();
|
|
95
|
+
expect(controller.signal.aborted).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should delete models", async () => {
|
|
99
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
100
|
+
registry.setModel(testId, model);
|
|
101
|
+
registry.delete(testId);
|
|
102
|
+
await expect(registry.getModel(testId)).rejects.toThrow();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should handle widget messages", async () => {
|
|
106
|
+
await handleWidgetMessage(registry, {
|
|
107
|
+
model_id: testId,
|
|
108
|
+
message: {
|
|
109
|
+
method: "open",
|
|
110
|
+
state: { count: 0 },
|
|
111
|
+
buffer_paths: [],
|
|
112
|
+
buffers: [],
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const model = await registry.getModel(testId);
|
|
116
|
+
expect(model.get("count")).toBe(0);
|
|
117
|
+
|
|
118
|
+
await handleWidgetMessage(registry, {
|
|
119
|
+
model_id: testId,
|
|
120
|
+
message: {
|
|
121
|
+
method: "update",
|
|
122
|
+
state: { count: 1 },
|
|
123
|
+
buffer_paths: [],
|
|
124
|
+
buffers: [],
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
expect(model.get("count")).toBe(1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should handle custom messages", async () => {
|
|
131
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
132
|
+
const callback = vi.fn();
|
|
133
|
+
model.on("msg:custom", callback);
|
|
134
|
+
registry.setModel(testId, model);
|
|
135
|
+
|
|
136
|
+
await handleWidgetMessage(registry, {
|
|
137
|
+
model_id: testId,
|
|
138
|
+
message: { method: "custom", content: { count: 1 }, buffers: [] },
|
|
139
|
+
});
|
|
140
|
+
expect(callback).toHaveBeenCalledWith({ count: 1 }, []);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should handle close messages", async () => {
|
|
144
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
145
|
+
registry.setModel(testId, model);
|
|
146
|
+
|
|
147
|
+
await handleWidgetMessage(registry, {
|
|
148
|
+
model_id: testId,
|
|
149
|
+
message: { method: "close" },
|
|
150
|
+
});
|
|
151
|
+
await expect(registry.getModel(testId)).rejects.toThrow();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("static mode", () => {
|
|
155
|
+
beforeEach(() => {
|
|
156
|
+
mockIsStatic.mockReturnValue(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
afterEach(() => {
|
|
160
|
+
mockIsStatic.mockReturnValue(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should create model with no-op comm in static mode", async () => {
|
|
164
|
+
await handleWidgetMessage(registry, {
|
|
165
|
+
model_id: testId,
|
|
166
|
+
message: {
|
|
167
|
+
method: "open",
|
|
168
|
+
state: { count: 42 },
|
|
169
|
+
buffer_paths: [],
|
|
170
|
+
buffers: [],
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const model = await registry.getModel(testId);
|
|
175
|
+
expect(model.get("count")).toBe(42);
|
|
176
|
+
|
|
177
|
+
// save_changes should not call the real request client
|
|
178
|
+
model.set("count", 100);
|
|
179
|
+
model.save_changes();
|
|
180
|
+
expect(mockSendModelValue).not.toHaveBeenCalled();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should not throw on send in static mode", async () => {
|
|
184
|
+
await handleWidgetMessage(registry, {
|
|
185
|
+
model_id: testId,
|
|
186
|
+
message: {
|
|
187
|
+
method: "open",
|
|
188
|
+
state: { count: 0 },
|
|
189
|
+
buffer_paths: [],
|
|
190
|
+
buffers: [],
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const model = await registry.getModel(testId);
|
|
195
|
+
// send() should silently no-op
|
|
196
|
+
await expect(model.send({ test: true })).resolves.toBeUndefined();
|
|
197
|
+
expect(mockSendModelValue).not.toHaveBeenCalled();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("WidgetRegistry.getWidget", () => {
|
|
203
|
+
let registry = new WidgetRegistry(50);
|
|
204
|
+
const testId = asModelId("widget-id");
|
|
205
|
+
let getModuleSpy: MockInstance<typeof WIDGET_DEF_REGISTRY.getModule>;
|
|
206
|
+
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
registry = new WidgetRegistry(50);
|
|
209
|
+
getModuleSpy = vi.spyOn(WIDGET_DEF_REGISTRY, "getModule");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
afterEach(() => {
|
|
213
|
+
getModuleSpy.mockRestore();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("binds an undisplayed widget from its ESM spec", async () => {
|
|
217
|
+
// The composition case: the child's model and spec arrived on its
|
|
218
|
+
// open message, but no display mount ever renders it standalone.
|
|
219
|
+
// getWidget must self-serve from the spec.
|
|
220
|
+
const exports = { hello: vi.fn() };
|
|
221
|
+
const widgetDef = {
|
|
222
|
+
initialize: vi.fn().mockResolvedValue(exports),
|
|
223
|
+
render: vi.fn(),
|
|
224
|
+
};
|
|
225
|
+
getModuleSpy.mockResolvedValue({ default: widgetDef });
|
|
226
|
+
|
|
227
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
228
|
+
registry.setModel(testId, model);
|
|
229
|
+
registry.setSpec(testId, SPEC);
|
|
230
|
+
|
|
231
|
+
const widget = await registry.getWidget(testId);
|
|
232
|
+
|
|
233
|
+
expect(getModuleSpy).toHaveBeenCalledWith({
|
|
234
|
+
jsUrl: SPEC.url,
|
|
235
|
+
jsHash: SPEC.hash,
|
|
236
|
+
kernelAuthored: true,
|
|
237
|
+
});
|
|
238
|
+
expect(widgetDef.initialize).toHaveBeenCalledTimes(1);
|
|
239
|
+
expect(widget.exports).toBe(exports);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("records the spec from an open message", async () => {
|
|
243
|
+
const widgetDef = { initialize: vi.fn(), render: vi.fn() };
|
|
244
|
+
getModuleSpy.mockResolvedValue({ default: widgetDef });
|
|
245
|
+
|
|
246
|
+
await handleWidgetMessage(registry, {
|
|
247
|
+
model_id: testId,
|
|
248
|
+
message: {
|
|
249
|
+
method: "open",
|
|
250
|
+
state: { count: 0 },
|
|
251
|
+
buffer_paths: [],
|
|
252
|
+
buffers: [],
|
|
253
|
+
esm_spec: { url: SPEC.url, hash: SPEC.hash },
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await registry.getWidget(testId);
|
|
258
|
+
expect(getModuleSpy).toHaveBeenCalledWith({
|
|
259
|
+
jsUrl: SPEC.url,
|
|
260
|
+
jsHash: SPEC.hash,
|
|
261
|
+
kernelAuthored: true,
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("shares one generation across concurrent and repeat callers", async () => {
|
|
266
|
+
const widgetDef = { initialize: vi.fn(), render: vi.fn() };
|
|
267
|
+
getModuleSpy.mockResolvedValue({ default: widgetDef });
|
|
268
|
+
|
|
269
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
270
|
+
registry.setModel(testId, model);
|
|
271
|
+
registry.setSpec(testId, SPEC);
|
|
272
|
+
|
|
273
|
+
const [a, b] = await Promise.all([
|
|
274
|
+
registry.getWidget(testId),
|
|
275
|
+
registry.getWidget(testId),
|
|
276
|
+
]);
|
|
277
|
+
const c = await registry.getWidget(testId);
|
|
278
|
+
expect(a.exports).toBe(b.exports);
|
|
279
|
+
expect(a.exports).toBe(c.exports);
|
|
280
|
+
expect(widgetDef.initialize).toHaveBeenCalledTimes(1);
|
|
281
|
+
expect(getModuleSpy).toHaveBeenCalledTimes(1);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("fails fast for a model with no ESM spec", async () => {
|
|
285
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
286
|
+
registry.setModel(testId, model);
|
|
287
|
+
|
|
288
|
+
await expect(registry.getWidget(testId)).rejects.toThrow(
|
|
289
|
+
/No ESM spec for model/,
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("rejects when the module import fails, then retries fresh", async () => {
|
|
294
|
+
getModuleSpy.mockRejectedValueOnce(new Error("network down"));
|
|
295
|
+
|
|
296
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
297
|
+
registry.setModel(testId, model);
|
|
298
|
+
registry.setSpec(testId, SPEC);
|
|
299
|
+
|
|
300
|
+
await expect(registry.getWidget(testId)).rejects.toThrow("network down");
|
|
301
|
+
|
|
302
|
+
// A failed generation must not poison the entry: the next call
|
|
303
|
+
// starts over from the spec.
|
|
304
|
+
const widgetDef = { initialize: vi.fn(), render: vi.fn() };
|
|
305
|
+
getModuleSpy.mockResolvedValue({ default: widgetDef });
|
|
306
|
+
const widget = await registry.getWidget(testId);
|
|
307
|
+
expect(widget.exports).toBeUndefined();
|
|
308
|
+
expect(widgetDef.initialize).toHaveBeenCalledTimes(1);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("rejects with the AFM error when the module has no usable exports", async () => {
|
|
312
|
+
getModuleSpy.mockResolvedValue({ notAWidget: true });
|
|
313
|
+
|
|
314
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
315
|
+
registry.setModel(testId, model);
|
|
316
|
+
registry.setSpec(testId, SPEC);
|
|
317
|
+
|
|
318
|
+
await expect(registry.getWidget(testId)).rejects.toThrow(
|
|
319
|
+
/missing a default export/,
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("rejects when the model never arrives", async () => {
|
|
324
|
+
await expect(registry.getWidget(asModelId("never-opened"))).rejects.toThrow(
|
|
325
|
+
"Model not found for key: never-opened",
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("rejects an in-flight initialize when the entry is deleted", async () => {
|
|
330
|
+
const widgetDef = {
|
|
331
|
+
initialize: vi.fn().mockReturnValue(new Promise(() => undefined)),
|
|
332
|
+
render: vi.fn(),
|
|
333
|
+
};
|
|
334
|
+
getModuleSpy.mockResolvedValue({ default: widgetDef });
|
|
335
|
+
|
|
336
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
337
|
+
registry.setModel(testId, model);
|
|
338
|
+
registry.setSpec(testId, SPEC);
|
|
339
|
+
|
|
340
|
+
const pending = registry.getWidget(testId);
|
|
341
|
+
pending.catch(() => undefined);
|
|
342
|
+
// Let the import resolve and initialize start.
|
|
343
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
344
|
+
|
|
345
|
+
registry.delete(testId);
|
|
346
|
+
await expect(pending).rejects.toThrow(/binding destroyed/);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("destroys the generation on a close message", async () => {
|
|
350
|
+
const initCleanup = vi.fn();
|
|
351
|
+
const widgetDef = {
|
|
352
|
+
initialize: vi.fn().mockResolvedValue(initCleanup),
|
|
353
|
+
render: vi.fn(),
|
|
354
|
+
};
|
|
355
|
+
getModuleSpy.mockResolvedValue({ default: widgetDef });
|
|
356
|
+
|
|
357
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
358
|
+
registry.setModel(testId, model);
|
|
359
|
+
registry.setSpec(testId, SPEC);
|
|
360
|
+
await registry.getWidget(testId);
|
|
361
|
+
|
|
362
|
+
await handleWidgetMessage(registry, {
|
|
363
|
+
model_id: testId,
|
|
364
|
+
message: { method: "close" },
|
|
365
|
+
});
|
|
366
|
+
expect(initCleanup).toHaveBeenCalledTimes(1);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe("WidgetRegistry generation swap (hot reload)", () => {
|
|
371
|
+
const testId = asModelId("hmr-id");
|
|
372
|
+
const SPEC_V1: EsmSpec = { url: "./@file/10-v1.js", hash: "hash-v1" };
|
|
373
|
+
const SPEC_V2: EsmSpec = { url: "./@file/10-v2.js", hash: "hash-v2" };
|
|
374
|
+
let getModuleSpy: MockInstance<typeof WIDGET_DEF_REGISTRY.getModule>;
|
|
375
|
+
|
|
376
|
+
const makeWidget = (label: string) => ({
|
|
377
|
+
initialize: vi.fn(),
|
|
378
|
+
render: vi.fn(({ el }: { el: HTMLElement }) => {
|
|
379
|
+
el.textContent = label;
|
|
380
|
+
}),
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
beforeEach(() => {
|
|
384
|
+
getModuleSpy = vi.spyOn(WIDGET_DEF_REGISTRY, "getModule");
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
afterEach(() => {
|
|
388
|
+
getModuleSpy.mockRestore();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
async function setup(isEditMode: boolean) {
|
|
392
|
+
const registry = new WidgetRegistry(50, () => isEditMode);
|
|
393
|
+
const v1 = makeWidget("v1");
|
|
394
|
+
const v2 = makeWidget("v2");
|
|
395
|
+
getModuleSpy.mockImplementation(async ({ jsUrl }) => {
|
|
396
|
+
return jsUrl === SPEC_V1.url ? { default: v1 } : { default: v2 };
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
await handleWidgetMessage(registry, {
|
|
400
|
+
model_id: testId,
|
|
401
|
+
message: {
|
|
402
|
+
method: "open",
|
|
403
|
+
state: { count: 3 },
|
|
404
|
+
buffer_paths: [],
|
|
405
|
+
buffers: [],
|
|
406
|
+
esm_spec: { url: SPEC_V1.url, hash: SPEC_V1.hash },
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
const model = await registry.getModel(testId);
|
|
410
|
+
const widget = await registry.getWidget(testId);
|
|
411
|
+
|
|
412
|
+
// Mount a live view the swap must re-render.
|
|
413
|
+
const el = document.createElement("div");
|
|
414
|
+
const viewController = new AbortController();
|
|
415
|
+
await widget.render({ el, signal: viewController.signal });
|
|
416
|
+
expect(el.textContent).toBe("v1");
|
|
417
|
+
|
|
418
|
+
return { registry, model, widget, el, viewController, v1, v2 };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
it("preserves the initial spec before first mount outside edit mode", async () => {
|
|
422
|
+
const registry = new WidgetRegistry(50, () => false);
|
|
423
|
+
const v1 = makeWidget("v1");
|
|
424
|
+
const v2 = makeWidget("v2");
|
|
425
|
+
getModuleSpy.mockImplementation(async ({ jsUrl }) => ({
|
|
426
|
+
default: jsUrl === SPEC_V1.url ? v1 : v2,
|
|
427
|
+
}));
|
|
428
|
+
registry.setModel(
|
|
429
|
+
testId,
|
|
430
|
+
new Model<ModelState>({ count: 0 }, createMockComm()),
|
|
431
|
+
);
|
|
432
|
+
registry.setSpec(testId, SPEC_V1);
|
|
433
|
+
|
|
434
|
+
// A viewer may receive an editor's hot-reload update before it ever
|
|
435
|
+
// mounts this widget. Its code must still be immutable for the model.
|
|
436
|
+
registry.setSpec(testId, SPEC_V2);
|
|
437
|
+
const widget = await registry.getWidget(testId);
|
|
438
|
+
const el = document.createElement("div");
|
|
439
|
+
await widget.render({ el });
|
|
440
|
+
|
|
441
|
+
expect(el.textContent).toBe("v1");
|
|
442
|
+
expect(v1.initialize).toHaveBeenCalledTimes(1);
|
|
443
|
+
expect(v2.initialize).not.toHaveBeenCalled();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("swaps the generation and re-renders live views in edit mode", async () => {
|
|
447
|
+
const { registry, model, el, v1, v2 } = await setup(true);
|
|
448
|
+
|
|
449
|
+
const onCount = vi.fn();
|
|
450
|
+
model.on("change:count", onCount);
|
|
451
|
+
|
|
452
|
+
await handleWidgetMessage(registry, {
|
|
453
|
+
model_id: testId,
|
|
454
|
+
message: {
|
|
455
|
+
method: "update",
|
|
456
|
+
state: {},
|
|
457
|
+
buffer_paths: [],
|
|
458
|
+
buffers: [],
|
|
459
|
+
esm_spec: { url: SPEC_V2.url, hash: SPEC_V2.hash },
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const next = await registry.getWidget(testId);
|
|
464
|
+
// Wait for the swap's re-render to land in the DOM.
|
|
465
|
+
await vi.waitFor(() => {
|
|
466
|
+
expect(el.textContent).toBe("v2");
|
|
467
|
+
});
|
|
468
|
+
expect(v2.initialize).toHaveBeenCalledTimes(1);
|
|
469
|
+
expect(v2.render).toHaveBeenCalledTimes(1);
|
|
470
|
+
// The old generation rendered exactly once, before the swap.
|
|
471
|
+
expect(v1.render).toHaveBeenCalledTimes(1);
|
|
472
|
+
// Model state persists across generations — that's the "hot" part.
|
|
473
|
+
expect(next.exports).toBeUndefined();
|
|
474
|
+
expect(model.get("count")).toBe(3);
|
|
475
|
+
// Listeners registered outside the binding survive.
|
|
476
|
+
model.set("count", 4);
|
|
477
|
+
expect(onCount).toHaveBeenCalledTimes(1);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("ignores replacement specs outside edit mode", async () => {
|
|
481
|
+
const { registry, el, v2 } = await setup(false);
|
|
482
|
+
|
|
483
|
+
await handleWidgetMessage(registry, {
|
|
484
|
+
model_id: testId,
|
|
485
|
+
message: {
|
|
486
|
+
method: "update",
|
|
487
|
+
state: {},
|
|
488
|
+
buffer_paths: [],
|
|
489
|
+
buffers: [],
|
|
490
|
+
esm_spec: { url: SPEC_V2.url, hash: SPEC_V2.hash },
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
await registry.getWidget(testId);
|
|
495
|
+
expect(v2.initialize).not.toHaveBeenCalled();
|
|
496
|
+
expect(el.textContent).toBe("v1");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("ignores an update whose spec hash is unchanged", async () => {
|
|
500
|
+
const { registry, v1 } = await setup(true);
|
|
501
|
+
|
|
502
|
+
await handleWidgetMessage(registry, {
|
|
503
|
+
model_id: testId,
|
|
504
|
+
message: {
|
|
505
|
+
method: "update",
|
|
506
|
+
state: {},
|
|
507
|
+
buffer_paths: [],
|
|
508
|
+
buffers: [],
|
|
509
|
+
esm_spec: { url: SPEC_V1.url, hash: SPEC_V1.hash },
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
await registry.getWidget(testId);
|
|
514
|
+
expect(v1.initialize).toHaveBeenCalledTimes(1);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it("cleans up the old generation's listeners on swap", async () => {
|
|
518
|
+
const { registry, model } = await setup(true);
|
|
519
|
+
|
|
520
|
+
// v1's render registered nothing; attach a listener the way a
|
|
521
|
+
// widget would, through a generation-scoped proxy — easiest to
|
|
522
|
+
// observe via initialize cleanup of a fresh generation:
|
|
523
|
+
const onCount = vi.fn();
|
|
524
|
+
const v3 = {
|
|
525
|
+
initialize: vi.fn(({ model }: { model: { on: Function } }) => {
|
|
526
|
+
model.on("change:count", onCount);
|
|
527
|
+
}),
|
|
528
|
+
render: vi.fn(),
|
|
529
|
+
};
|
|
530
|
+
getModuleSpy.mockResolvedValue({ default: v3 });
|
|
531
|
+
|
|
532
|
+
await handleWidgetMessage(registry, {
|
|
533
|
+
model_id: testId,
|
|
534
|
+
message: {
|
|
535
|
+
method: "update",
|
|
536
|
+
state: {},
|
|
537
|
+
buffer_paths: [],
|
|
538
|
+
buffers: [],
|
|
539
|
+
esm_spec: { url: "./@file/10-v3.js", hash: "hash-v3" },
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
await registry.getWidget(testId);
|
|
543
|
+
|
|
544
|
+
model.set("count", 10);
|
|
545
|
+
expect(onCount).toHaveBeenCalledTimes(1);
|
|
546
|
+
|
|
547
|
+
// Swap again: v3's listener must be gone afterwards.
|
|
548
|
+
getModuleSpy.mockResolvedValue({ default: makeWidget("v4") });
|
|
549
|
+
await handleWidgetMessage(registry, {
|
|
550
|
+
model_id: testId,
|
|
551
|
+
message: {
|
|
552
|
+
method: "update",
|
|
553
|
+
state: {},
|
|
554
|
+
buffer_paths: [],
|
|
555
|
+
buffers: [],
|
|
556
|
+
esm_spec: { url: "./@file/10-v4.js", hash: "hash-v4" },
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
await registry.getWidget(testId);
|
|
560
|
+
|
|
561
|
+
model.set("count", 11);
|
|
562
|
+
expect(onCount).toHaveBeenCalledTimes(1);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("starts a new generation without waiting for stale initialize", async () => {
|
|
566
|
+
const registry = new WidgetRegistry(50, () => true);
|
|
567
|
+
const first = {
|
|
568
|
+
initialize: vi.fn().mockReturnValue(new Promise(() => undefined)),
|
|
569
|
+
render: vi.fn(),
|
|
570
|
+
};
|
|
571
|
+
const second = { initialize: vi.fn(), render: vi.fn() };
|
|
572
|
+
getModuleSpy.mockImplementation(async ({ jsUrl }) => ({
|
|
573
|
+
default: jsUrl === SPEC_V1.url ? first : second,
|
|
574
|
+
}));
|
|
575
|
+
registry.setModel(
|
|
576
|
+
testId,
|
|
577
|
+
new Model<ModelState>({ count: 0 }, createMockComm()),
|
|
578
|
+
);
|
|
579
|
+
registry.setSpec(testId, SPEC_V1);
|
|
580
|
+
|
|
581
|
+
const waiting = registry.getWidget(testId);
|
|
582
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
583
|
+
registry.setSpec(testId, SPEC_V2);
|
|
584
|
+
|
|
585
|
+
await expect(waiting).resolves.toBeDefined();
|
|
586
|
+
expect(second.initialize).toHaveBeenCalledTimes(1);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it("waits for old render cleanup before rendering the replacement", async () => {
|
|
590
|
+
const registry = new WidgetRegistry(50, () => true);
|
|
591
|
+
let cleanupStarted!: () => void;
|
|
592
|
+
const started = new Promise<void>((resolve) => {
|
|
593
|
+
cleanupStarted = resolve;
|
|
594
|
+
});
|
|
595
|
+
let finishCleanup!: () => void;
|
|
596
|
+
const cleanupGate = new Promise<void>((resolve) => {
|
|
597
|
+
finishCleanup = resolve;
|
|
598
|
+
});
|
|
599
|
+
const v1 = {
|
|
600
|
+
initialize: vi.fn(),
|
|
601
|
+
render: vi.fn(({ el }: { el: HTMLElement }) => {
|
|
602
|
+
el.textContent = "v1";
|
|
603
|
+
return async () => {
|
|
604
|
+
cleanupStarted();
|
|
605
|
+
await cleanupGate;
|
|
606
|
+
el.textContent = "stale cleanup";
|
|
607
|
+
};
|
|
608
|
+
}),
|
|
609
|
+
};
|
|
610
|
+
const v2 = makeWidget("v2");
|
|
611
|
+
getModuleSpy.mockImplementation(async ({ jsUrl }) => ({
|
|
612
|
+
default: jsUrl === SPEC_V1.url ? v1 : v2,
|
|
613
|
+
}));
|
|
614
|
+
registry.setModel(
|
|
615
|
+
testId,
|
|
616
|
+
new Model<ModelState>({ count: 0 }, createMockComm()),
|
|
617
|
+
);
|
|
618
|
+
registry.setSpec(testId, SPEC_V1);
|
|
619
|
+
const widget = await registry.getWidget(testId);
|
|
620
|
+
const el = document.createElement("div");
|
|
621
|
+
await widget.render({ el });
|
|
622
|
+
|
|
623
|
+
registry.setSpec(testId, SPEC_V2);
|
|
624
|
+
await started;
|
|
625
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
626
|
+
const rendersBeforeCleanupFinished = v2.render.mock.calls.length;
|
|
627
|
+
|
|
628
|
+
finishCleanup();
|
|
629
|
+
expect(rendersBeforeCleanupFinished).toBe(0);
|
|
630
|
+
await vi.waitFor(() => {
|
|
631
|
+
expect(el.textContent).toBe("v2");
|
|
632
|
+
});
|
|
633
|
+
expect(v2.render).toHaveBeenCalledTimes(1);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe("WidgetRegistry runtime styles", () => {
|
|
638
|
+
it("mounts composed widget CSS in its render root", async () => {
|
|
639
|
+
const registry = new WidgetRegistry(50);
|
|
640
|
+
const id = asModelId("styled-child");
|
|
641
|
+
const getModuleSpy = vi
|
|
642
|
+
.spyOn(WIDGET_DEF_REGISTRY, "getModule")
|
|
643
|
+
.mockResolvedValue({ default: { render: vi.fn() } });
|
|
644
|
+
const model = new Model<ModelState>(
|
|
645
|
+
{ _css: ".child { color: red; }" },
|
|
646
|
+
createMockComm(),
|
|
647
|
+
);
|
|
648
|
+
registry.setModel(id, model);
|
|
649
|
+
registry.setSpec(id, {
|
|
650
|
+
url: "./@file/styled-child.js",
|
|
651
|
+
hash: "styled-child-hash",
|
|
652
|
+
});
|
|
653
|
+
const host = document.createElement("div");
|
|
654
|
+
const root = host.attachShadow({ mode: "open" });
|
|
655
|
+
const el = document.createElement("div");
|
|
656
|
+
root.append(el);
|
|
657
|
+
const controller = new AbortController();
|
|
658
|
+
|
|
659
|
+
try {
|
|
660
|
+
await registry.createView({
|
|
661
|
+
modelId: id,
|
|
662
|
+
el,
|
|
663
|
+
signal: controller.signal,
|
|
664
|
+
});
|
|
665
|
+
expect(root.querySelector("style")?.textContent).toContain("color: red");
|
|
666
|
+
|
|
667
|
+
model.set("_css", ".child { color: blue; }");
|
|
668
|
+
expect(root.querySelector("style")?.textContent).toContain("color: blue");
|
|
669
|
+
|
|
670
|
+
controller.abort();
|
|
671
|
+
expect(root.querySelector("style")).toBeNull();
|
|
672
|
+
} finally {
|
|
673
|
+
registry.delete(id);
|
|
674
|
+
getModuleSpy.mockRestore();
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
describe("WidgetRegistry custom messages during rendezvous", () => {
|
|
680
|
+
it("emits custom messages arriving before getWidget resolves", async () => {
|
|
681
|
+
// Regression guard for the rendezvous ordering: state and custom
|
|
682
|
+
// messages must flow to a model even while a generation is pending.
|
|
683
|
+
const registry = new WidgetRegistry(50);
|
|
684
|
+
const testId = asModelId("pending-custom");
|
|
685
|
+
const widgetDef = { initialize: vi.fn(), render: vi.fn() };
|
|
686
|
+
const getModuleSpy = vi
|
|
687
|
+
.spyOn(WIDGET_DEF_REGISTRY, "getModule")
|
|
688
|
+
.mockResolvedValue({ default: widgetDef });
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
692
|
+
const callback = vi.fn();
|
|
693
|
+
model.on("msg:custom", callback);
|
|
694
|
+
registry.setModel(testId, model);
|
|
695
|
+
registry.setSpec(testId, SPEC);
|
|
696
|
+
|
|
697
|
+
const pending = registry.getWidget(testId);
|
|
698
|
+
getMarimoInternal(model).emitCustomMessage({
|
|
699
|
+
method: "custom",
|
|
700
|
+
content: { ping: 1 },
|
|
701
|
+
});
|
|
702
|
+
await pending;
|
|
703
|
+
expect(callback).toHaveBeenCalledWith({ ping: 1 }, []);
|
|
704
|
+
} finally {
|
|
705
|
+
getModuleSpy.mockRestore();
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
});
|