/* Copyright 2026 Marimo. All rights reserved. */
import { render } from "@testing-library/react";
import { createStore, Provider } from "jotai";
import { beforeAll, describe, expect, it } from "vitest";
import { SetupMocks } from "@/__mocks__/common";
import { MockNotebook } from "@/__mocks__/notebook";
import { MockRequestClient } from "@/__mocks__/requests";
import { cellId } from "@/__tests__/branded";
import { TooltipProvider } from "@/components/ui/tooltip";
import { notebookAtom } from "@/core/cells/cells";
import type { CellRuntimeState } from "@/core/cells/types";
import { createCellRuntimeState } from "@/core/cells/types";
import { defaultUserConfig } from "@/core/config/config-schema";
import type { AppMode } from "@/core/mode";
import { requestClientAtom } from "@/core/network/requests";
import { Cell } from "../notebook-cell";
const cid = cellId("test");
function createTestWrapper() {
const store = createStore();
store.set(requestClientAtom, MockRequestClient.create());
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
);
return { wrapper, store };
}
function seedNotebook(
store: ReturnType,
runtimeOverrides: Partial = {},
) {
const notebook = MockNotebook.notebookState({
cellData: {
[cid]: {
code: "1 + 1",
name: "test_cell",
edited: false,
serializedEditorState: null,
config: {
disabled: false,
hide_code: false,
column: null,
},
},
},
});
notebook.cellRuntime[cid] = createCellRuntimeState({
status: "idle",
output: {
channel: "output",
mimetype: "text/html",
data: "persist me",
timestamp: 0,
},
...runtimeOverrides,
});
store.set(notebookAtom, notebook);
}
function renderCell(mode: AppMode) {
return (
|
);
}
beforeAll(() => {
SetupMocks.resizeObserver();
global.HTMLDivElement.prototype.scrollIntoView = () => {
// do nothing
};
});
describe("output persistence across mode changes", () => {
it("preserves the output DOM node when toggling edit <-> present", () => {
const { store, wrapper } = createTestWrapper();
seedNotebook(store);
const { container, rerender } = render(renderCell("edit"), { wrapper });
const outputNode = container.querySelector(
'[data-cell-role="output"]',
);
expect(outputNode).toBeTruthy();
// A property survives only if the exact DOM node instance survives.
(outputNode as HTMLElement & { __sentinel?: string }).__sentinel = "alive";
rerender(renderCell("present"));
const presentNode = container.querySelector(
'[data-cell-role="output"]',
);
expect(presentNode).toBe(outputNode);
expect(presentNode?.isConnected).toBe(true);
expect(
(presentNode as HTMLElement & { __sentinel?: string }).__sentinel,
).toBe("alive");
// Edit chrome is hidden but stays mounted while presenting
const tray = container.querySelector(
"[data-testid='cell-tray']",
);
expect(tray).toBeTruthy();
expect(tray?.hidden).toBe(true);
// Present styling is applied
expect(
container
.querySelector(`[data-cell-id="${cid}"]`)
?.classList.contains("published"),
).toBe(true);
rerender(renderCell("edit"));
expect(container.querySelector('[data-cell-role="output"]')).toBe(
outputNode,
);
expect(
container.querySelector("[data-testid='cell-tray']")?.hidden,
).toBe(false);
expect(
container
.querySelector(`[data-cell-id="${cid}"]`)
?.classList.contains("published"),
).toBe(false);
});
it("hides all edit chrome while presenting", () => {
const { store, wrapper } = createTestWrapper();
seedNotebook(store);
const { container, rerender } = render(renderCell("edit"), { wrapper });
// Returns true when the element is not visible: either gone from the DOM or
// inside a `hidden` ancestor. Edit chrome may unmount while presenting (only
// the cell *output* is contractually kept mounted — see the DOM-node test
// above); either way it must not be visible.
const isHidden = (testId: string) => {
const el = container.querySelector(`[data-testid='${testId}']`);
return el === null || el.closest("[hidden]") !== null;
};
// Everything visible in edit mode
const editChrome = [
"cell-tray",
"drag-button",
"cell-actions-button",
"delete-button",
"create-cell-button",
"console-output-area",
];
for (const testId of editChrome) {
expect({ testId, hidden: isHidden(testId) }).toEqual({
testId,
hidden: false,
});
}
rerender(renderCell("present"));
for (const testId of editChrome) {
expect({ testId, hidden: isHidden(testId) }).toEqual({
testId,
hidden: true,
});
}
rerender(renderCell("edit"));
for (const testId of editChrome) {
expect({ testId, hidden: isHidden(testId) }).toEqual({
testId,
hidden: false,
});
}
});
it("does not dim edited-but-not-run outputs as stale while presenting", () => {
const { store, wrapper } = createTestWrapper();
seedNotebook(store);
// Mark the cell as edited without re-running it
const notebook = store.get(notebookAtom);
notebook.cellData[cid] = { ...notebook.cellData[cid], edited: true };
store.set(notebookAtom, { ...notebook });
const { container, rerender } = render(renderCell("edit"), { wrapper });
expect(container.querySelector(".marimo-output-stale")).toBeTruthy();
// The read view never dims pending edits; present mode matches it
rerender(renderCell("present"));
expect(container.querySelector(".marimo-output-stale")).toBeFalsy();
rerender(renderCell("edit"));
expect(container.querySelector(".marimo-output-stale")).toBeTruthy();
});
it("hides errored cells while presenting without unmounting them", () => {
const { store, wrapper } = createTestWrapper();
seedNotebook(store, {
errored: true,
output: {
channel: "marimo-error",
mimetype: "application/vnd.marimo+error",
data: [{ type: "exception", exception_type: "ValueError", msg: "!" }],
timestamp: 0,
},
});
const { container, rerender } = render(renderCell("edit"), { wrapper });
const outputNode = container.querySelector(
'[data-cell-role="output"]',
);
expect(outputNode).toBeTruthy();
const getSortableWrapper = () =>
container.querySelector(`[data-cell-id="${cid}"]`)
?.parentElement;
expect(getSortableWrapper()?.hidden).toBe(false);
rerender(renderCell("present"));
// Still mounted, but hidden
expect(container.querySelector('[data-cell-role="output"]')).toBe(
outputNode,
);
expect(getSortableWrapper()?.hidden).toBe(true);
rerender(renderCell("edit"));
expect(container.querySelector('[data-cell-role="output"]')).toBe(
outputNode,
);
expect(getSortableWrapper()?.hidden).toBe(false);
});
});