@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,313 @@
|
|
|
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 { createHost } from "../host";
|
|
12
|
+
import { Model } from "../model";
|
|
13
|
+
import { WIDGET_REGISTRY } from "../registry";
|
|
14
|
+
import type { ModelState, WidgetModelId } from "../types";
|
|
15
|
+
import { visibleForTesting, WIDGET_DEF_REGISTRY } from "../widget-binding";
|
|
16
|
+
import { WIDGET_REF_PREFIX } from "../widget-ref";
|
|
17
|
+
|
|
18
|
+
const { WidgetBinding } = visibleForTesting;
|
|
19
|
+
|
|
20
|
+
const asModelId = (id: string): WidgetModelId => id as WidgetModelId;
|
|
21
|
+
const ref = (id: string): string => `${WIDGET_REF_PREFIX}${id}`;
|
|
22
|
+
|
|
23
|
+
function createMockComm() {
|
|
24
|
+
return {
|
|
25
|
+
sendUpdate: vi.fn().mockResolvedValue(undefined),
|
|
26
|
+
sendCustomMessage: vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("createHost", () => {
|
|
31
|
+
// Each test uses a unique model id so global WIDGET_REGISTRY state
|
|
32
|
+
// from earlier tests doesn't leak in.
|
|
33
|
+
let nextId = 0;
|
|
34
|
+
let modelId: WidgetModelId;
|
|
35
|
+
let parentController: AbortController;
|
|
36
|
+
let getModuleSpy: MockInstance<typeof WIDGET_DEF_REGISTRY.getModule>;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
nextId += 1;
|
|
40
|
+
modelId = asModelId(`host-test-${nextId}`);
|
|
41
|
+
parentController = new AbortController();
|
|
42
|
+
getModuleSpy = vi.spyOn(WIDGET_DEF_REGISTRY, "getModule");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
WIDGET_REGISTRY.delete(modelId);
|
|
47
|
+
getModuleSpy.mockRestore();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** Register a child in the global registry the way an open message
|
|
51
|
+
* would: model + ESM spec, with the module load answered by the spy. */
|
|
52
|
+
function registerChild(
|
|
53
|
+
widgetDef: object,
|
|
54
|
+
state?: ModelState,
|
|
55
|
+
): Model<ModelState> {
|
|
56
|
+
const childModel = new Model<ModelState>(
|
|
57
|
+
state ?? { count: 0 },
|
|
58
|
+
createMockComm(),
|
|
59
|
+
);
|
|
60
|
+
WIDGET_REGISTRY.setModel(modelId, childModel);
|
|
61
|
+
WIDGET_REGISTRY.setSpec(modelId, {
|
|
62
|
+
url: `./@file/10-${modelId}.js`,
|
|
63
|
+
// Unique per test: the def registry caches modules by hash.
|
|
64
|
+
hash: `hash-${modelId}`,
|
|
65
|
+
});
|
|
66
|
+
getModuleSpy.mockResolvedValue({ default: widgetDef });
|
|
67
|
+
return childModel;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe("getModel", () => {
|
|
71
|
+
it("resolves the child model by ref", async () => {
|
|
72
|
+
const childModel = new Model<ModelState>({ value: 42 }, createMockComm());
|
|
73
|
+
WIDGET_REGISTRY.setModel(modelId, childModel);
|
|
74
|
+
|
|
75
|
+
const host = createHost(WIDGET_REGISTRY, parentController.signal);
|
|
76
|
+
const resolved = await host.getModel(ref(modelId));
|
|
77
|
+
expect(resolved.get("value")).toBe(42);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("rejects an invalid ref", async () => {
|
|
81
|
+
const host = createHost(WIDGET_REGISTRY, parentController.signal);
|
|
82
|
+
await expect(host.getModel("IPY_MODEL_xyz")).rejects.toThrow(/Invalid/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("scopes listeners on the resolved model to the parent's signal", async () => {
|
|
86
|
+
const childModel = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
87
|
+
WIDGET_REGISTRY.setModel(modelId, childModel);
|
|
88
|
+
|
|
89
|
+
const host = createHost(WIDGET_REGISTRY, parentController.signal);
|
|
90
|
+
const resolved = await host.getModel(ref(modelId));
|
|
91
|
+
|
|
92
|
+
const cb = vi.fn();
|
|
93
|
+
resolved.on("change:count", cb);
|
|
94
|
+
childModel.set("count", 1);
|
|
95
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
96
|
+
|
|
97
|
+
// Aborting the parent signal should clear the listener even though
|
|
98
|
+
// the caller never wired it up themselves.
|
|
99
|
+
parentController.abort();
|
|
100
|
+
childModel.set("count", 2);
|
|
101
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("getWidget", () => {
|
|
106
|
+
it("resolves an undisplayed child with exports and a render method", async () => {
|
|
107
|
+
const exports = { setValue: vi.fn() };
|
|
108
|
+
const widgetDef = {
|
|
109
|
+
initialize: vi.fn().mockResolvedValue(exports),
|
|
110
|
+
render: vi.fn(),
|
|
111
|
+
};
|
|
112
|
+
registerChild(widgetDef);
|
|
113
|
+
|
|
114
|
+
const host = createHost(WIDGET_REGISTRY, parentController.signal);
|
|
115
|
+
const resolved = await host.getWidget(ref(modelId));
|
|
116
|
+
expect(resolved.exports).toBe(exports);
|
|
117
|
+
|
|
118
|
+
const el = document.createElement("div");
|
|
119
|
+
await resolved.render({ el });
|
|
120
|
+
expect(widgetDef.render).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(widgetDef.render.mock.calls[0][0].el).toBe(el);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("waits for an in-flight `initialize` to settle", async () => {
|
|
125
|
+
let resolveInit!: (v: object) => void;
|
|
126
|
+
const widgetDef = {
|
|
127
|
+
initialize: vi.fn().mockReturnValue(
|
|
128
|
+
new Promise<object>((r) => {
|
|
129
|
+
resolveInit = r;
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
render: vi.fn(),
|
|
133
|
+
};
|
|
134
|
+
registerChild(widgetDef);
|
|
135
|
+
|
|
136
|
+
const host = createHost(WIDGET_REGISTRY, parentController.signal);
|
|
137
|
+
const widgetPromise = host.getWidget(ref(modelId));
|
|
138
|
+
|
|
139
|
+
// Until initialize resolves, getWidget should be pending.
|
|
140
|
+
let resolvedExports: unknown;
|
|
141
|
+
widgetPromise.then((w) => {
|
|
142
|
+
resolvedExports = w.exports;
|
|
143
|
+
});
|
|
144
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
145
|
+
expect(resolvedExports).toBeUndefined();
|
|
146
|
+
|
|
147
|
+
const exports = { ready: true };
|
|
148
|
+
resolveInit(exports);
|
|
149
|
+
const resolved = await widgetPromise;
|
|
150
|
+
expect(resolved.exports).toBe(exports);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("rejects if the child is deleted mid-initialize", async () => {
|
|
154
|
+
const widgetDef = {
|
|
155
|
+
initialize: vi.fn().mockReturnValue(new Promise(() => undefined)),
|
|
156
|
+
render: vi.fn(),
|
|
157
|
+
};
|
|
158
|
+
registerChild(widgetDef);
|
|
159
|
+
|
|
160
|
+
const host = createHost(WIDGET_REGISTRY, parentController.signal);
|
|
161
|
+
const widgetPromise = host.getWidget(ref(modelId));
|
|
162
|
+
widgetPromise.catch(() => undefined);
|
|
163
|
+
// Let the import resolve and initialize start.
|
|
164
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
165
|
+
|
|
166
|
+
WIDGET_REGISTRY.delete(modelId);
|
|
167
|
+
|
|
168
|
+
await expect(widgetPromise).rejects.toThrow(/binding destroyed/);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("rejects for a child that has no ESM spec", async () => {
|
|
172
|
+
// e.g. a ref to a traditional jupyter widget's model.
|
|
173
|
+
const childModel = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
174
|
+
WIDGET_REGISTRY.setModel(modelId, childModel);
|
|
175
|
+
|
|
176
|
+
const host = createHost(WIDGET_REGISTRY, parentController.signal);
|
|
177
|
+
await expect(host.getWidget(ref(modelId))).rejects.toThrow(/No ESM spec/);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("uses the parent's signal as a default for child render", async () => {
|
|
181
|
+
const widgetDef = {
|
|
182
|
+
initialize: vi.fn(),
|
|
183
|
+
render: vi.fn(),
|
|
184
|
+
};
|
|
185
|
+
registerChild(widgetDef);
|
|
186
|
+
|
|
187
|
+
const host = createHost(WIDGET_REGISTRY, parentController.signal);
|
|
188
|
+
const resolved = await host.getWidget(ref(modelId));
|
|
189
|
+
|
|
190
|
+
await resolved.render({ el: document.createElement("div") });
|
|
191
|
+
const childRenderSignal = widgetDef.render.mock.calls[0][0].signal;
|
|
192
|
+
expect(childRenderSignal.aborted).toBe(false);
|
|
193
|
+
|
|
194
|
+
// Aborting the parent signal cascades to the child's view.
|
|
195
|
+
parentController.abort();
|
|
196
|
+
expect(childRenderSignal.aborted).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("uses the caller-supplied signal when given, ignoring the parent default", async () => {
|
|
200
|
+
const widgetDef = {
|
|
201
|
+
initialize: vi.fn(),
|
|
202
|
+
render: vi.fn(),
|
|
203
|
+
};
|
|
204
|
+
registerChild(widgetDef);
|
|
205
|
+
|
|
206
|
+
const host = createHost(WIDGET_REGISTRY, parentController.signal);
|
|
207
|
+
const resolved = await host.getWidget(ref(modelId));
|
|
208
|
+
|
|
209
|
+
const childController = new AbortController();
|
|
210
|
+
await resolved.render({
|
|
211
|
+
el: document.createElement("div"),
|
|
212
|
+
signal: childController.signal,
|
|
213
|
+
});
|
|
214
|
+
const childRenderSignal = widgetDef.render.mock.calls[0][0].signal;
|
|
215
|
+
expect(childRenderSignal.aborted).toBe(false);
|
|
216
|
+
|
|
217
|
+
childController.abort();
|
|
218
|
+
expect(childRenderSignal.aborted).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("WidgetBinding receives a host in render props", () => {
|
|
224
|
+
it("passes a host into render whose getWidget resolves siblings", async () => {
|
|
225
|
+
const parentId = asModelId("host-render-parent");
|
|
226
|
+
const childId = asModelId("host-render-child");
|
|
227
|
+
const parentController = new AbortController();
|
|
228
|
+
const getModuleSpy = vi.spyOn(WIDGET_DEF_REGISTRY, "getModule");
|
|
229
|
+
try {
|
|
230
|
+
const childExports = { id: "child" };
|
|
231
|
+
const childWidget = {
|
|
232
|
+
initialize: vi.fn().mockResolvedValue(childExports),
|
|
233
|
+
render: vi.fn(),
|
|
234
|
+
};
|
|
235
|
+
const childModel = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
236
|
+
WIDGET_REGISTRY.setModel(childId, childModel);
|
|
237
|
+
WIDGET_REGISTRY.setSpec(childId, {
|
|
238
|
+
url: "./@file/10-host-render-child.js",
|
|
239
|
+
hash: "hash-host-render-child",
|
|
240
|
+
});
|
|
241
|
+
getModuleSpy.mockResolvedValue({ default: childWidget });
|
|
242
|
+
|
|
243
|
+
const parentModel = new Model<ModelState>(
|
|
244
|
+
{ child: `${WIDGET_REF_PREFIX}${childId}` },
|
|
245
|
+
createMockComm(),
|
|
246
|
+
);
|
|
247
|
+
WIDGET_REGISTRY.setModel(parentId, parentModel);
|
|
248
|
+
|
|
249
|
+
const parentWidget = {
|
|
250
|
+
initialize: vi.fn(),
|
|
251
|
+
render: vi.fn(),
|
|
252
|
+
};
|
|
253
|
+
const parentBinding = await WidgetBinding.create({
|
|
254
|
+
widgetDef: parentWidget,
|
|
255
|
+
model: parentModel,
|
|
256
|
+
createHost: (signal) => createHost(WIDGET_REGISTRY, signal),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await parentBinding.createView(
|
|
260
|
+
{ el: document.createElement("div") },
|
|
261
|
+
{ signal: parentController.signal },
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const renderProps = parentWidget.render.mock.calls[0][0];
|
|
265
|
+
expect(renderProps.host).toBeDefined();
|
|
266
|
+
expect(typeof renderProps.host.getWidget).toBe("function");
|
|
267
|
+
|
|
268
|
+
const resolved = await renderProps.host.getWidget(
|
|
269
|
+
parentModel.get("child"),
|
|
270
|
+
);
|
|
271
|
+
expect(resolved.exports).toBe(childExports);
|
|
272
|
+
} finally {
|
|
273
|
+
WIDGET_REGISTRY.delete(parentId);
|
|
274
|
+
WIDGET_REGISTRY.delete(childId);
|
|
275
|
+
getModuleSpy.mockRestore();
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("host-mounted views hydrate like display-mounted ones", () => {
|
|
281
|
+
it("replays current state to a child's render listeners", async () => {
|
|
282
|
+
const childId = asModelId("host-hydration-child");
|
|
283
|
+
const parentController = new AbortController();
|
|
284
|
+
const getModuleSpy = vi.spyOn(WIDGET_DEF_REGISTRY, "getModule");
|
|
285
|
+
try {
|
|
286
|
+
const childWidget = {
|
|
287
|
+
render: vi.fn(({ model, el }) => {
|
|
288
|
+
el.textContent = "count is 5";
|
|
289
|
+
model.on("change:count", () => {
|
|
290
|
+
el.textContent = `count is ${model.get("count")}`;
|
|
291
|
+
});
|
|
292
|
+
}),
|
|
293
|
+
};
|
|
294
|
+
const childModel = new Model<ModelState>({ count: 8 }, createMockComm());
|
|
295
|
+
WIDGET_REGISTRY.setModel(childId, childModel);
|
|
296
|
+
WIDGET_REGISTRY.setSpec(childId, {
|
|
297
|
+
url: "./@file/10-host-hydration-child.js",
|
|
298
|
+
hash: "hash-host-hydration-child",
|
|
299
|
+
});
|
|
300
|
+
getModuleSpy.mockResolvedValue({ default: childWidget });
|
|
301
|
+
|
|
302
|
+
const host = createHost(WIDGET_REGISTRY, parentController.signal);
|
|
303
|
+
const resolved = await host.getWidget(ref(childId));
|
|
304
|
+
|
|
305
|
+
const el = document.createElement("div");
|
|
306
|
+
await resolved.render({ el });
|
|
307
|
+
expect(el.textContent).toBe("count is 8");
|
|
308
|
+
} finally {
|
|
309
|
+
WIDGET_REGISTRY.delete(childId);
|
|
310
|
+
getModuleSpy.mockRestore();
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { Model } from "../model";
|
|
4
|
+
import { modelProxy } from "../model-proxy";
|
|
5
|
+
import type { ModelState } from "../types";
|
|
6
|
+
|
|
7
|
+
function createMockComm() {
|
|
8
|
+
return {
|
|
9
|
+
sendUpdate: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
sendCustomMessage: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("Model.on signal option", () => {
|
|
15
|
+
it("removes the listener when the signal aborts", () => {
|
|
16
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const cb = vi.fn();
|
|
19
|
+
|
|
20
|
+
model.on("change:count", cb, { signal: controller.signal });
|
|
21
|
+
model.set("count", 1);
|
|
22
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
23
|
+
|
|
24
|
+
controller.abort();
|
|
25
|
+
model.set("count", 2);
|
|
26
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("does not register if the signal is already aborted", () => {
|
|
30
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
controller.abort();
|
|
33
|
+
const cb = vi.fn();
|
|
34
|
+
|
|
35
|
+
model.on("change:count", cb, { signal: controller.signal });
|
|
36
|
+
model.set("count", 1);
|
|
37
|
+
expect(cb).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("scopes listeners independently per signal", () => {
|
|
41
|
+
const model = new Model<ModelState>({ value: 0 }, createMockComm());
|
|
42
|
+
const view1 = new AbortController();
|
|
43
|
+
const view2 = new AbortController();
|
|
44
|
+
const cb1 = vi.fn();
|
|
45
|
+
const cb2 = vi.fn();
|
|
46
|
+
|
|
47
|
+
model.on("change:value", cb1, { signal: view1.signal });
|
|
48
|
+
model.on("change:value", cb2, { signal: view2.signal });
|
|
49
|
+
|
|
50
|
+
model.set("value", 1);
|
|
51
|
+
expect(cb1).toHaveBeenCalledTimes(1);
|
|
52
|
+
expect(cb2).toHaveBeenCalledTimes(1);
|
|
53
|
+
|
|
54
|
+
view1.abort();
|
|
55
|
+
model.set("value", 2);
|
|
56
|
+
expect(cb1).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(cb2).toHaveBeenCalledTimes(2);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("allows a listener to call off() during emit without skipping siblings", () => {
|
|
61
|
+
// Snapshot-before-iterate guard: a listener that removes itself
|
|
62
|
+
// (typical signal-abort cleanup path) shouldn't drop a sibling.
|
|
63
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
64
|
+
const sibling = vi.fn();
|
|
65
|
+
const selfRemoving = vi.fn(() => model.off("change:count", selfRemoving));
|
|
66
|
+
|
|
67
|
+
model.on("change:count", selfRemoving);
|
|
68
|
+
model.on("change:count", sibling);
|
|
69
|
+
|
|
70
|
+
model.set("count", 1);
|
|
71
|
+
expect(selfRemoving).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(sibling).toHaveBeenCalledTimes(1);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("modelProxy", () => {
|
|
77
|
+
it("auto-ties on() to the supplied signal", () => {
|
|
78
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
const proxy = modelProxy(model, controller.signal);
|
|
81
|
+
const cb = vi.fn();
|
|
82
|
+
|
|
83
|
+
proxy.on("change:count", cb);
|
|
84
|
+
model.set("count", 1);
|
|
85
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
86
|
+
|
|
87
|
+
// Aborting the signal should clear the listener even though the
|
|
88
|
+
// widget author never wired it up themselves.
|
|
89
|
+
controller.abort();
|
|
90
|
+
model.set("count", 2);
|
|
91
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("scopes child listeners to per-view signals", () => {
|
|
95
|
+
const model = new Model<ModelState>({ value: 0 }, createMockComm());
|
|
96
|
+
const view1 = new AbortController();
|
|
97
|
+
const view2 = new AbortController();
|
|
98
|
+
const cb1 = vi.fn();
|
|
99
|
+
const cb2 = vi.fn();
|
|
100
|
+
|
|
101
|
+
modelProxy(model, view1.signal).on("change:value", cb1);
|
|
102
|
+
modelProxy(model, view2.signal).on("change:value", cb2);
|
|
103
|
+
|
|
104
|
+
model.set("value", 1);
|
|
105
|
+
expect(cb1).toHaveBeenCalledTimes(1);
|
|
106
|
+
expect(cb2).toHaveBeenCalledTimes(1);
|
|
107
|
+
|
|
108
|
+
// Dispose view1 — view2's listener stays.
|
|
109
|
+
view1.abort();
|
|
110
|
+
model.set("value", 2);
|
|
111
|
+
expect(cb1).toHaveBeenCalledTimes(1);
|
|
112
|
+
expect(cb2).toHaveBeenCalledTimes(2);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("scopes legacy widget_manager child listeners to the parent signal", async () => {
|
|
116
|
+
const parent = new Model<ModelState>({}, createMockComm());
|
|
117
|
+
const child = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
118
|
+
vi.spyOn(parent.widget_manager, "get_model").mockResolvedValue(child);
|
|
119
|
+
const controller = new AbortController();
|
|
120
|
+
const parentProxy = modelProxy(parent, controller.signal);
|
|
121
|
+
|
|
122
|
+
const childProxy =
|
|
123
|
+
await parentProxy.widget_manager.get_model<ModelState>("child-id");
|
|
124
|
+
const callback = vi.fn();
|
|
125
|
+
childProxy.on("change:count", callback);
|
|
126
|
+
|
|
127
|
+
child.set("count", 1);
|
|
128
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
129
|
+
|
|
130
|
+
controller.abort();
|
|
131
|
+
child.set("count", 2);
|
|
132
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("forwards get / set / save_changes", () => {
|
|
136
|
+
const comm = createMockComm();
|
|
137
|
+
const model = new Model<ModelState>({ count: 0 }, comm);
|
|
138
|
+
const controller = new AbortController();
|
|
139
|
+
const proxy = modelProxy(model, controller.signal);
|
|
140
|
+
|
|
141
|
+
proxy.set("count", 5);
|
|
142
|
+
expect(proxy.get("count")).toBe(5);
|
|
143
|
+
proxy.save_changes();
|
|
144
|
+
expect(comm.sendUpdate).toHaveBeenCalledWith({ count: 5 });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("forwards off() through to the underlying model", () => {
|
|
148
|
+
const model = new Model<ModelState>({ count: 0 }, createMockComm());
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const proxy = modelProxy(model, controller.signal);
|
|
151
|
+
const cb = vi.fn();
|
|
152
|
+
|
|
153
|
+
proxy.on("change:count", cb);
|
|
154
|
+
proxy.off("change:count", cb);
|
|
155
|
+
model.set("count", 1);
|
|
156
|
+
expect(cb).not.toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -9,16 +9,9 @@ import {
|
|
|
9
9
|
vi,
|
|
10
10
|
} from "vitest";
|
|
11
11
|
import { TestUtils } from "@/__tests__/test-helpers";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Model,
|
|
16
|
-
visibleForTesting,
|
|
17
|
-
} from "../model";
|
|
18
|
-
import type { WidgetModelId } from "../types";
|
|
19
|
-
import { BINDING_MANAGER } from "../widget-binding";
|
|
20
|
-
|
|
21
|
-
const { ModelManager } = visibleForTesting;
|
|
12
|
+
import { getMarimoInternal, Model } from "../model";
|
|
13
|
+
import { WidgetRegistry } from "../registry";
|
|
14
|
+
import type { ModelState, WidgetModelId } from "../types";
|
|
22
15
|
|
|
23
16
|
// Helper to create typed model IDs for tests
|
|
24
17
|
const asModelId = (id: string): WidgetModelId => id as WidgetModelId;
|
|
@@ -38,7 +31,7 @@ vi.mock("@/core/static/static-state", () => ({
|
|
|
38
31
|
}));
|
|
39
32
|
|
|
40
33
|
// Helper to create a mock MarimoComm
|
|
41
|
-
function createMockComm
|
|
34
|
+
function createMockComm() {
|
|
42
35
|
return {
|
|
43
36
|
sendUpdate: vi.fn().mockResolvedValue(undefined),
|
|
44
37
|
sendCustomMessage: vi.fn().mockResolvedValue(undefined),
|
|
@@ -47,7 +40,7 @@ function createMockComm<T>() {
|
|
|
47
40
|
|
|
48
41
|
describe("Model", () => {
|
|
49
42
|
let model: Model<{ foo: string; bar: number }>;
|
|
50
|
-
let mockComm: ReturnType<typeof createMockComm
|
|
43
|
+
let mockComm: ReturnType<typeof createMockComm>;
|
|
51
44
|
|
|
52
45
|
beforeEach(() => {
|
|
53
46
|
mockComm = createMockComm();
|
|
@@ -211,13 +204,13 @@ describe("Model", () => {
|
|
|
211
204
|
|
|
212
205
|
describe("widget_manager", () => {
|
|
213
206
|
const childModelId = asModelId("test-id");
|
|
214
|
-
const childModel = new Model({ foo: "test" }, createMockComm());
|
|
215
|
-
const manager = new
|
|
207
|
+
const childModel = new Model<ModelState>({ foo: "test" }, createMockComm());
|
|
208
|
+
const manager = new WidgetRegistry(10);
|
|
216
209
|
let previousModelManager = Model._modelManager;
|
|
217
210
|
|
|
218
211
|
beforeAll(() => {
|
|
219
212
|
previousModelManager = Model._modelManager;
|
|
220
|
-
manager.
|
|
213
|
+
manager.setModel(childModelId, childModel);
|
|
221
214
|
Model._modelManager = manager;
|
|
222
215
|
});
|
|
223
216
|
|
|
@@ -272,25 +265,6 @@ describe("Model", () => {
|
|
|
272
265
|
});
|
|
273
266
|
});
|
|
274
267
|
|
|
275
|
-
describe("reemitState", () => {
|
|
276
|
-
it("should emit change events for current values without state changes", async () => {
|
|
277
|
-
const onFoo = vi.fn();
|
|
278
|
-
const onBar = vi.fn();
|
|
279
|
-
const onAny = vi.fn();
|
|
280
|
-
|
|
281
|
-
model.on("change:foo", onFoo);
|
|
282
|
-
model.on("change:bar", onBar);
|
|
283
|
-
model.on("change", onAny);
|
|
284
|
-
|
|
285
|
-
getMarimoInternal(model).reemitState();
|
|
286
|
-
await TestUtils.nextTick();
|
|
287
|
-
|
|
288
|
-
expect(onFoo).toHaveBeenCalledWith("test");
|
|
289
|
-
expect(onBar).toHaveBeenCalledWith(123);
|
|
290
|
-
expect(onAny).toHaveBeenCalledTimes(1);
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
|
|
294
268
|
describe("emitCustomMessage", () => {
|
|
295
269
|
it("should handle custom messages", () => {
|
|
296
270
|
const callback = vi.fn();
|
|
@@ -323,146 +297,3 @@ describe("Model", () => {
|
|
|
323
297
|
});
|
|
324
298
|
});
|
|
325
299
|
});
|
|
326
|
-
|
|
327
|
-
describe("ModelManager", () => {
|
|
328
|
-
let modelManager = new ModelManager(50);
|
|
329
|
-
const testId = asModelId("test-id");
|
|
330
|
-
|
|
331
|
-
beforeEach(() => {
|
|
332
|
-
// Clear the model manager before each test
|
|
333
|
-
modelManager = new ModelManager(50);
|
|
334
|
-
mockSendModelValue.mockClear();
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it("should set and get models", async () => {
|
|
338
|
-
const model = new Model({ count: 0 }, createMockComm());
|
|
339
|
-
modelManager.set(testId, model);
|
|
340
|
-
const retrievedModel = await modelManager.get(testId);
|
|
341
|
-
expect(retrievedModel).toBe(model);
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
it("should handle model not found", async () => {
|
|
345
|
-
await expect(modelManager.get(asModelId("non-existent"))).rejects.toThrow(
|
|
346
|
-
"Model not found for key: non-existent",
|
|
347
|
-
);
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it("should delete models", async () => {
|
|
351
|
-
const model = new Model({ count: 0 }, createMockComm());
|
|
352
|
-
modelManager.set(testId, model);
|
|
353
|
-
modelManager.delete(testId);
|
|
354
|
-
await expect(modelManager.get(testId)).rejects.toThrow();
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it("should handle widget messages", async () => {
|
|
358
|
-
await handleWidgetMessage(modelManager, {
|
|
359
|
-
model_id: testId,
|
|
360
|
-
message: {
|
|
361
|
-
method: "open",
|
|
362
|
-
state: { count: 0 },
|
|
363
|
-
buffer_paths: [],
|
|
364
|
-
buffers: [],
|
|
365
|
-
},
|
|
366
|
-
});
|
|
367
|
-
const model = await modelManager.get(testId);
|
|
368
|
-
expect(model.get("count")).toBe(0);
|
|
369
|
-
|
|
370
|
-
await handleWidgetMessage(modelManager, {
|
|
371
|
-
model_id: testId,
|
|
372
|
-
message: {
|
|
373
|
-
method: "update",
|
|
374
|
-
state: { count: 1 },
|
|
375
|
-
buffer_paths: [],
|
|
376
|
-
buffers: [],
|
|
377
|
-
},
|
|
378
|
-
});
|
|
379
|
-
expect(model.get("count")).toBe(1);
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
it("should handle custom messages", async () => {
|
|
383
|
-
const model = new Model({ count: 0 }, createMockComm());
|
|
384
|
-
const callback = vi.fn();
|
|
385
|
-
model.on("msg:custom", callback);
|
|
386
|
-
modelManager.set(testId, model);
|
|
387
|
-
|
|
388
|
-
await handleWidgetMessage(modelManager, {
|
|
389
|
-
model_id: testId,
|
|
390
|
-
message: { method: "custom", content: { count: 1 }, buffers: [] },
|
|
391
|
-
});
|
|
392
|
-
expect(callback).toHaveBeenCalledWith({ count: 1 }, []);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it("should handle close messages", async () => {
|
|
396
|
-
const model = new Model({ count: 0 }, createMockComm());
|
|
397
|
-
modelManager.set(testId, model);
|
|
398
|
-
|
|
399
|
-
await handleWidgetMessage(modelManager, {
|
|
400
|
-
model_id: testId,
|
|
401
|
-
message: { method: "close" },
|
|
402
|
-
});
|
|
403
|
-
await expect(modelManager.get(testId)).rejects.toThrow();
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it("should destroy binding on close message", async () => {
|
|
407
|
-
const model = new Model({ count: 0 }, createMockComm());
|
|
408
|
-
modelManager.set(testId, model);
|
|
409
|
-
|
|
410
|
-
// Create a binding for this model
|
|
411
|
-
BINDING_MANAGER.getOrCreate(testId);
|
|
412
|
-
expect(BINDING_MANAGER.has(testId)).toBe(true);
|
|
413
|
-
|
|
414
|
-
await handleWidgetMessage(modelManager, {
|
|
415
|
-
model_id: testId,
|
|
416
|
-
message: { method: "close" },
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
expect(BINDING_MANAGER.has(testId)).toBe(false);
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
describe("static mode", () => {
|
|
423
|
-
beforeEach(() => {
|
|
424
|
-
mockIsStatic.mockReturnValue(true);
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
afterAll(() => {
|
|
428
|
-
mockIsStatic.mockReturnValue(false);
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it("should create model with no-op comm in static mode", async () => {
|
|
432
|
-
await handleWidgetMessage(modelManager, {
|
|
433
|
-
model_id: testId,
|
|
434
|
-
message: {
|
|
435
|
-
method: "open",
|
|
436
|
-
state: { count: 42 },
|
|
437
|
-
buffer_paths: [],
|
|
438
|
-
buffers: [],
|
|
439
|
-
},
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
const model = await modelManager.get(testId);
|
|
443
|
-
expect(model.get("count")).toBe(42);
|
|
444
|
-
|
|
445
|
-
// save_changes should not call the real request client
|
|
446
|
-
model.set("count", 100);
|
|
447
|
-
model.save_changes();
|
|
448
|
-
expect(mockSendModelValue).not.toHaveBeenCalled();
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
it("should not throw on send in static mode", async () => {
|
|
452
|
-
await handleWidgetMessage(modelManager, {
|
|
453
|
-
model_id: testId,
|
|
454
|
-
message: {
|
|
455
|
-
method: "open",
|
|
456
|
-
state: { count: 0 },
|
|
457
|
-
buffer_paths: [],
|
|
458
|
-
buffers: [],
|
|
459
|
-
},
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
const model = await modelManager.get(testId);
|
|
463
|
-
// send() should silently no-op
|
|
464
|
-
await expect(model.send({ test: true })).resolves.toBeUndefined();
|
|
465
|
-
expect(mockSendModelValue).not.toHaveBeenCalled();
|
|
466
|
-
});
|
|
467
|
-
});
|
|
468
|
-
});
|