@marimo-team/islands 0.23.15-dev1 → 0.23.15-dev10
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/{chat-ui-DinOvbWu.js → chat-ui-FiwuOgQU.js} +3094 -3094
- package/dist/{code-visibility-PV_v9dwf.js → code-visibility-CyTw8jup.js} +171 -171
- package/dist/{html-to-image-_wGfk8V-.js → html-to-image-JXL8cvmt.js} +2218 -2205
- package/dist/main.js +310 -306
- package/dist/{process-output-Mh4UrjwM.js → process-output-CnDIXtM-.js} +1 -1
- package/dist/{reveal-component-DVEmgnsr.js → reveal-component-CgdFrza3.js} +2 -2
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/chat/__tests__/chat-utils.test.ts +244 -1
- package/src/components/chat/__tests__/message-queue.test.tsx +121 -0
- package/src/components/chat/chat-panel.tsx +196 -67
- package/src/components/chat/chat-utils.ts +111 -2
- package/src/components/editor/SortableCell.tsx +6 -2
- package/src/components/editor/__tests__/output-persistence.test.tsx +241 -0
- package/src/components/editor/cell/cell-context-menu.tsx +7 -0
- package/src/components/editor/columns/__tests__/cell-column.test.tsx +105 -0
- package/src/components/editor/columns/cell-column.tsx +34 -4
- package/src/components/editor/columns/sortable-column.tsx +24 -4
- package/src/components/editor/notebook-cell.tsx +203 -106
- package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +66 -0
- package/src/components/editor/renderers/cell-array.tsx +26 -13
- package/src/components/editor/renderers/cells-renderer.tsx +8 -2
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
- package/src/core/cells/__tests__/utils.test.ts +59 -0
- package/src/core/cells/utils.ts +51 -0
- package/src/css/app/Cell.css +10 -0
- package/src/hooks/__tests__/useHotkey.test.tsx +88 -0
- package/src/hooks/useHotkey.ts +29 -4
- package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +71 -3
- package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +1 -0
|
@@ -33,11 +33,14 @@ import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
|
|
|
33
33
|
import { MarkdownLanguageAdapter } from "@/core/codemirror/language/languages/markdown";
|
|
34
34
|
import { useResolvedMarimoConfig } from "@/core/config/config";
|
|
35
35
|
import { CSSClasses, KnownQueryParams } from "@/core/constants";
|
|
36
|
-
import type {
|
|
36
|
+
import type { OutputMessage } from "@/core/kernel/messages";
|
|
37
37
|
import { kernelStateAtom } from "@/core/kernel/state";
|
|
38
38
|
import { useNotebookCodeAvailable } from "@/core/meta/code-visibility";
|
|
39
39
|
import { showCodeInRunModeAtom } from "@/core/meta/state";
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
publishedCellClasses,
|
|
42
|
+
shouldHidePublishedCell,
|
|
43
|
+
} from "@/core/cells/utils";
|
|
41
44
|
import { type AppMode, kioskModeAtom } from "@/core/mode";
|
|
42
45
|
import { useRequestClient } from "@/core/network/requests";
|
|
43
46
|
import type { CellConfig } from "@/core/network/types";
|
|
@@ -362,12 +365,13 @@ const VerticalCell = memo(
|
|
|
362
365
|
const className = cn(
|
|
363
366
|
"marimo-cell",
|
|
364
367
|
"hover-actions-parent empty:invisible",
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
368
|
+
published
|
|
369
|
+
? publishedCellClasses({ errored, stopped })
|
|
370
|
+
: {
|
|
371
|
+
"has-error": errored,
|
|
372
|
+
stopped: stopped,
|
|
373
|
+
borderless: isPureMarkdown,
|
|
374
|
+
},
|
|
371
375
|
);
|
|
372
376
|
|
|
373
377
|
// Read mode and show code
|
|
@@ -419,19 +423,15 @@ const VerticalCell = memo(
|
|
|
419
423
|
);
|
|
420
424
|
}
|
|
421
425
|
|
|
422
|
-
const outputIsError = isErrorMime(output?.mimetype);
|
|
423
426
|
// When show_tracebacks is enabled, show error outputs inline
|
|
424
427
|
// instead of hiding them
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
output
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
);
|
|
433
|
-
const hidden =
|
|
434
|
-
(errored || interrupted || stopped || outputIsError) && !hasTraceback;
|
|
428
|
+
const hidden = shouldHidePublishedCell({
|
|
429
|
+
errored,
|
|
430
|
+
interrupted,
|
|
431
|
+
stopped,
|
|
432
|
+
output,
|
|
433
|
+
showErrorTracebacks,
|
|
434
|
+
});
|
|
435
435
|
if (hidden) {
|
|
436
436
|
return null;
|
|
437
437
|
}
|
|
@@ -8,11 +8,13 @@ import {
|
|
|
8
8
|
} from "@/core/cells/cells";
|
|
9
9
|
import { CellId } from "@/core/cells/ids";
|
|
10
10
|
import type { CellData, CellRuntimeState } from "@/core/cells/types";
|
|
11
|
+
import type { OutputMessage } from "@/core/kernel/messages";
|
|
11
12
|
import { MultiColumn } from "@/utils/id-tree";
|
|
12
13
|
import {
|
|
13
14
|
disabledCellIds,
|
|
14
15
|
enabledCellIds,
|
|
15
16
|
isUninstantiated,
|
|
17
|
+
shouldHidePublishedCell,
|
|
16
18
|
staleCellIds,
|
|
17
19
|
} from "../utils";
|
|
18
20
|
|
|
@@ -426,3 +428,60 @@ describe("enabledCellIds", () => {
|
|
|
426
428
|
expect(result).toEqual([cellId2, cellId3]);
|
|
427
429
|
});
|
|
428
430
|
});
|
|
431
|
+
|
|
432
|
+
describe("shouldHidePublishedCell", () => {
|
|
433
|
+
const base = {
|
|
434
|
+
errored: false,
|
|
435
|
+
interrupted: false,
|
|
436
|
+
stopped: false,
|
|
437
|
+
output: null,
|
|
438
|
+
showErrorTracebacks: false,
|
|
439
|
+
};
|
|
440
|
+
const errorOutput = (traceback?: string): OutputMessage => ({
|
|
441
|
+
channel: "marimo-error",
|
|
442
|
+
mimetype: "application/vnd.marimo+error",
|
|
443
|
+
data: [
|
|
444
|
+
{ type: "exception", exception_type: "ValueError", msg: "!", traceback },
|
|
445
|
+
],
|
|
446
|
+
timestamp: 0,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("does not hide a healthy cell", () => {
|
|
450
|
+
expect(shouldHidePublishedCell(base)).toBe(false);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it.each(["errored", "interrupted", "stopped"] as const)(
|
|
454
|
+
"hides a %s cell",
|
|
455
|
+
(key) => {
|
|
456
|
+
expect(shouldHidePublishedCell({ ...base, [key]: true })).toBe(true);
|
|
457
|
+
},
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
it("hides error outputs", () => {
|
|
461
|
+
expect(shouldHidePublishedCell({ ...base, output: errorOutput() })).toBe(
|
|
462
|
+
true,
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("shows error outputs with a traceback when show_tracebacks is enabled", () => {
|
|
467
|
+
expect(
|
|
468
|
+
shouldHidePublishedCell({
|
|
469
|
+
...base,
|
|
470
|
+
errored: true,
|
|
471
|
+
output: errorOutput("Traceback (most recent call last) ..."),
|
|
472
|
+
showErrorTracebacks: true,
|
|
473
|
+
}),
|
|
474
|
+
).toBe(false);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("hides error outputs without a traceback even when show_tracebacks is enabled", () => {
|
|
478
|
+
expect(
|
|
479
|
+
shouldHidePublishedCell({
|
|
480
|
+
...base,
|
|
481
|
+
errored: true,
|
|
482
|
+
output: errorOutput(),
|
|
483
|
+
showErrorTracebacks: true,
|
|
484
|
+
}),
|
|
485
|
+
).toBe(true);
|
|
486
|
+
});
|
|
487
|
+
});
|
package/src/core/cells/utils.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import type { EditorView } from "@codemirror/view";
|
|
4
4
|
import { Objects } from "@/utils/objects";
|
|
5
|
+
import type { MarimoError, OutputMessage } from "../kernel/messages";
|
|
6
|
+
import { isErrorMime } from "../mime";
|
|
5
7
|
import type { RuntimeState } from "../network/types";
|
|
6
8
|
import type { NotebookState } from "./cells";
|
|
7
9
|
import type { CellId } from "./ids";
|
|
@@ -196,3 +198,52 @@ export function cellStatusClasses({
|
|
|
196
198
|
stale: status === "disabled-transitively",
|
|
197
199
|
};
|
|
198
200
|
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Status classes for a published cell (read or present mode, output only).
|
|
204
|
+
*/
|
|
205
|
+
export function publishedCellClasses({
|
|
206
|
+
errored,
|
|
207
|
+
stopped,
|
|
208
|
+
}: {
|
|
209
|
+
errored: boolean;
|
|
210
|
+
stopped: boolean;
|
|
211
|
+
}) {
|
|
212
|
+
return {
|
|
213
|
+
published: true,
|
|
214
|
+
"has-error": errored,
|
|
215
|
+
stopped: stopped,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Whether a published cell (read or present mode) should be hidden.
|
|
221
|
+
*
|
|
222
|
+
* Errored, interrupted, and stopped cells (and error outputs) are hidden in
|
|
223
|
+
* published views, unless `show_tracebacks` is enabled and the output carries
|
|
224
|
+
* an exception traceback to display inline.
|
|
225
|
+
*/
|
|
226
|
+
export function shouldHidePublishedCell({
|
|
227
|
+
errored,
|
|
228
|
+
interrupted,
|
|
229
|
+
stopped,
|
|
230
|
+
output,
|
|
231
|
+
showErrorTracebacks,
|
|
232
|
+
}: {
|
|
233
|
+
errored: boolean;
|
|
234
|
+
interrupted: boolean;
|
|
235
|
+
stopped: boolean;
|
|
236
|
+
output: OutputMessage | null;
|
|
237
|
+
showErrorTracebacks: boolean;
|
|
238
|
+
}): boolean {
|
|
239
|
+
const outputIsError = isErrorMime(output?.mimetype);
|
|
240
|
+
const hasTraceback =
|
|
241
|
+
showErrorTracebacks &&
|
|
242
|
+
outputIsError &&
|
|
243
|
+
Array.isArray(output?.data) &&
|
|
244
|
+
output.data.some(
|
|
245
|
+
(e: MarimoError) =>
|
|
246
|
+
e.type === "exception" && "traceback" in e && e.traceback,
|
|
247
|
+
);
|
|
248
|
+
return (errored || interrupted || stopped || outputIsError) && !hasTraceback;
|
|
249
|
+
}
|
package/src/css/app/Cell.css
CHANGED
|
@@ -255,6 +255,12 @@
|
|
|
255
255
|
border: none;
|
|
256
256
|
box-shadow: none;
|
|
257
257
|
|
|
258
|
+
/* No dividers between sections. Tailwind v4's divide-y is structural
|
|
259
|
+
* (`> :not(:last-child)`), so hidden-but-mounted children (e.g. the tray
|
|
260
|
+
* while presenting) still count as siblings and would otherwise draw a
|
|
261
|
+
* stray rule under the output. */
|
|
262
|
+
@apply divide-y-0;
|
|
263
|
+
|
|
258
264
|
.output-area {
|
|
259
265
|
/* flow-root interferes with margin collapsing, but appears to be unneeded
|
|
260
266
|
* when cell outlines are hidden; clear:both is sufficient.
|
|
@@ -443,6 +449,10 @@
|
|
|
443
449
|
/* Set a fixed gutter width to ensure the hover area is always wide enough */
|
|
444
450
|
/* And make the gutter bigger when the window is super wide */
|
|
445
451
|
--gutter-width: 100px;
|
|
452
|
+
|
|
453
|
+
/* Vertical gap between notebook cells (gap-5). Overridable via custom css;
|
|
454
|
+
* zeroed while presenting, when published cells flow together. */
|
|
455
|
+
--notebook-cell-gap: 1.25rem;
|
|
446
456
|
}
|
|
447
457
|
|
|
448
458
|
/* ------------------------ CodeMirror Editor ----------------------------- */
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
});
|
package/src/hooks/useHotkey.ts
CHANGED
|
@@ -17,13 +17,31 @@ type HotkeyHandler = (
|
|
|
17
17
|
// oxlint-disable-next-line typescript/no-invalid-void-type
|
|
18
18
|
) => boolean | void | undefined | Promise<void>;
|
|
19
19
|
|
|
20
|
+
interface HotkeyOptions {
|
|
21
|
+
/**
|
|
22
|
+
* If true, the hotkey is not handled at all: the callback does not run, the
|
|
23
|
+
* event's default is not prevented, and the action is not registered in the
|
|
24
|
+
* shortcut registry.
|
|
25
|
+
*
|
|
26
|
+
* This differs from passing `Functions.NOOP` as the callback, which still
|
|
27
|
+
* swallows the keystroke (preventDefault) to suppress native browser/OS
|
|
28
|
+
* behavior.
|
|
29
|
+
*/
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
/**
|
|
21
34
|
* Registers a hotkey listener for the given shortcut.
|
|
22
35
|
*
|
|
23
36
|
* @param callback The callback to run when the shortcut is pressed. It does not need to be memoized as this hook will always use the latest callback.
|
|
24
37
|
* If the callback returns false, preventDefault will not be called on the event.
|
|
25
38
|
*/
|
|
26
|
-
export function useHotkey(
|
|
39
|
+
export function useHotkey(
|
|
40
|
+
shortcut: HotkeyAction,
|
|
41
|
+
callback: HotkeyHandler,
|
|
42
|
+
options: HotkeyOptions = {},
|
|
43
|
+
) {
|
|
44
|
+
const { disabled = false } = options;
|
|
27
45
|
const { registerAction, unregisterAction } = useSetRegisteredAction();
|
|
28
46
|
const hotkeys = useAtomValue(hotkeysAtom);
|
|
29
47
|
|
|
@@ -31,7 +49,7 @@ export function useHotkey(shortcut: HotkeyAction, callback: HotkeyHandler) {
|
|
|
31
49
|
const memoizeCallback = useEvent((evt?: KeyboardEvent) => callback(evt));
|
|
32
50
|
|
|
33
51
|
const listener = useEvent((e: KeyboardEvent) => {
|
|
34
|
-
if (e.defaultPrevented) {
|
|
52
|
+
if (disabled || e.defaultPrevented) {
|
|
35
53
|
return;
|
|
36
54
|
}
|
|
37
55
|
|
|
@@ -51,11 +69,18 @@ export function useHotkey(shortcut: HotkeyAction, callback: HotkeyHandler) {
|
|
|
51
69
|
|
|
52
70
|
// Register with the shortcut registry
|
|
53
71
|
useEffect(() => {
|
|
54
|
-
if (!isNOOP) {
|
|
72
|
+
if (!isNOOP && !disabled) {
|
|
55
73
|
registerAction(shortcut, memoizeCallback);
|
|
56
74
|
return () => unregisterAction(shortcut);
|
|
57
75
|
}
|
|
58
|
-
}, [
|
|
76
|
+
}, [
|
|
77
|
+
memoizeCallback,
|
|
78
|
+
shortcut,
|
|
79
|
+
isNOOP,
|
|
80
|
+
disabled,
|
|
81
|
+
registerAction,
|
|
82
|
+
unregisterAction,
|
|
83
|
+
]);
|
|
59
84
|
}
|
|
60
85
|
|
|
61
86
|
/**
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import
|
|
4
|
-
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
type Data,
|
|
5
|
+
MatplotlibRenderer,
|
|
6
|
+
type MatplotlibState,
|
|
7
|
+
visibleForTesting,
|
|
8
|
+
} from "../matplotlib-renderer";
|
|
5
9
|
|
|
6
10
|
const {
|
|
7
11
|
pixelToData,
|
|
@@ -185,3 +189,67 @@ describe("isInAxes", () => {
|
|
|
185
189
|
expect(isInAxes({ x: 300, y: 351 }, LINEAR_AXES)).toBe(false);
|
|
186
190
|
});
|
|
187
191
|
});
|
|
192
|
+
|
|
193
|
+
describe("MatplotlibRenderer", () => {
|
|
194
|
+
afterEach(() => {
|
|
195
|
+
vi.unstubAllGlobals();
|
|
196
|
+
vi.restoreAllMocks();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("restores a new selection when the chart changes", () => {
|
|
200
|
+
vi.stubGlobal(
|
|
201
|
+
"matchMedia",
|
|
202
|
+
vi.fn(() => ({ addEventListener: vi.fn() })),
|
|
203
|
+
);
|
|
204
|
+
vi.stubGlobal(
|
|
205
|
+
"requestAnimationFrame",
|
|
206
|
+
vi.fn(() => 1),
|
|
207
|
+
);
|
|
208
|
+
vi.stubGlobal("cancelAnimationFrame", vi.fn());
|
|
209
|
+
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null);
|
|
210
|
+
|
|
211
|
+
const value = {
|
|
212
|
+
type: "box",
|
|
213
|
+
has_selection: true,
|
|
214
|
+
data: { x_min: 2, x_max: 4, y_min: 2, y_max: 4 },
|
|
215
|
+
} as const;
|
|
216
|
+
const state: MatplotlibState = {
|
|
217
|
+
...LINEAR_AXES,
|
|
218
|
+
axesPixelBounds: [0, 0, 100, 100],
|
|
219
|
+
yBounds: [0, 10],
|
|
220
|
+
chartBase64: "first",
|
|
221
|
+
width: 100,
|
|
222
|
+
height: 100,
|
|
223
|
+
selectionColor: "blue",
|
|
224
|
+
selectionOpacity: 0.15,
|
|
225
|
+
strokeWidth: 2,
|
|
226
|
+
debounce: false,
|
|
227
|
+
value: { has_selection: false },
|
|
228
|
+
setValue: vi.fn(),
|
|
229
|
+
};
|
|
230
|
+
const container = document.createElement("div");
|
|
231
|
+
const controller = new AbortController();
|
|
232
|
+
const renderer = new MatplotlibRenderer(container, {
|
|
233
|
+
state,
|
|
234
|
+
signal: controller.signal,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Cell reruns reuse the renderer, updating the chart and initial value together.
|
|
238
|
+
renderer.update({ ...state, chartBase64: "second", value });
|
|
239
|
+
|
|
240
|
+
const canvas = container.querySelector("canvas");
|
|
241
|
+
expect(canvas).not.toBeNull();
|
|
242
|
+
if (!canvas) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
vi.spyOn(canvas, "getBoundingClientRect").mockReturnValue(
|
|
246
|
+
DOMRect.fromRect({ width: 100, height: 100 }),
|
|
247
|
+
);
|
|
248
|
+
canvas.dispatchEvent(
|
|
249
|
+
new MouseEvent("pointermove", { clientX: 30, clientY: 70 }),
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
expect(canvas.style.cursor).toBe("move");
|
|
253
|
+
controller.abort();
|
|
254
|
+
});
|
|
255
|
+
});
|