/* Copyright 2026 Marimo. All rights reserved. */ import { act, renderHook, waitFor } from "@testing-library/react"; import { createStore, Provider } from "jotai"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { MockNotebook } from "@/__mocks__/notebook"; import { variableName } from "@/__tests__/branded"; import { notebookAtom } from "@/core/cells/cells"; import { CellId } from "@/core/cells/ids"; import { createCellRuntimeState } from "@/core/cells/types"; import { variablesAtom } from "@/core/variables/state"; import type { Milliseconds } from "@/utils/time"; import { usePendingDelete, usePendingDeleteService, } from "../pending-delete-service"; const mockDeleteCell = vi.fn(); const mockDeleteManyCells = vi.fn(); vi.mock("@/components/editor/cell/useDeleteCell", () => ({ useDeleteCellCallback: () => mockDeleteCell, useDeleteManyCellsCallback: () => mockDeleteManyCells, })); function createTestWrapper() { const store = createStore(); const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); return { wrapper, store }; } // Don't clear all mocks - just reset the specific one we care about beforeEach(() => { vi.resetAllMocks(); }); describe("pending-delete-service", () => { it("auto-deletes simple cells (no dependencies, short execution time)", async () => { const { wrapper, store } = createTestWrapper(); const cellId = CellId.create(); const notebook = MockNotebook.notebookState({ cellData: { [cellId]: { code: "print('hello')" }, }, }); notebook.cellRuntime[cellId] = createCellRuntimeState({ runElapsedTimeMs: 1 as Milliseconds, }); store.set(notebookAtom, notebook); store.set(variablesAtom, {}); const { result } = renderHook( () => { const service = usePendingDeleteService(); const deleteState = usePendingDelete(cellId); return { service, deleteState }; }, { wrapper }, ); act(() => { result.current.service.submit([cellId]); }); // should flush single cell await waitFor(() => { expect(mockDeleteCell).toHaveBeenCalledWith({ cellId: cellId }); }); }); it("requires confirmation for expensive cells (long execution time)", () => { const { wrapper, store } = createTestWrapper(); const cellId = CellId.create(); const notebook = MockNotebook.notebookState({ cellData: { [cellId]: { code: "expensive_computation()" }, }, }); notebook.cellRuntime[cellId] = createCellRuntimeState({ runElapsedTimeMs: 100_000 as Milliseconds, }); store.set(notebookAtom, notebook); store.set(variablesAtom, {}); const { result: serviceResult } = renderHook( () => usePendingDeleteService(), { wrapper }, ); act(() => { serviceResult.current.submit([cellId]); }); const { result } = renderHook(() => usePendingDelete(cellId), { wrapper, }); expect(mockDeleteManyCells).not.toHaveBeenCalled(); expect(result.current.isPending).toBe(true); act(() => { if (result.current.isPending && "confirm" in result.current) { result.current.confirm(); } }); expect(mockDeleteManyCells).toHaveBeenCalledWith({ cellIds: [cellId], }); }); it("requires confirmation for cells with dependencies", () => { const { wrapper, store } = createTestWrapper(); const cell1Id = CellId.create(); const cell2Id = CellId.create(); const notebook = MockNotebook.notebookState({ cellData: { [cell1Id]: { code: "x = 1" }, [cell2Id]: { code: "y = x + 1" }, }, }); // Both have short execution times notebook.cellRuntime[cell1Id] = createCellRuntimeState({ runElapsedTimeMs: 100 as Milliseconds, }); notebook.cellRuntime[cell2Id] = createCellRuntimeState({ runElapsedTimeMs: 50 as Milliseconds, }); store.set(notebookAtom, notebook); store.set(variablesAtom, { [variableName("x")]: { name: variableName("x"), declaredBy: [cell1Id], usedBy: [cell2Id], }, }); // cell1 with dependencies const { result: serviceResult } = renderHook( () => usePendingDeleteService(), { wrapper }, ); act(() => { serviceResult.current.submit([cell1Id]); }); // ensure doesn't auto-delete renderHook(() => usePendingDelete(cell1Id), { wrapper }); expect(mockDeleteManyCells).not.toHaveBeenCalled(); }); it("prevents individual confirmation when multiple cells are pending", () => { const { wrapper, store } = createTestWrapper(); const cell1Id = CellId.create(); const cell2Id = CellId.create(); const notebook = MockNotebook.notebookState({ cellData: { [cell1Id]: { code: "expensive1()" }, [cell2Id]: { code: "expensive2()" }, }, }); notebook.cellRuntime[cell1Id] = createCellRuntimeState({ runElapsedTimeMs: 3000 as Milliseconds, }); notebook.cellRuntime[cell2Id] = createCellRuntimeState({ runElapsedTimeMs: 4000 as Milliseconds, }); store.set(notebookAtom, notebook); store.set(variablesAtom, {}); const { result: serviceResult } = renderHook( () => usePendingDeleteService(), { wrapper }, ); act(() => { serviceResult.current.submit([cell1Id, cell2Id]); }); const { result } = renderHook(() => usePendingDelete(cell1Id), { wrapper, }); expect(result.current.isPending).toBe(true); if (result.current.isPending) { expect(result.current.shouldConfirmDelete).toBe(false); expect("confirm" in result.current).toBe(false); } }); });