/* Copyright 2026 Marimo. All rights reserved. */ import { act, renderHook } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { cellId } from "@/__tests__/branded"; import type { CellId } from "@/core/cells/ids"; import type { RuntimeCell } from "@/core/cells/types"; import type { SlideConfig } from "../../editor/renderers/slides-layout/types"; import { deckSlideType, parkedRendersSource, shouldShowCode, syncCodeToggleKeyBinding, useParkedPreview, } from "../reveal-component"; const A = cellId("a"); const B = cellId("b"); const cells = ( ...entries: Array<[CellId, SlideConfig]> ): ReadonlyMap => new Map(entries); // The hook only reads `cell.id`, so a minimal stub is enough. Cast is confined // to this test helper (see `@/__tests__/branded` for the same rationale). const cell = (id: CellId): RuntimeCell => ({ id }) as RuntimeCell; describe("syncCodeToggleKeyBinding", () => { it("tracks whether code is available after Reveal is ready", () => { const deck = { addKeyBinding: vi.fn(), removeKeyBinding: vi.fn(), }; const onToggle = vi.fn(); syncCodeToggleKeyBinding({ deck, enabled: false, onToggle }); expect(deck.removeKeyBinding).toHaveBeenLastCalledWith(67); expect(deck.addKeyBinding).not.toHaveBeenCalled(); syncCodeToggleKeyBinding({ deck, enabled: true, onToggle }); expect(deck.addKeyBinding).toHaveBeenCalledWith( { keyCode: 67, key: "C", description: "Toggle code editor", }, onToggle, ); }); }); describe("shouldShowCode", () => { it("is off when code is unavailable, regardless of config/override", () => { expect( shouldShowCode({ cells: cells([A, { showCode: true }]), cellId: A, showCodeOverrides: new Set([A]), codeAvailable: false, }), ).toBe(false); }); it("is off when there is no active cell", () => { expect( shouldShowCode({ cells: cells([A, { showCode: true }]), cellId: undefined, showCodeOverrides: new Set(), codeAvailable: true, }), ).toBe(false); }); it("follows the persisted config when there is no override", () => { expect( shouldShowCode({ cells: cells([A, { showCode: true }]), cellId: A, showCodeOverrides: new Set(), codeAvailable: true, }), ).toBe(true); // Missing config entry defaults to off. expect( shouldShowCode({ cells: cells(), cellId: A, showCodeOverrides: new Set(), codeAvailable: true, }), ).toBe(false); }); it("shows code when either the config or the override is set (logical OR)", () => { // Peek: config unset, override present -> shown. expect( shouldShowCode({ cells: cells(), cellId: A, showCodeOverrides: new Set([A]), codeAvailable: true, }), ).toBe(true); // Configured + override present -> still shown; the override never hides. expect( shouldShowCode({ cells: cells([A, { showCode: true }]), cellId: A, showCodeOverrides: new Set([A]), codeAvailable: true, }), ).toBe(true); }); }); describe("deckSlideType", () => { const NONE_IDS: ReadonlySet = new Set(); it("uses the configured type for a normal cell", () => { expect( deckSlideType({ cell: cell(A), noOutputIds: NONE_IDS, heldEditCellId: null, slideConfigs: cells([A, { type: "fragment" }]), }), ).toBe("fragment"); }); it("defaults to a top-level slide when no type is configured", () => { expect( deckSlideType({ cell: cell(A), noOutputIds: NONE_IDS, heldEditCellId: null, slideConfigs: cells(), }), ).toBe("slide"); }); it("drops output-less cells from the deck", () => { expect( deckSlideType({ cell: cell(A), noOutputIds: new Set([A]), heldEditCellId: null, // Even an explicit type is overridden by the skip. slideConfigs: cells([A, { type: "sub-slide" }]), }), ).toBe("skip"); }); it("drops the held-edit cell so it isn't mounted twice", () => { expect( deckSlideType({ cell: cell(A), noOutputIds: NONE_IDS, heldEditCellId: A, slideConfigs: cells([A, { type: "slide" }]), }), ).toBe("skip"); }); }); describe("parkedRendersSource", () => { it("follows `showCode` for a cell that has output", () => { expect( parkedRendersSource({ isNoOutputPreview: false, isEditable: false, showCode: true, }), ).toBe(true); expect( parkedRendersSource({ isNoOutputPreview: false, isEditable: true, showCode: false, }), ).toBe(false); }); it("falls back to source for an editable no-output cell even when showCode is off", () => { expect( parkedRendersSource({ isNoOutputPreview: true, isEditable: true, showCode: false, }), ).toBe(true); }); it("renders output for a read-only no-output cell with showCode off", () => { expect( parkedRendersSource({ isNoOutputPreview: true, isEditable: false, showCode: false, }), ).toBe(false); }); }); describe("useParkedPreview", () => { const NO_CONFIG = cells(); const NONE = new Set(); it("is inert for a rendered cell with output", () => { const { result } = renderHook(() => useParkedPreview({ activeCell: cell(A), slideConfigs: NO_CONFIG, noOutputIds: NONE, }), ); expect(result.current).toEqual({ parkedPreviewCell: null, isHeldEdit: false, isNoOutputPreview: false, heldEditCellId: null, heldShowsCode: true, toggleHeldShowsCode: expect.any(Function), }); }); it("is inert when there is no active cell", () => { const { result } = renderHook(() => useParkedPreview({ activeCell: undefined, slideConfigs: NO_CONFIG, noOutputIds: NONE, }), ); expect(result.current).toEqual({ parkedPreviewCell: null, isHeldEdit: false, isNoOutputPreview: false, heldEditCellId: null, heldShowsCode: true, toggleHeldShowsCode: expect.any(Function), }); }); it("parks a skipped cell without flagging it as a no-output preview", () => { const { result } = renderHook(() => useParkedPreview({ activeCell: cell(A), slideConfigs: cells([A, { type: "skip" }]), noOutputIds: NONE, }), ); expect(result.current).toEqual({ parkedPreviewCell: cell(A), isHeldEdit: false, isNoOutputPreview: false, heldEditCellId: null, heldShowsCode: true, toggleHeldShowsCode: expect.any(Function), }); }); it("parks an output-less cell as a no-output preview", () => { const { result } = renderHook(() => useParkedPreview({ activeCell: cell(A), slideConfigs: NO_CONFIG, noOutputIds: new Set([A]), }), ); expect(result.current).toEqual({ parkedPreviewCell: cell(A), isHeldEdit: false, isNoOutputPreview: true, heldEditCellId: null, heldShowsCode: true, toggleHeldShowsCode: expect.any(Function), }); }); it("holds the cell in the overlay once it gains output", () => { const { result, rerender } = renderHook( (props: Parameters[0]) => useParkedPreview(props), { initialProps: { activeCell: cell(A), slideConfigs: NO_CONFIG, noOutputIds: new Set([A]), }, }, ); expect(result.current.isNoOutputPreview).toBe(true); expect(result.current.isHeldEdit).toBe(false); // Same cell, now with output: keep it parked so the editor isn't remounted. rerender({ activeCell: cell(A), slideConfigs: NO_CONFIG, noOutputIds: NONE, }); expect(result.current).toEqual({ parkedPreviewCell: cell(A), isHeldEdit: true, isNoOutputPreview: false, heldEditCellId: A, heldShowsCode: true, toggleHeldShowsCode: expect.any(Function), }); }); it("releases the hold when a different cell becomes active", () => { const { result, rerender } = renderHook( (props: Parameters[0]) => useParkedPreview(props), { initialProps: { activeCell: cell(A), slideConfigs: NO_CONFIG, noOutputIds: new Set([A]), }, }, ); // Arm the hold, then let A gain output (held). rerender({ activeCell: cell(A), slideConfigs: NO_CONFIG, noOutputIds: NONE, }); expect(result.current.isHeldEdit).toBe(true); // Navigate to a rendered B: the hold on A is released. rerender({ activeCell: cell(B), slideConfigs: NO_CONFIG, noOutputIds: NONE, }); expect(result.current).toEqual({ parkedPreviewCell: null, isHeldEdit: false, isNoOutputPreview: false, heldEditCellId: null, heldShowsCode: true, toggleHeldShowsCode: expect.any(Function), }); }); it("releases a skipped cell into the deck as soon as it is un-skipped", () => { const { result, rerender } = renderHook( (props: Parameters[0]) => useParkedPreview(props), { initialProps: { activeCell: cell(A), slideConfigs: cells([A, { type: "skip" }]), noOutputIds: NONE, }, }, ); expect(result.current.parkedPreviewCell).toEqual(cell(A)); // Un-skip the still-active cell: it has output, so it must rejoin the deck // immediately rather than staying held in the overlay until navigation. rerender({ activeCell: cell(A), slideConfigs: NO_CONFIG, noOutputIds: NONE, }); expect(result.current).toEqual({ parkedPreviewCell: null, isHeldEdit: false, isNoOutputPreview: false, heldEditCellId: null, heldShowsCode: true, toggleHeldShowsCode: expect.any(Function), }); }); it("shows the held editor by default and toggles it off without navigating", () => { const { result, rerender } = renderHook( (props: Parameters[0]) => useParkedPreview(props), { initialProps: { activeCell: cell(A), slideConfigs: NO_CONFIG, noOutputIds: new Set([A]), }, }, ); // A gains output: held, with its editor shown by default. rerender({ activeCell: cell(A), slideConfigs: NO_CONFIG, noOutputIds: NONE, }); expect(result.current.isHeldEdit).toBe(true); expect(result.current.heldShowsCode).toBe(true); // The `C` toggle hides the editor in place, no navigation required. act(() => { result.current.toggleHeldShowsCode(); }); expect(result.current.heldShowsCode).toBe(false); // Toggling again brings it back. act(() => { result.current.toggleHeldShowsCode(); }); expect(result.current.heldShowsCode).toBe(true); }); it("resets held code visibility when a new cell takes the held slot", () => { const { result, rerender } = renderHook( (props: Parameters[0]) => useParkedPreview(props), { initialProps: { activeCell: cell(A), slideConfigs: NO_CONFIG, noOutputIds: new Set([A]), }, }, ); rerender({ activeCell: cell(A), slideConfigs: NO_CONFIG, noOutputIds: NONE, }); act(() => { result.current.toggleHeldShowsCode(); }); expect(result.current.heldShowsCode).toBe(false); // Move to a fresh output-less cell B, then let it gain output (held). rerender({ activeCell: cell(B), slideConfigs: NO_CONFIG, noOutputIds: new Set([B]), }); rerender({ activeCell: cell(B), slideConfigs: NO_CONFIG, noOutputIds: NONE, }); expect(result.current.heldEditCellId).toBe(B); // The new cell starts with its editor visible again. expect(result.current.heldShowsCode).toBe(true); }); });