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