@marimo-team/islands 0.23.15-dev32 → 0.23.15-dev4
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-7p7rt9iw.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
- package/dist/{ErrorBoundary-B_CAG7_e.js → ErrorBoundary-DE6tzZf-.js} +1 -1
- package/dist/{ImageComparisonComponent-1i5IBhon.js → ImageComparisonComponent-CHrI72em.js} +129 -173
- package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
- package/dist/assets/worker-DAWRHcPq.js +73 -0
- package/dist/{chat-ui-C0wkSwxB.js → chat-ui-DinOvbWu.js} +3111 -3111
- package/dist/{code-visibility-CEHWVGz1.js → code-visibility-Dk8cVOny.js} +864 -946
- package/dist/{debounce-BOD3DbfP.js → constants-T20xxyNf.js} +24 -1
- package/dist/{formats-BX3uDQbB.js → formats-Dzx4J_z1.js} +1 -1
- package/dist/{glide-data-editor-CwZz71BD.js → glide-data-editor-CjTu7ukN.js} +3 -3
- package/dist/{html-to-image-D8Q5SLSJ.js → html-to-image-_wGfk8V-.js} +2241 -2277
- package/dist/{input-BGPrFH3g.js → input-DtsN7xm-.js} +2 -2
- package/dist/main.js +1201 -1297
- package/dist/{mermaid-UdmxG2PZ.js → mermaid-BYqXy_NE.js} +2 -2
- package/dist/{process-output-C_A12w9J.js → process-output-Mh4UrjwM.js} +1 -1
- package/dist/{reveal-component-kLDhMoa2.js → reveal-component-B5DXLyO7.js} +605 -618
- package/dist/{spec-DSs9v0xx.js → spec-Cz-Bj1JI.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-CKpRx4TS.js → toDate-CWNNlFEX.js} +4 -3
- package/dist/{useAsyncData-BSOyAbac.js → useAsyncData-KfHB8wQR.js} +1 -1
- package/dist/{useDeepCompareMemoize-BTLeWzuR.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
- package/dist/{useLifecycle-C6wHjkhW.js → useLifecycle-DegSo0lV.js} +1 -1
- package/dist/{useTheme-Df_vGflw.js → useTheme-6eZ3GOTS.js} +27 -62
- package/dist/{vega-component-DNHEV0u0.js → vega-component-DzyyM9fc.js} +6 -6
- package/package.json +1 -1
- package/src/components/chat/__tests__/chat-utils.test.ts +1 -244
- package/src/components/chat/chat-panel.tsx +67 -196
- package/src/components/chat/chat-utils.ts +2 -111
- package/src/components/editor/SortableCell.tsx +2 -6
- package/src/components/editor/actions/pair-with-agent-modal.tsx +64 -20
- package/src/components/editor/ai/add-cell-with-ai.tsx +5 -14
- package/src/components/editor/ai/ai-completion-editor.tsx +1 -4
- package/src/components/editor/cell/cell-context-menu.tsx +0 -7
- package/src/components/editor/chrome/panels/packages-panel.tsx +1 -11
- package/src/components/editor/columns/cell-column.tsx +4 -34
- package/src/components/editor/columns/sortable-column.tsx +4 -24
- package/src/components/editor/navigation/__tests__/navigation.test.ts +0 -33
- package/src/components/editor/navigation/navigation.ts +1 -8
- package/src/components/editor/notebook-cell.tsx +106 -203
- package/src/components/editor/renderers/cell-array.tsx +13 -26
- package/src/components/editor/renderers/cells-renderer.tsx +2 -8
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -29
- package/src/components/editor/renderers/slides-layout/types.ts +0 -4
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
- package/src/components/slides/reveal-component.tsx +4 -37
- package/src/components/slides/slide-form.tsx +0 -72
- package/src/core/cells/__tests__/utils.test.ts +0 -59
- package/src/core/cells/utils.ts +0 -51
- package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +3 -97
- package/src/core/codemirror/completion/signature-hint.ts +11 -78
- package/src/core/constants.ts +0 -6
- package/src/core/islands/__tests__/bridge.test.ts +46 -341
- package/src/core/islands/__tests__/parse.test.ts +0 -105
- package/src/core/islands/bootstrap.ts +3 -8
- package/src/core/islands/bridge.ts +23 -116
- package/src/core/islands/components/web-components.tsx +15 -76
- package/src/core/islands/constants.ts +0 -1
- package/src/core/islands/main.ts +3 -35
- package/src/core/islands/parse.ts +23 -70
- package/src/core/islands/worker/worker.tsx +57 -145
- package/src/core/wasm/worker/bootstrap.ts +20 -113
- package/src/css/app/Cell.css +0 -10
- package/src/hooks/useHotkey.ts +4 -29
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +9 -6
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +61 -22
- package/src/plugins/impl/anywidget/model-proxy.ts +13 -0
- package/src/plugins/impl/anywidget/widget-binding.ts +34 -4
- package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +2 -53
- package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +3 -71
- package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +0 -1
- package/src/theme/useTheme.ts +1 -16
- package/dist/assets/__vite-browser-external-CjNAy01L.js +0 -1
- package/dist/assets/worker-B3XPCb6y.js +0 -98
- package/src/components/chat/__tests__/message-queue.test.tsx +0 -121
- package/src/components/editor/__tests__/output-persistence.test.tsx +0 -241
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +0 -153
- package/src/components/editor/actions/pair-with-agent-commands.ts +0 -109
- package/src/components/editor/columns/__tests__/cell-column.test.tsx +0 -105
- package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +0 -66
- package/src/core/islands/components/__tests__/web-components.test.tsx +0 -214
- package/src/core/islands/worker/__tests__/controller.test.ts +0 -173
- package/src/hooks/__tests__/useHotkey.test.tsx +0 -88
- package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +0 -71
- package/src/theme/__tests__/useTheme.test.ts +0 -68
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { act } from "react";
|
|
4
|
-
import {
|
|
5
|
-
afterAll,
|
|
6
|
-
afterEach,
|
|
7
|
-
beforeAll,
|
|
8
|
-
beforeEach,
|
|
9
|
-
describe,
|
|
10
|
-
expect,
|
|
11
|
-
it,
|
|
12
|
-
} from "vitest";
|
|
13
|
-
import { cellId } from "@/__tests__/branded";
|
|
14
|
-
import { MockNotebook } from "@/__mocks__/notebook";
|
|
15
|
-
import { MockRequestClient } from "@/__mocks__/requests";
|
|
16
|
-
import { notebookAtom } from "@/core/cells/cells";
|
|
17
|
-
import {
|
|
18
|
-
ISLAND_DATA_ATTRIBUTES,
|
|
19
|
-
ISLAND_TAG_NAMES,
|
|
20
|
-
ISLANDS_JSON_SCRIPT_TYPE,
|
|
21
|
-
} from "@/core/islands/constants";
|
|
22
|
-
import { requestClientAtom } from "@/core/network/requests";
|
|
23
|
-
import { store } from "@/core/state/jotai";
|
|
24
|
-
import { parseMarimoIslandApps } from "../../parse";
|
|
25
|
-
import { MarimoIslandElement } from "../web-components";
|
|
26
|
-
|
|
27
|
-
beforeAll(() => {
|
|
28
|
-
setReactActEnvironment(true);
|
|
29
|
-
if (!customElements.get(ISLAND_TAG_NAMES.ISLAND)) {
|
|
30
|
-
customElements.define(ISLAND_TAG_NAMES.ISLAND, MarimoIslandElement);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterAll(() => {
|
|
35
|
-
setReactActEnvironment(false);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
store.set(notebookAtom, MockNotebook.notebookState({ cellData: {} }));
|
|
40
|
-
store.set(requestClientAtom, MockRequestClient.create());
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
afterEach(async () => {
|
|
44
|
-
await actAndFlush(() => document.body.replaceChildren());
|
|
45
|
-
store.set(requestClientAtom, null);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe("MarimoIslandElement lifecycle", () => {
|
|
49
|
-
it("binds a reactive island after its runtime cell index is materialized", async () => {
|
|
50
|
-
setNotebook("outgoing-cell", "outgoing runtime output");
|
|
51
|
-
const container = document.createElement("div");
|
|
52
|
-
container.innerHTML = `
|
|
53
|
-
<marimo-island data-app-id="app-1" data-reactive="true">
|
|
54
|
-
<marimo-cell-output><div>initial output</div></marimo-cell-output>
|
|
55
|
-
<marimo-cell-code hidden>${encodeURIComponent("value = 1")}</marimo-cell-code>
|
|
56
|
-
</marimo-island>
|
|
57
|
-
`;
|
|
58
|
-
await actAndFlush(() => document.body.append(container));
|
|
59
|
-
|
|
60
|
-
const island = container.querySelector<HTMLElement>(
|
|
61
|
-
ISLAND_TAG_NAMES.ISLAND,
|
|
62
|
-
);
|
|
63
|
-
expect(island).not.toBeNull();
|
|
64
|
-
expect(island?.getAttribute("data-status")).toBeNull();
|
|
65
|
-
|
|
66
|
-
await actAndFlush(() => {
|
|
67
|
-
parseMarimoIslandApps(container);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
expect(island?.textContent).toContain("outgoing runtime output");
|
|
71
|
-
|
|
72
|
-
await actAndFlush(() => {
|
|
73
|
-
setNotebook("runtime-cell");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
expect(island?.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX)).toBe("0");
|
|
77
|
-
expect(island?.getAttribute("data-status")).toBe("idle");
|
|
78
|
-
|
|
79
|
-
await actAndFlush(() => {
|
|
80
|
-
island?.remove();
|
|
81
|
-
if (island) {
|
|
82
|
-
container.append(island);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
expect(island?.textContent).toContain("initial output");
|
|
86
|
-
|
|
87
|
-
island?.setAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID, "source-cell");
|
|
88
|
-
island?.setAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE, "false");
|
|
89
|
-
expect((island as MarimoIslandElement | null)?.cellId).toBeUndefined();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("refreshes a reused payload island and clears an old app binding", async () => {
|
|
93
|
-
const container = document.createElement("div");
|
|
94
|
-
container.innerHTML = `
|
|
95
|
-
<marimo-island
|
|
96
|
-
data-app-id="app-1"
|
|
97
|
-
data-cell-id="cell-1"
|
|
98
|
-
data-reactive="true"
|
|
99
|
-
>
|
|
100
|
-
<marimo-cell-output><div>old output</div></marimo-cell-output>
|
|
101
|
-
<marimo-cell-code hidden>${encodeURIComponent('print("old")')}</marimo-cell-code>
|
|
102
|
-
</marimo-island>
|
|
103
|
-
`;
|
|
104
|
-
const script = document.createElement("script");
|
|
105
|
-
script.type = ISLANDS_JSON_SCRIPT_TYPE;
|
|
106
|
-
script.textContent = payloadSource({
|
|
107
|
-
code: 'print("unchanged")',
|
|
108
|
-
outputHtml: "<div>old output</div>",
|
|
109
|
-
});
|
|
110
|
-
container.append(script);
|
|
111
|
-
await actAndFlush(() => document.body.append(container));
|
|
112
|
-
|
|
113
|
-
await actAndFlush(() => {
|
|
114
|
-
parseMarimoIslandApps(container);
|
|
115
|
-
script.textContent = payloadSource({
|
|
116
|
-
code: 'print("unchanged")',
|
|
117
|
-
outputHtml: "<div>updated output</div>",
|
|
118
|
-
});
|
|
119
|
-
parseMarimoIslandApps(container);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
const island = container.querySelector<HTMLElement>(
|
|
123
|
-
ISLAND_TAG_NAMES.ISLAND,
|
|
124
|
-
);
|
|
125
|
-
expect(island?.textContent).toContain("updated output");
|
|
126
|
-
expect(island?.textContent).not.toContain("old output");
|
|
127
|
-
expect(island?.querySelector(ISLAND_TAG_NAMES.CELL_OUTPUT)).toBeNull();
|
|
128
|
-
|
|
129
|
-
await actAndFlush(() => {
|
|
130
|
-
setNotebook("outgoing-cell", "outgoing runtime output");
|
|
131
|
-
});
|
|
132
|
-
expect(island?.textContent).toContain("outgoing runtime output");
|
|
133
|
-
|
|
134
|
-
await actAndFlush(() => {
|
|
135
|
-
island?.setAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID, "app-2");
|
|
136
|
-
island?.setAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID, "cell-2");
|
|
137
|
-
script.textContent = payloadSource({
|
|
138
|
-
appId: "app-2",
|
|
139
|
-
cellId: "cell-2",
|
|
140
|
-
code: 'print("next")',
|
|
141
|
-
outputHtml: "<div>next app output</div>",
|
|
142
|
-
});
|
|
143
|
-
parseMarimoIslandApps(container);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
expect(island?.textContent).toContain("next app output");
|
|
147
|
-
expect(island?.textContent).not.toContain("outgoing runtime output");
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
function setNotebook(id: string, output?: string): void {
|
|
152
|
-
const runtimeCellId = cellId(id);
|
|
153
|
-
store.set(
|
|
154
|
-
notebookAtom,
|
|
155
|
-
MockNotebook.notebookState({
|
|
156
|
-
cellData: { [runtimeCellId]: {} },
|
|
157
|
-
cellRuntime: output
|
|
158
|
-
? {
|
|
159
|
-
[runtimeCellId]: {
|
|
160
|
-
output: {
|
|
161
|
-
channel: "output",
|
|
162
|
-
data: `<div>${output}</div>`,
|
|
163
|
-
mimetype: "text/html",
|
|
164
|
-
timestamp: 0,
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
}
|
|
168
|
-
: undefined,
|
|
169
|
-
}),
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function payloadSource({
|
|
174
|
-
appId = "app-1",
|
|
175
|
-
cellId = "cell-1",
|
|
176
|
-
code,
|
|
177
|
-
outputHtml,
|
|
178
|
-
}: {
|
|
179
|
-
appId?: string;
|
|
180
|
-
cellId?: string;
|
|
181
|
-
code: string;
|
|
182
|
-
outputHtml: string;
|
|
183
|
-
}): string {
|
|
184
|
-
return JSON.stringify({
|
|
185
|
-
schemaVersion: 1,
|
|
186
|
-
appId,
|
|
187
|
-
cells: [
|
|
188
|
-
{
|
|
189
|
-
cellId,
|
|
190
|
-
code,
|
|
191
|
-
outputHtml,
|
|
192
|
-
outputMimetype: "text/html",
|
|
193
|
-
reactive: true,
|
|
194
|
-
displayCode: false,
|
|
195
|
-
displayOutput: true,
|
|
196
|
-
},
|
|
197
|
-
],
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function actAndFlush(action: () => void): Promise<void> {
|
|
202
|
-
await act(async () => {
|
|
203
|
-
action();
|
|
204
|
-
await Promise.resolve();
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function setReactActEnvironment(value: boolean): void {
|
|
209
|
-
(
|
|
210
|
-
globalThis as typeof globalThis & {
|
|
211
|
-
IS_REACT_ACT_ENVIRONMENT: boolean;
|
|
212
|
-
}
|
|
213
|
-
).IS_REACT_ACT_ENVIRONMENT = value;
|
|
214
|
-
}
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import type { PyodideInterface } from "pyodide";
|
|
4
|
-
import { describe, expect, it, vi } from "vitest";
|
|
5
|
-
import { ReadonlyWasmController } from "../controller";
|
|
6
|
-
|
|
7
|
-
class TestController extends ReadonlyWasmController {
|
|
8
|
-
setPyodide(pyodide: PyodideInterface) {
|
|
9
|
-
this.pyodide = pyodide;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function createSessionResources(
|
|
14
|
-
stopImplementation: () => Promise<void> = () => Promise.resolve(),
|
|
15
|
-
) {
|
|
16
|
-
const bridge = { destroy: vi.fn() };
|
|
17
|
-
const init = Object.assign(vi.fn(), { destroy: vi.fn() });
|
|
18
|
-
const stop = Object.assign(vi.fn(stopImplementation), {
|
|
19
|
-
destroy: vi.fn(),
|
|
20
|
-
});
|
|
21
|
-
const packages = { destroy: vi.fn(), toJs: () => [] };
|
|
22
|
-
const sessionResources = Object.assign([bridge, init, packages, stop], {
|
|
23
|
-
destroy: vi.fn(),
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
return { bridge, init, packages, sessionResources, stop };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function createPyodideStub(
|
|
30
|
-
sessions = Array.from({ length: 4 }, () => createSessionResources()),
|
|
31
|
-
) {
|
|
32
|
-
const [first] = sessions;
|
|
33
|
-
if (!first) {
|
|
34
|
-
throw new Error("At least one session is required");
|
|
35
|
-
}
|
|
36
|
-
const loadPackagesFromImports = vi.fn().mockResolvedValue(undefined);
|
|
37
|
-
const pyodide = {
|
|
38
|
-
runPython: vi
|
|
39
|
-
.fn()
|
|
40
|
-
.mockImplementation(() => sessions.shift()?.sessionResources),
|
|
41
|
-
loadPackagesFromImports,
|
|
42
|
-
loadedPackages: {},
|
|
43
|
-
runPythonAsync: vi.fn(),
|
|
44
|
-
} as unknown as PyodideInterface;
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
...first,
|
|
48
|
-
loadPackagesFromImports,
|
|
49
|
-
pyodide,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function startSession(controller: TestController, code: string) {
|
|
54
|
-
return controller.startSession({
|
|
55
|
-
code,
|
|
56
|
-
filename: `${code}.py`,
|
|
57
|
-
onMessage: vi.fn(),
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
describe("WASM controller session lifecycle", () => {
|
|
62
|
-
it("stops and releases the active session", async () => {
|
|
63
|
-
const { init, packages, pyodide, sessionResources, stop } =
|
|
64
|
-
createPyodideStub();
|
|
65
|
-
const controller = new TestController();
|
|
66
|
-
controller.setPyodide(pyodide);
|
|
67
|
-
|
|
68
|
-
await startSession(controller, "current");
|
|
69
|
-
await vi.waitFor(() => expect(init).toHaveBeenCalledOnce());
|
|
70
|
-
await controller.stopSession();
|
|
71
|
-
|
|
72
|
-
expect(stop).toHaveBeenCalledOnce();
|
|
73
|
-
expect(stop.destroy).toHaveBeenCalledOnce();
|
|
74
|
-
expect(sessionResources.destroy).toHaveBeenCalledOnce();
|
|
75
|
-
expect(packages.destroy).toHaveBeenCalledOnce();
|
|
76
|
-
expect(init.destroy).toHaveBeenCalledOnce();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("loads dependencies serially and skips superseded sessions", async () => {
|
|
80
|
-
let finishFirstLoad!: () => void;
|
|
81
|
-
const { loadPackagesFromImports, pyodide } = createPyodideStub();
|
|
82
|
-
loadPackagesFromImports
|
|
83
|
-
.mockReturnValueOnce(
|
|
84
|
-
new Promise<void>((resolve) => {
|
|
85
|
-
finishFirstLoad = resolve;
|
|
86
|
-
}),
|
|
87
|
-
)
|
|
88
|
-
.mockResolvedValueOnce(undefined);
|
|
89
|
-
const controller = new TestController();
|
|
90
|
-
controller.setPyodide(pyodide);
|
|
91
|
-
|
|
92
|
-
await startSession(controller, "first_dependency");
|
|
93
|
-
await startSession(controller, "superseded_dependency");
|
|
94
|
-
await controller.stopSession();
|
|
95
|
-
await startSession(controller, "current_dependency");
|
|
96
|
-
|
|
97
|
-
expect(loadPackagesFromImports).toHaveBeenCalledOnce();
|
|
98
|
-
finishFirstLoad();
|
|
99
|
-
await vi.waitFor(() =>
|
|
100
|
-
expect(loadPackagesFromImports).toHaveBeenCalledTimes(2),
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
const loadedSources = loadPackagesFromImports.mock.calls.map(
|
|
104
|
-
([source]) => source as string,
|
|
105
|
-
);
|
|
106
|
-
expect(loadedSources[1]).toContain("current_dependency");
|
|
107
|
-
expect(loadedSources.join("\n")).not.toContain("superseded_dependency");
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("stops every session created for a multi-app page", async () => {
|
|
111
|
-
const first = createSessionResources();
|
|
112
|
-
const second = createSessionResources();
|
|
113
|
-
const { pyodide } = createPyodideStub([first, second]);
|
|
114
|
-
const controller = new TestController();
|
|
115
|
-
controller.setPyodide(pyodide);
|
|
116
|
-
|
|
117
|
-
await startSession(controller, "first");
|
|
118
|
-
await startSession(controller, "second");
|
|
119
|
-
second.bridge.destroy();
|
|
120
|
-
|
|
121
|
-
expect(first.stop.destroy).not.toHaveBeenCalled();
|
|
122
|
-
|
|
123
|
-
await controller.stopSession();
|
|
124
|
-
|
|
125
|
-
expect(first.stop).toHaveBeenCalledOnce();
|
|
126
|
-
expect(second.stop).toHaveBeenCalledOnce();
|
|
127
|
-
expect(first.stop.destroy).toHaveBeenCalledOnce();
|
|
128
|
-
expect(second.stop.destroy).toHaveBeenCalledOnce();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("continues stopping sessions after one stop fails", async () => {
|
|
132
|
-
const failure = new Error("stop failed");
|
|
133
|
-
const first = createSessionResources(() => Promise.reject(failure));
|
|
134
|
-
const second = createSessionResources();
|
|
135
|
-
const { pyodide } = createPyodideStub([first, second]);
|
|
136
|
-
const controller = new TestController();
|
|
137
|
-
controller.setPyodide(pyodide);
|
|
138
|
-
|
|
139
|
-
await startSession(controller, "first");
|
|
140
|
-
await startSession(controller, "second");
|
|
141
|
-
|
|
142
|
-
await expect(controller.stopSession()).rejects.toThrow("stop failed");
|
|
143
|
-
expect(first.stop).toHaveBeenCalledOnce();
|
|
144
|
-
expect(second.stop).toHaveBeenCalledOnce();
|
|
145
|
-
expect(first.stop.destroy).not.toHaveBeenCalled();
|
|
146
|
-
expect(second.stop.destroy).toHaveBeenCalledOnce();
|
|
147
|
-
|
|
148
|
-
first.stop.mockResolvedValueOnce(undefined);
|
|
149
|
-
await controller.stopSession();
|
|
150
|
-
|
|
151
|
-
expect(first.stop).toHaveBeenCalledTimes(2);
|
|
152
|
-
expect(first.stop.destroy).toHaveBeenCalledOnce();
|
|
153
|
-
expect(second.stop).toHaveBeenCalledOnce();
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("reports and retries a falsy stop failure", async () => {
|
|
157
|
-
const session = createSessionResources(() => Promise.reject(false));
|
|
158
|
-
const { pyodide } = createPyodideStub([session]);
|
|
159
|
-
const controller = new TestController();
|
|
160
|
-
controller.setPyodide(pyodide);
|
|
161
|
-
|
|
162
|
-
await startSession(controller, "current");
|
|
163
|
-
|
|
164
|
-
await expect(controller.stopSession()).rejects.toBe(false);
|
|
165
|
-
expect(session.stop.destroy).not.toHaveBeenCalled();
|
|
166
|
-
|
|
167
|
-
session.stop.mockResolvedValueOnce(undefined);
|
|
168
|
-
await controller.stopSession();
|
|
169
|
-
|
|
170
|
-
expect(session.stop).toHaveBeenCalledTimes(2);
|
|
171
|
-
expect(session.stop.destroy).toHaveBeenCalledOnce();
|
|
172
|
-
});
|
|
173
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import { renderHook } from "@testing-library/react";
|
|
3
|
-
import { createStore, Provider } from "jotai";
|
|
4
|
-
import { describe, expect, it, vi } from "vitest";
|
|
5
|
-
import { Functions } from "@/utils/functions";
|
|
6
|
-
import { useHotkey } from "../useHotkey";
|
|
7
|
-
|
|
8
|
-
// global.hideCode is bound to "Mod-." by default; "mod" accepts ctrl or meta.
|
|
9
|
-
const SHORTCUT = "global.hideCode";
|
|
10
|
-
|
|
11
|
-
function createWrapper() {
|
|
12
|
-
const store = createStore();
|
|
13
|
-
return ({ children }: { children: React.ReactNode }) => (
|
|
14
|
-
<Provider store={store}>{children}</Provider>
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function press() {
|
|
19
|
-
const event = new KeyboardEvent("keydown", {
|
|
20
|
-
key: ".",
|
|
21
|
-
ctrlKey: true,
|
|
22
|
-
cancelable: true,
|
|
23
|
-
bubbles: true,
|
|
24
|
-
});
|
|
25
|
-
document.dispatchEvent(event);
|
|
26
|
-
return event;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
describe("useHotkey", () => {
|
|
30
|
-
it("invokes the callback and prevents default", () => {
|
|
31
|
-
const callback = vi.fn();
|
|
32
|
-
renderHook(() => useHotkey(SHORTCUT, callback), {
|
|
33
|
-
wrapper: createWrapper(),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const event = press();
|
|
37
|
-
expect(callback).toHaveBeenCalledOnce();
|
|
38
|
-
expect(event.defaultPrevented).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("does not prevent default when the callback returns false", () => {
|
|
42
|
-
const callback = vi.fn(() => false);
|
|
43
|
-
renderHook(() => useHotkey(SHORTCUT, callback), {
|
|
44
|
-
wrapper: createWrapper(),
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const event = press();
|
|
48
|
-
expect(callback).toHaveBeenCalledOnce();
|
|
49
|
-
expect(event.defaultPrevented).toBe(false);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("still swallows the keystroke for NOOP catch-all handlers", () => {
|
|
53
|
-
renderHook(() => useHotkey(SHORTCUT, Functions.NOOP), {
|
|
54
|
-
wrapper: createWrapper(),
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const event = press();
|
|
58
|
-
expect(event.defaultPrevented).toBe(true);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("neither runs the callback nor prevents default when disabled", () => {
|
|
62
|
-
const callback = vi.fn();
|
|
63
|
-
renderHook(() => useHotkey(SHORTCUT, callback, { disabled: true }), {
|
|
64
|
-
wrapper: createWrapper(),
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const event = press();
|
|
68
|
-
expect(callback).not.toHaveBeenCalled();
|
|
69
|
-
expect(event.defaultPrevented).toBe(false);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("re-enables when disabled flips back to false", () => {
|
|
73
|
-
const callback = vi.fn();
|
|
74
|
-
const { rerender } = renderHook(
|
|
75
|
-
({ disabled }: { disabled: boolean }) =>
|
|
76
|
-
useHotkey(SHORTCUT, callback, { disabled }),
|
|
77
|
-
{ wrapper: createWrapper(), initialProps: { disabled: true } },
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
press();
|
|
81
|
-
expect(callback).not.toHaveBeenCalled();
|
|
82
|
-
|
|
83
|
-
rerender({ disabled: false });
|
|
84
|
-
const event = press();
|
|
85
|
-
expect(callback).toHaveBeenCalledOnce();
|
|
86
|
-
expect(event.defaultPrevented).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { fireEvent, render } from "@testing-library/react";
|
|
4
|
-
import { describe, expect, it } from "vitest";
|
|
5
|
-
import ImageComparisonComponent from "../ImageComparisonComponent";
|
|
6
|
-
|
|
7
|
-
const baseProps = {
|
|
8
|
-
beforeSrc: "before.png",
|
|
9
|
-
afterSrc: "after.png",
|
|
10
|
-
value: 50,
|
|
11
|
-
direction: "horizontal" as const,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
describe("ImageComparisonComponent", () => {
|
|
15
|
-
it("renders the comparison slider with both images", () => {
|
|
16
|
-
const { getByAltText, container } = render(
|
|
17
|
-
<ImageComparisonComponent {...baseProps} />,
|
|
18
|
-
);
|
|
19
|
-
expect(getByAltText("Before")).toBeTruthy();
|
|
20
|
-
expect(getByAltText("After")).toBeTruthy();
|
|
21
|
-
expect(container.querySelector("img-comparison-slider")).toBeTruthy();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("shows a visible error instead of collapsing when an image fails to load", () => {
|
|
25
|
-
const brokenSrc = "https://example.com/does-not-exist.png";
|
|
26
|
-
const { getByAltText, queryByText, container } = render(
|
|
27
|
-
<ImageComparisonComponent {...baseProps} beforeSrc={brokenSrc} />,
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
// No error before the image fails.
|
|
31
|
-
expect(queryByText(/Failed to load/)).toBeNull();
|
|
32
|
-
|
|
33
|
-
fireEvent.error(getByAltText("Before"));
|
|
34
|
-
|
|
35
|
-
// The slider is replaced with a visible error mentioning the source, so
|
|
36
|
-
// the output is never a silent blank.
|
|
37
|
-
expect(container.querySelector("img-comparison-slider")).toBeNull();
|
|
38
|
-
const error = queryByText(/Failed to load image/);
|
|
39
|
-
expect(error).toBeTruthy();
|
|
40
|
-
expect(error?.textContent).toContain(brokenSrc);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("clears the error when the sources change so new images can load", () => {
|
|
44
|
-
const brokenSrc = "https://example.com/does-not-exist.png";
|
|
45
|
-
const { getByAltText, queryByText, container, rerender } = render(
|
|
46
|
-
<ImageComparisonComponent {...baseProps} beforeSrc={brokenSrc} />,
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
fireEvent.error(getByAltText("Before"));
|
|
50
|
-
expect(queryByText(/Failed to load/)).toBeTruthy();
|
|
51
|
-
|
|
52
|
-
// Re-render with a new source (e.g. the user fixed the notebook): the error
|
|
53
|
-
// should clear and the slider should mount again rather than stay stuck.
|
|
54
|
-
rerender(<ImageComparisonComponent {...baseProps} beforeSrc="fixed.png" />);
|
|
55
|
-
expect(queryByText(/Failed to load/)).toBeNull();
|
|
56
|
-
expect(container.querySelector("img-comparison-slider")).toBeTruthy();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("truncates long sources (e.g. data URLs) in the error message", () => {
|
|
60
|
-
const longSrc = `data:image/png;base64,${"A".repeat(200)}`;
|
|
61
|
-
const { getByAltText, queryByText } = render(
|
|
62
|
-
<ImageComparisonComponent {...baseProps} afterSrc={longSrc} />,
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
fireEvent.error(getByAltText("After"));
|
|
66
|
-
|
|
67
|
-
const error = queryByText(/Failed to load image/);
|
|
68
|
-
expect(error?.textContent).toContain("…");
|
|
69
|
-
expect(error?.textContent).not.toContain("A".repeat(200));
|
|
70
|
-
});
|
|
71
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
-
import { configOverridesAtom, userConfigAtom } from "@/core/config/config";
|
|
5
|
-
import { defaultUserConfig } from "@/core/config/config-schema";
|
|
6
|
-
import { store } from "@/core/state/jotai";
|
|
7
|
-
import type { Theme } from "../useTheme";
|
|
8
|
-
import { resolvedThemeAtom, visibleForTesting } from "../useTheme";
|
|
9
|
-
|
|
10
|
-
const { themeFromQueryParam } = visibleForTesting;
|
|
11
|
-
|
|
12
|
-
function setQuery(search: string): void {
|
|
13
|
-
window.history.replaceState({}, "", search === "" ? "/" : `/?${search}`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function setConfigTheme(theme: Theme): void {
|
|
17
|
-
const config = defaultUserConfig();
|
|
18
|
-
store.set(userConfigAtom, {
|
|
19
|
-
...config,
|
|
20
|
-
display: { ...config.display, theme },
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
setQuery("");
|
|
26
|
-
store.set(userConfigAtom, defaultUserConfig());
|
|
27
|
-
store.set(configOverridesAtom, {});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe("themeFromQueryParam", () => {
|
|
31
|
-
it.each(["light", "dark", "system"] as const)(
|
|
32
|
-
"returns the valid theme %s",
|
|
33
|
-
(theme) => {
|
|
34
|
-
setQuery(`theme=${theme}`);
|
|
35
|
-
expect(themeFromQueryParam()).toBe(theme);
|
|
36
|
-
},
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
it("returns undefined when the param is absent", () => {
|
|
40
|
-
setQuery("");
|
|
41
|
-
expect(themeFromQueryParam()).toBeUndefined();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("returns undefined for an invalid value", () => {
|
|
45
|
-
setQuery("theme=blue");
|
|
46
|
-
expect(themeFromQueryParam()).toBeUndefined();
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("resolvedThemeAtom with a theme query param", () => {
|
|
51
|
-
it("uses the saved config theme when no param is present", () => {
|
|
52
|
-
setConfigTheme("dark");
|
|
53
|
-
setQuery("");
|
|
54
|
-
expect(store.get(resolvedThemeAtom)).toBe("dark");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("lets the query param override the saved config theme", () => {
|
|
58
|
-
setConfigTheme("light");
|
|
59
|
-
setQuery("theme=dark");
|
|
60
|
-
expect(store.get(resolvedThemeAtom)).toBe("dark");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("falls back to the config theme for an invalid param value", () => {
|
|
64
|
-
setConfigTheme("dark");
|
|
65
|
-
setQuery("theme=blue");
|
|
66
|
-
expect(store.get(resolvedThemeAtom)).toBe("dark");
|
|
67
|
-
});
|
|
68
|
-
});
|