/* Copyright 2026 Marimo. All rights reserved. */ import { act, fireEvent, render, screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { SetupMocks } from "@/__mocks__/common"; import { cellId } from "@/__tests__/branded"; import { TooltipProvider } from "@/components/ui/tooltip"; import type { WithResponse } from "@/core/cells/types"; import type { OutputMessage } from "@/core/kernel/messages"; import { CONSOLE_CLEAR_DEBOUNCE_MS, ConsoleOutput } from "../ConsoleOutput"; SetupMocks.resizeObserver(); const renderWithProvider = (ui: React.ReactElement) => { return render({ui}); }; describe("ConsoleOutput integration", () => { const createOutput = (data: string, channel = "stdout"): OutputMessage => ({ channel: channel as "stdout" | "stderr", mimetype: "text/plain", data, timestamp: 0, }); const defaultProps = { cellId: cellId("cell-1"), cellName: "test_cell", consoleOutputs: [] as WithResponse[], stale: false, interrupted: false, debuggerActive: false, onSubmitDebugger: () => { // noop }, }; it("should render console output with clickable URLs", () => { const props = { ...defaultProps, consoleOutputs: [ { ...createOutput("Check out https://marimo.io for more info"), response: undefined, }, ], }; renderWithProvider(); const link = screen.getByRole("link", { name: "https://marimo.io" }); expect(link).toBeInTheDocument(); expect(link).toHaveAttribute("href", "https://marimo.io"); }); }); describe("ConsoleOutput pdb history", () => { const defaultProps = { cellId: cellId("cell-1"), cellName: "test_cell", consoleOutputs: [] as WithResponse[], stale: false, interrupted: false, debuggerActive: false, onSubmitDebugger: vi.fn(), }; const stdinPrompt = ( data: string, response?: string, ): WithResponse => ({ channel: "stdin" as const, mimetype: "text/plain", data, timestamp: 0, response, }); it("should persist command history across StdInput remounts", () => { // Initial state: pdb prompt waiting for input const outputs1: WithResponse[] = [stdinPrompt("(Pdb) ")]; const onSubmitDebugger = vi.fn(); const { rerender } = renderWithProvider( , ); const input = screen.getByTestId("console-input"); // Type "next" and submit fireEvent.change(input, { target: { value: "next" } }); fireEvent.keyDown(input, { key: "Enter" }); expect(onSubmitDebugger).toHaveBeenCalledWith("next", 0); // Simulate server response: old stdin gets a response, new stdin prompt appears const outputs2: WithResponse[] = [ stdinPrompt("(Pdb) ", "next"), stdinPrompt("(Pdb) "), ]; rerender( , ); // New StdInput mounted — press ArrowUp to recall previous command const newInput = screen.getByTestId("console-input"); fireEvent.keyDown(newInput, { key: "ArrowUp" }); expect(newInput).toHaveValue("next"); }); it("should submit an empty string when Enter is pressed with no input", () => { // Many CLIs prompt "Press Enter to continue" and expect "" back. const onSubmitDebugger = vi.fn(); const outputs: WithResponse[] = [ stdinPrompt("Press Enter to continue: "), ]; renderWithProvider( , ); const input = screen.getByTestId("console-input"); fireEvent.keyDown(input, { key: "Enter" }); expect(onSubmitDebugger).toHaveBeenCalledWith("", 0); }); it("should not record empty submissions in input history", () => { const onSubmitDebugger = vi.fn(); const outputs1: WithResponse[] = [stdinPrompt("(Pdb) ")]; const { rerender } = renderWithProvider( , ); let input = screen.getByTestId("console-input"); fireEvent.change(input, { target: { value: "step" } }); fireEvent.keyDown(input, { key: "Enter" }); const outputs2: WithResponse[] = [ stdinPrompt("(Pdb) ", "step"), stdinPrompt("(Pdb) "), ]; rerender( , ); // Submit an empty value; this should NOT enter the history stack. input = screen.getByTestId("console-input"); fireEvent.keyDown(input, { key: "Enter" }); const outputs3: WithResponse[] = [ stdinPrompt("(Pdb) ", "step"), stdinPrompt("(Pdb) ", ""), stdinPrompt("(Pdb) "), ]; rerender( , ); // ArrowUp should jump back to "step", skipping the empty submission. input = screen.getByTestId("console-input"); fireEvent.keyDown(input, { key: "ArrowUp" }); expect(input).toHaveValue("step"); }); it("should navigate through multiple history entries across remounts", () => { const onSubmitDebugger = vi.fn(); // First prompt const outputs1: WithResponse[] = [stdinPrompt("(Pdb) ")]; const { rerender } = renderWithProvider( , ); // Submit "step" let input = screen.getByTestId("console-input"); fireEvent.change(input, { target: { value: "step" } }); fireEvent.keyDown(input, { key: "Enter" }); // Second prompt const outputs2: WithResponse[] = [ stdinPrompt("(Pdb) ", "step"), stdinPrompt("(Pdb) "), ]; rerender( , ); // Submit "print(x)" input = screen.getByTestId("console-input"); fireEvent.change(input, { target: { value: "print(x)" } }); fireEvent.keyDown(input, { key: "Enter" }); // Third prompt const outputs3: WithResponse[] = [ stdinPrompt("(Pdb) ", "step"), stdinPrompt("(Pdb) ", "print(x)"), stdinPrompt("(Pdb) "), ]; rerender( , ); // ArrowUp should show most recent command first input = screen.getByTestId("console-input"); fireEvent.keyDown(input, { key: "ArrowUp" }); expect(input).toHaveValue("print(x)"); // ArrowUp again should show older command fireEvent.keyDown(input, { key: "ArrowUp" }); expect(input).toHaveValue("step"); // ArrowDown should go back to "print(x)" fireEvent.keyDown(input, { key: "ArrowDown" }); expect(input).toHaveValue("print(x)"); // ArrowDown again should return to empty input fireEvent.keyDown(input, { key: "ArrowDown" }); expect(input).toHaveValue(""); }); it("should distinguish an interrupted prompt from a bare-Enter submission", () => { // After interrupt, cell.ts coerces pending stdin prompts to response: "". // We must render that case differently from a real bare-Enter response, // so the user isn't told they "submitted" a blank value. const interruptedOutputs: WithResponse[] = [ stdinPrompt("Press Enter to continue: ", ""), ]; const { rerender } = renderWithProvider( , ); // No response chunk should be rendered for an interrupted pending prompt. expect(screen.queryByLabelText("stdin response")).not.toBeInTheDocument(); // Same outputs, but the cell isn't interrupted -- this is a real // bare-Enter submission, so we should render the (empty) placeholder. rerender( , ); expect(screen.getByLabelText("stdin response")).toBeInTheDocument(); expect(screen.getByText("(empty)")).toBeInTheDocument(); }); }); describe("ConsoleOutput debounced clearing", () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); const createOutput = ( data: string, channel = "stdout", ): WithResponse => ({ channel: channel as "stdout" | "stderr", mimetype: "text/plain", data, timestamp: 0, response: undefined, }); const defaultProps = { cellId: cellId("cell-1"), cellName: "test_cell", consoleOutputs: [] as WithResponse[], stale: false, interrupted: false, debuggerActive: false, onSubmitDebugger: vi.fn(), }; it("should keep old outputs visible when cleared, then show new outputs immediately", () => { const outputs1 = [createOutput("hello world")]; const { rerender } = renderWithProvider( , ); // Old output is visible expect(screen.getByText("hello world")).toBeInTheDocument(); // Clear outputs (simulates cell re-run) rerender( , ); // Old output should still be visible during debounce period expect(screen.getByText("hello world")).toBeInTheDocument(); // New outputs arrive before debounce fires const outputs2 = [createOutput("new output")]; rerender( , ); // New output should be shown immediately expect(screen.getByText("new output")).toBeInTheDocument(); expect(screen.queryByText("hello world")).not.toBeInTheDocument(); }); it("should clear outputs after debounce period if no new outputs arrive", () => { const outputs1 = [createOutput("old output")]; const { rerender } = renderWithProvider( , ); expect(screen.getByText("old output")).toBeInTheDocument(); // Clear outputs rerender( , ); // Still visible during debounce expect(screen.getByText("old output")).toBeInTheDocument(); // Advance past debounce period act(() => { vi.advanceTimersByTime(CONSOLE_CLEAR_DEBOUNCE_MS + 1); }); // Now the output should be cleared expect(screen.queryByText("old output")).not.toBeInTheDocument(); }); });