@marimo-team/frontend 0.23.10-dev25 → 0.23.10-dev26

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.
@@ -0,0 +1,425 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, renderHook } from "@testing-library/react";
4
+ import { describe, expect, it } from "vitest";
5
+ import { cellId } from "@/__tests__/branded";
6
+ import type { CellId } from "@/core/cells/ids";
7
+ import type { RuntimeCell } from "@/core/cells/types";
8
+ import type { SlideConfig } from "../../editor/renderers/slides-layout/types";
9
+ import {
10
+ deckSlideType,
11
+ parkedRendersSource,
12
+ shouldShowCode,
13
+ useParkedPreview,
14
+ } from "../reveal-component";
15
+
16
+ const A = cellId("a");
17
+ const B = cellId("b");
18
+
19
+ const cells = (
20
+ ...entries: Array<[CellId, SlideConfig]>
21
+ ): ReadonlyMap<CellId, SlideConfig> => new Map(entries);
22
+
23
+ // The hook only reads `cell.id`, so a minimal stub is enough. Cast is confined
24
+ // to this test helper (see `@/__tests__/branded` for the same rationale).
25
+ const cell = (id: CellId): RuntimeCell => ({ id }) as RuntimeCell;
26
+
27
+ describe("shouldShowCode", () => {
28
+ it("is off when the code toggle is unavailable, regardless of config/override", () => {
29
+ expect(
30
+ shouldShowCode({
31
+ cells: cells([A, { showCode: true }]),
32
+ cellId: A,
33
+ showCodeOverrides: new Set([A]),
34
+ codeToggleEnabled: false,
35
+ }),
36
+ ).toBe(false);
37
+ });
38
+
39
+ it("is off when there is no active cell", () => {
40
+ expect(
41
+ shouldShowCode({
42
+ cells: cells([A, { showCode: true }]),
43
+ cellId: undefined,
44
+ showCodeOverrides: new Set(),
45
+ codeToggleEnabled: true,
46
+ }),
47
+ ).toBe(false);
48
+ });
49
+
50
+ it("follows the persisted config when there is no override", () => {
51
+ expect(
52
+ shouldShowCode({
53
+ cells: cells([A, { showCode: true }]),
54
+ cellId: A,
55
+ showCodeOverrides: new Set(),
56
+ codeToggleEnabled: true,
57
+ }),
58
+ ).toBe(true);
59
+ // Missing config entry defaults to off.
60
+ expect(
61
+ shouldShowCode({
62
+ cells: cells(),
63
+ cellId: A,
64
+ showCodeOverrides: new Set(),
65
+ codeToggleEnabled: true,
66
+ }),
67
+ ).toBe(false);
68
+ });
69
+
70
+ it("shows code when either the config or the override is set (logical OR)", () => {
71
+ // Peek: config unset, override present -> shown.
72
+ expect(
73
+ shouldShowCode({
74
+ cells: cells(),
75
+ cellId: A,
76
+ showCodeOverrides: new Set([A]),
77
+ codeToggleEnabled: true,
78
+ }),
79
+ ).toBe(true);
80
+ // Configured + override present -> still shown; the override never hides.
81
+ expect(
82
+ shouldShowCode({
83
+ cells: cells([A, { showCode: true }]),
84
+ cellId: A,
85
+ showCodeOverrides: new Set([A]),
86
+ codeToggleEnabled: true,
87
+ }),
88
+ ).toBe(true);
89
+ });
90
+ });
91
+
92
+ describe("deckSlideType", () => {
93
+ const NONE_IDS: ReadonlySet<CellId> = new Set();
94
+
95
+ it("uses the configured type for a normal cell", () => {
96
+ expect(
97
+ deckSlideType({
98
+ cell: cell(A),
99
+ noOutputIds: NONE_IDS,
100
+ heldEditCellId: null,
101
+ slideConfigs: cells([A, { type: "fragment" }]),
102
+ }),
103
+ ).toBe("fragment");
104
+ });
105
+
106
+ it("defaults to a top-level slide when no type is configured", () => {
107
+ expect(
108
+ deckSlideType({
109
+ cell: cell(A),
110
+ noOutputIds: NONE_IDS,
111
+ heldEditCellId: null,
112
+ slideConfigs: cells(),
113
+ }),
114
+ ).toBe("slide");
115
+ });
116
+
117
+ it("drops output-less cells from the deck", () => {
118
+ expect(
119
+ deckSlideType({
120
+ cell: cell(A),
121
+ noOutputIds: new Set([A]),
122
+ heldEditCellId: null,
123
+ // Even an explicit type is overridden by the skip.
124
+ slideConfigs: cells([A, { type: "sub-slide" }]),
125
+ }),
126
+ ).toBe("skip");
127
+ });
128
+
129
+ it("drops the held-edit cell so it isn't mounted twice", () => {
130
+ expect(
131
+ deckSlideType({
132
+ cell: cell(A),
133
+ noOutputIds: NONE_IDS,
134
+ heldEditCellId: A,
135
+ slideConfigs: cells([A, { type: "slide" }]),
136
+ }),
137
+ ).toBe("skip");
138
+ });
139
+ });
140
+
141
+ describe("parkedRendersSource", () => {
142
+ it("follows `showCode` for a cell that has output", () => {
143
+ expect(
144
+ parkedRendersSource({
145
+ isNoOutputPreview: false,
146
+ isEditable: false,
147
+ showCode: true,
148
+ }),
149
+ ).toBe(true);
150
+ expect(
151
+ parkedRendersSource({
152
+ isNoOutputPreview: false,
153
+ isEditable: true,
154
+ showCode: false,
155
+ }),
156
+ ).toBe(false);
157
+ });
158
+
159
+ it("falls back to source for an editable no-output cell even when showCode is off", () => {
160
+ expect(
161
+ parkedRendersSource({
162
+ isNoOutputPreview: true,
163
+ isEditable: true,
164
+ showCode: false,
165
+ }),
166
+ ).toBe(true);
167
+ });
168
+
169
+ it("renders output for a read-only no-output cell with showCode off", () => {
170
+ expect(
171
+ parkedRendersSource({
172
+ isNoOutputPreview: true,
173
+ isEditable: false,
174
+ showCode: false,
175
+ }),
176
+ ).toBe(false);
177
+ });
178
+ });
179
+
180
+ describe("useParkedPreview", () => {
181
+ const NO_CONFIG = cells();
182
+ const NONE = new Set<CellId>();
183
+
184
+ it("is inert for a rendered cell with output", () => {
185
+ const { result } = renderHook(() =>
186
+ useParkedPreview({
187
+ activeCell: cell(A),
188
+ slideConfigs: NO_CONFIG,
189
+ noOutputIds: NONE,
190
+ }),
191
+ );
192
+ expect(result.current).toEqual({
193
+ parkedPreviewCell: null,
194
+ isHeldEdit: false,
195
+ isNoOutputPreview: false,
196
+ heldEditCellId: null,
197
+ heldShowsCode: true,
198
+ toggleHeldShowsCode: expect.any(Function),
199
+ });
200
+ });
201
+
202
+ it("is inert when there is no active cell", () => {
203
+ const { result } = renderHook(() =>
204
+ useParkedPreview({
205
+ activeCell: undefined,
206
+ slideConfigs: NO_CONFIG,
207
+ noOutputIds: NONE,
208
+ }),
209
+ );
210
+ expect(result.current).toEqual({
211
+ parkedPreviewCell: null,
212
+ isHeldEdit: false,
213
+ isNoOutputPreview: false,
214
+ heldEditCellId: null,
215
+ heldShowsCode: true,
216
+ toggleHeldShowsCode: expect.any(Function),
217
+ });
218
+ });
219
+
220
+ it("parks a skipped cell without flagging it as a no-output preview", () => {
221
+ const { result } = renderHook(() =>
222
+ useParkedPreview({
223
+ activeCell: cell(A),
224
+ slideConfigs: cells([A, { type: "skip" }]),
225
+ noOutputIds: NONE,
226
+ }),
227
+ );
228
+ expect(result.current).toEqual({
229
+ parkedPreviewCell: cell(A),
230
+ isHeldEdit: false,
231
+ isNoOutputPreview: false,
232
+ heldEditCellId: null,
233
+ heldShowsCode: true,
234
+ toggleHeldShowsCode: expect.any(Function),
235
+ });
236
+ });
237
+
238
+ it("parks an output-less cell as a no-output preview", () => {
239
+ const { result } = renderHook(() =>
240
+ useParkedPreview({
241
+ activeCell: cell(A),
242
+ slideConfigs: NO_CONFIG,
243
+ noOutputIds: new Set([A]),
244
+ }),
245
+ );
246
+ expect(result.current).toEqual({
247
+ parkedPreviewCell: cell(A),
248
+ isHeldEdit: false,
249
+ isNoOutputPreview: true,
250
+ heldEditCellId: null,
251
+ heldShowsCode: true,
252
+ toggleHeldShowsCode: expect.any(Function),
253
+ });
254
+ });
255
+
256
+ it("holds the cell in the overlay once it gains output", () => {
257
+ const { result, rerender } = renderHook(
258
+ (props: Parameters<typeof useParkedPreview>[0]) =>
259
+ useParkedPreview(props),
260
+ {
261
+ initialProps: {
262
+ activeCell: cell(A),
263
+ slideConfigs: NO_CONFIG,
264
+ noOutputIds: new Set([A]),
265
+ },
266
+ },
267
+ );
268
+ expect(result.current.isNoOutputPreview).toBe(true);
269
+ expect(result.current.isHeldEdit).toBe(false);
270
+
271
+ // Same cell, now with output: keep it parked so the editor isn't remounted.
272
+ rerender({
273
+ activeCell: cell(A),
274
+ slideConfigs: NO_CONFIG,
275
+ noOutputIds: NONE,
276
+ });
277
+ expect(result.current).toEqual({
278
+ parkedPreviewCell: cell(A),
279
+ isHeldEdit: true,
280
+ isNoOutputPreview: false,
281
+ heldEditCellId: A,
282
+ heldShowsCode: true,
283
+ toggleHeldShowsCode: expect.any(Function),
284
+ });
285
+ });
286
+
287
+ it("releases the hold when a different cell becomes active", () => {
288
+ const { result, rerender } = renderHook(
289
+ (props: Parameters<typeof useParkedPreview>[0]) =>
290
+ useParkedPreview(props),
291
+ {
292
+ initialProps: {
293
+ activeCell: cell(A),
294
+ slideConfigs: NO_CONFIG,
295
+ noOutputIds: new Set([A]),
296
+ },
297
+ },
298
+ );
299
+ // Arm the hold, then let A gain output (held).
300
+ rerender({
301
+ activeCell: cell(A),
302
+ slideConfigs: NO_CONFIG,
303
+ noOutputIds: NONE,
304
+ });
305
+ expect(result.current.isHeldEdit).toBe(true);
306
+
307
+ // Navigate to a rendered B: the hold on A is released.
308
+ rerender({
309
+ activeCell: cell(B),
310
+ slideConfigs: NO_CONFIG,
311
+ noOutputIds: NONE,
312
+ });
313
+ expect(result.current).toEqual({
314
+ parkedPreviewCell: null,
315
+ isHeldEdit: false,
316
+ isNoOutputPreview: false,
317
+ heldEditCellId: null,
318
+ heldShowsCode: true,
319
+ toggleHeldShowsCode: expect.any(Function),
320
+ });
321
+ });
322
+
323
+ it("releases a skipped cell into the deck as soon as it is un-skipped", () => {
324
+ const { result, rerender } = renderHook(
325
+ (props: Parameters<typeof useParkedPreview>[0]) =>
326
+ useParkedPreview(props),
327
+ {
328
+ initialProps: {
329
+ activeCell: cell(A),
330
+ slideConfigs: cells([A, { type: "skip" }]),
331
+ noOutputIds: NONE,
332
+ },
333
+ },
334
+ );
335
+ expect(result.current.parkedPreviewCell).toEqual(cell(A));
336
+
337
+ // Un-skip the still-active cell: it has output, so it must rejoin the deck
338
+ // immediately rather than staying held in the overlay until navigation.
339
+ rerender({
340
+ activeCell: cell(A),
341
+ slideConfigs: NO_CONFIG,
342
+ noOutputIds: NONE,
343
+ });
344
+ expect(result.current).toEqual({
345
+ parkedPreviewCell: null,
346
+ isHeldEdit: false,
347
+ isNoOutputPreview: false,
348
+ heldEditCellId: null,
349
+ heldShowsCode: true,
350
+ toggleHeldShowsCode: expect.any(Function),
351
+ });
352
+ });
353
+
354
+ it("shows the held editor by default and toggles it off without navigating", () => {
355
+ const { result, rerender } = renderHook(
356
+ (props: Parameters<typeof useParkedPreview>[0]) =>
357
+ useParkedPreview(props),
358
+ {
359
+ initialProps: {
360
+ activeCell: cell(A),
361
+ slideConfigs: NO_CONFIG,
362
+ noOutputIds: new Set([A]),
363
+ },
364
+ },
365
+ );
366
+ // A gains output: held, with its editor shown by default.
367
+ rerender({
368
+ activeCell: cell(A),
369
+ slideConfigs: NO_CONFIG,
370
+ noOutputIds: NONE,
371
+ });
372
+ expect(result.current.isHeldEdit).toBe(true);
373
+ expect(result.current.heldShowsCode).toBe(true);
374
+
375
+ // The `C` toggle hides the editor in place, no navigation required.
376
+ act(() => {
377
+ result.current.toggleHeldShowsCode();
378
+ });
379
+ expect(result.current.heldShowsCode).toBe(false);
380
+
381
+ // Toggling again brings it back.
382
+ act(() => {
383
+ result.current.toggleHeldShowsCode();
384
+ });
385
+ expect(result.current.heldShowsCode).toBe(true);
386
+ });
387
+
388
+ it("resets held code visibility when a new cell takes the held slot", () => {
389
+ const { result, rerender } = renderHook(
390
+ (props: Parameters<typeof useParkedPreview>[0]) =>
391
+ useParkedPreview(props),
392
+ {
393
+ initialProps: {
394
+ activeCell: cell(A),
395
+ slideConfigs: NO_CONFIG,
396
+ noOutputIds: new Set([A]),
397
+ },
398
+ },
399
+ );
400
+ rerender({
401
+ activeCell: cell(A),
402
+ slideConfigs: NO_CONFIG,
403
+ noOutputIds: NONE,
404
+ });
405
+ act(() => {
406
+ result.current.toggleHeldShowsCode();
407
+ });
408
+ expect(result.current.heldShowsCode).toBe(false);
409
+
410
+ // Move to a fresh output-less cell B, then let it gain output (held).
411
+ rerender({
412
+ activeCell: cell(B),
413
+ slideConfigs: NO_CONFIG,
414
+ noOutputIds: new Set([B]),
415
+ });
416
+ rerender({
417
+ activeCell: cell(B),
418
+ slideConfigs: NO_CONFIG,
419
+ noOutputIds: NONE,
420
+ });
421
+ expect(result.current.heldEditCellId).toBe(B);
422
+ // The new cell starts with its editor visible again.
423
+ expect(result.current.heldShowsCode).toBe(true);
424
+ });
425
+ });