import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; import { ScriptRunDialog } from "../ScriptRunDialog"; // Mock the API vi.mock("../../api", () => ({ killPtyTerminalSession: vi.fn(() => Promise.resolve({ killed: true })), })); // Track useTerminal callback registrations const mockCallbacks = { data: [] as ((data: string) => void)[], scrollback: [] as ((data: string) => void)[], exit: [] as ((code: number) => void)[], connect: [] as ((info: { shell: string; cwd: string }) => void)[], }; const defaultUseTerminalReturn = { connectionStatus: "connected" as const, sendInput: vi.fn(), resize: vi.fn(), onData: vi.fn((cb: (data: string) => void) => { mockCallbacks.data.push(cb); return () => { mockCallbacks.data = mockCallbacks.data.filter((c) => c !== cb); }; }), onExit: vi.fn((cb: (code: number) => void) => { mockCallbacks.exit.push(cb); return () => { mockCallbacks.exit = mockCallbacks.exit.filter((c) => c !== cb); }; }), onConnect: vi.fn((cb: (info: { shell: string; cwd: string }) => void) => { mockCallbacks.connect.push(cb); return () => { mockCallbacks.connect = mockCallbacks.connect.filter((c) => c !== cb); }; }), onScrollback: vi.fn((cb: (data: string) => void) => { mockCallbacks.scrollback.push(cb); return () => { mockCallbacks.scrollback = mockCallbacks.scrollback.filter((c) => c !== cb); }; }), reconnect: vi.fn(), }; const mockUseTerminal = vi.fn(() => ({ ...defaultUseTerminalReturn })); vi.mock("../../hooks/useTerminal", () => ({ useTerminal: (...args: unknown[]) => mockUseTerminal(...args), })); const defaultProps = { isOpen: true as boolean, onClose: vi.fn(), scriptName: "build", sessionId: "sess-123", command: "npm run build", }; beforeEach(() => { vi.clearAllMocks(); mockCallbacks.data = []; mockCallbacks.scrollback = []; mockCallbacks.exit = []; mockCallbacks.connect = []; mockUseTerminal.mockClear(); mockUseTerminal.mockReturnValue({ ...defaultUseTerminalReturn }); }); describe("ScriptRunDialog", () => { it("renders with script name and command when open", () => { render(); expect(screen.getByTestId("script-run-dialog-overlay")).toBeTruthy(); expect(screen.getByText("build")).toBeTruthy(); expect(screen.getByTestId("script-run-command")).toBeTruthy(); expect(screen.getByText("npm run build")).toBeTruthy(); }); it("returns null when not open", () => { render(); expect(screen.queryByTestId("script-run-dialog-overlay")).toBeNull(); }); it("shows loading state while waiting for output", () => { render(); expect(screen.getByText("Waiting for output...")).toBeTruthy(); }); it("renders live output as it arrives", async () => { render(); // Simulate data arriving await act(async () => { mockCallbacks.data.forEach((cb) => cb("Building project...\n")); }); await waitFor(() => { expect(screen.getByText(/Building project\.\.\./)).toBeTruthy(); }); // Simulate more data await act(async () => { mockCallbacks.data.forEach((cb) => cb("Done!\n")); }); await waitFor(() => { expect(screen.getByText(/Done!/)).toBeTruthy(); }); }); it("shows completed status on exit code 0", async () => { render(); await act(async () => { mockCallbacks.exit.forEach((cb) => cb(0)); }); await waitFor(() => { expect(screen.getByText("Completed")).toBeTruthy(); expect(screen.getByTestId("script-run-exit-code")).toBeTruthy(); expect(screen.getByText("Exit code: 0")).toBeTruthy(); }); }); it("shows error status on non-zero exit code", async () => { render(); await act(async () => { mockCallbacks.exit.forEach((cb) => cb(1)); }); await waitFor(() => { expect(screen.getByText(/Failed \(exit code 1\)/)).toBeTruthy(); expect(screen.getByText("Exit code: 1")).toBeTruthy(); const exitCodeEl = screen.getByTestId("script-run-exit-code"); expect(exitCodeEl.classList.contains("error")).toBe(true); }); }); it("calls killPtyTerminalSession when closed while running", async () => { const onClose = vi.fn(); const { killPtyTerminalSession } = await import("../../api"); render(); // Click close button while still running fireEvent.click(screen.getByTestId("script-run-dialog-close")); await waitFor(() => { expect(killPtyTerminalSession).toHaveBeenCalledWith("sess-123"); expect(onClose).toHaveBeenCalled(); }); }); it("does not kill session when closed after completion", async () => { const onClose = vi.fn(); const { killPtyTerminalSession } = await import("../../api"); render(); // Process exits successfully await act(async () => { mockCallbacks.exit.forEach((cb) => cb(0)); }); // Click close after completion fireEvent.click(screen.getByTestId("script-run-dialog-close")); await waitFor(() => { expect(killPtyTerminalSession).not.toHaveBeenCalled(); expect(onClose).toHaveBeenCalled(); }); }); it("closes on Escape key", async () => { const onClose = vi.fn(); render(); await act(async () => { fireEvent.keyDown(document, { key: "Escape" }); }); expect(onClose).toHaveBeenCalled(); }); it("closes on overlay click", async () => { const onClose = vi.fn(); render(); fireEvent.click(screen.getByTestId("script-run-dialog-overlay")); expect(onClose).toHaveBeenCalled(); }); it("shows connecting status when not yet connected", async () => { mockUseTerminal.mockReturnValue({ ...defaultUseTerminalReturn, connectionStatus: "connecting", }); render(); expect(screen.getByText("Connecting...")).toBeTruthy(); }); });