/* 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, 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, 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 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(""); }); }); 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, 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(); }); });