import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, waitFor, act } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; // ── xterm mocks ─────────────────────────────────────────────────────────────── // onData callbacks registered by the component — keyed by call order const onDataHandlers: Array<(data: string) => void> = []; const mockTerminalInstance = { open: vi.fn(), write: vi.fn(), writeln: vi.fn(), dispose: vi.fn(), onData: vi.fn((cb: (data: string) => void) => { onDataHandlers.push(cb); }), loadAddon: vi.fn(), options: {} as Record, }; // Must use function (not arrow) so `new` works vi.mock("xterm", () => ({ Terminal: vi.fn(function () { return mockTerminalInstance; }), })); const mockFitAddon = { fit: vi.fn(), dispose: vi.fn() }; vi.mock("xterm-addon-fit", () => ({ FitAddon: vi.fn(function () { return mockFitAddon; }), })); const mockWebLinksAddon = { dispose: vi.fn() }; vi.mock("xterm-addon-web-links", () => ({ WebLinksAddon: vi.fn(function () { return mockWebLinksAddon; }), })); // ── Tauri command mock ──────────────────────────────────────────────────────── vi.mock("@/lib/tauriCommands", () => ({ execPodCmd: vi.fn(), })); import * as tauriCommands from "@/lib/tauriCommands"; import { Terminal } from "@/components/Kubernetes/Terminal"; type MockedFn unknown = (...args: unknown[]) => unknown> = T & ReturnType; const execPodCmdMock = tauriCommands.execPodCmd as MockedFn; const defaultProps = { clusterId: "cluster-1", namespace: "default", }; const withPodProps = { ...defaultProps, podName: "nginx-abc", containerName: "nginx", }; // ── helper: get the onData handler registered for a session ────────────────── function getOnDataCallback(): (data: string) => void { const cb = onDataHandlers[onDataHandlers.length - 1]; if (!cb) throw new Error("No onData handler registered — terminal may not have mounted"); return cb; } // ── tests ───────────────────────────────────────────────────────────────────── describe("Terminal component", () => { beforeEach(() => { vi.clearAllMocks(); onDataHandlers.length = 0; // Re-wire onData to push into our handler array after clearAllMocks mockTerminalInstance.onData.mockImplementation((cb: (data: string) => void) => { onDataHandlers.push(cb); }); execPodCmdMock.mockResolvedValue({ stdout: "hello\nworld", stderr: "", exit_code: 0 }); }); afterEach(() => { vi.restoreAllMocks(); }); describe("empty state", () => { it("renders without crashing", () => { render(); }); it("shows 'Select a pod to connect' when no pod/container is provided", () => { render(); expect(screen.getByText(/select a pod to connect/i)).toBeInTheDocument(); }); it("does not show a tab bar when there are no sessions", () => { render(); expect(screen.queryByRole("tab")).not.toBeInTheDocument(); }); }); describe("session management", () => { it("shows tab bar when a session is auto-created from props", async () => { render(); await waitFor(() => { expect(screen.getByRole("tab")).toBeInTheDocument(); }); }); it("tab label contains pod/container name", async () => { render(); await waitFor(() => screen.getByRole("tab")); expect(screen.getByRole("tab").textContent).toContain("nginx-abc"); }); it("clicking '+' button adds a new session tab", async () => { render(); await waitFor(() => screen.getByRole("tab")); expect(screen.getAllByRole("tab")).toHaveLength(1); const addButton = screen.getByRole("button", { name: /add session/i }); await userEvent.click(addButton); await waitFor(() => { expect(screen.getAllByRole("tab")).toHaveLength(2); }); }); it("clicking the X on a tab removes that session", async () => { render(); await waitFor(() => screen.getByRole("tab")); const closeBtn = screen.getByRole("button", { name: /close/i }); await userEvent.click(closeBtn); await waitFor(() => { expect(screen.queryByRole("tab")).not.toBeInTheDocument(); }); }); it("removing the last session goes back to the empty state", async () => { render(); await waitFor(() => screen.getByRole("tab")); const closeBtn = screen.getByRole("button", { name: /close/i }); await userEvent.click(closeBtn); await waitFor(() => { expect(screen.getByText(/select a pod to connect/i)).toBeInTheDocument(); }); }); }); describe("IPC integration", () => { it("calls execPodCmd with correct arguments when a command is entered", async () => { render(); await waitFor(() => screen.getByRole("tab")); // onData must have been registered by now expect(mockTerminalInstance.onData).toHaveBeenCalled(); const onDataCallback = getOnDataCallback(); await act(async () => { onDataCallback("l"); onDataCallback("s"); onDataCallback("\r"); }); await waitFor(() => { expect(execPodCmdMock).toHaveBeenCalledWith( "cluster-1", "default", "nginx-abc", "nginx", "ls", expect.any(String) ); }); }); it("writes command output to the terminal after execution", async () => { execPodCmdMock.mockResolvedValue({ stdout: "file1.txt\nfile2.txt", stderr: "", exit_code: 0 }); render(); await waitFor(() => screen.getByRole("tab")); const onDataCallback = getOnDataCallback(); await act(async () => { onDataCallback("l"); onDataCallback("s"); onDataCallback("\r"); }); await waitFor(() => { const writeCalls = mockTerminalInstance.write.mock.calls.map((c: unknown[]) => c[0] as string); expect(writeCalls.some((s) => s.includes("file1.txt"))).toBe(true); }); }); it("handles IPC errors gracefully by writing an error message to the terminal", async () => { execPodCmdMock.mockRejectedValue(new Error("connection refused")); render(); await waitFor(() => screen.getByRole("tab")); const onDataCallback = getOnDataCallback(); await act(async () => { onDataCallback("e"); onDataCallback("c"); onDataCallback("h"); onDataCallback("o"); onDataCallback("\r"); }); await waitFor(() => { const writeCalls = mockTerminalInstance.write.mock.calls.map((c: unknown[]) => c[0] as string); expect( writeCalls.some((s) => s.toLowerCase().includes("error") || s.includes("connection refused")) ).toBe(true); }); }); it("writes stderr output to the terminal when exit_code is non-zero", async () => { execPodCmdMock.mockResolvedValue({ stdout: "", stderr: "command not found", exit_code: 127 }); render(); await waitFor(() => screen.getByRole("tab")); const onDataCallback = getOnDataCallback(); await act(async () => { onDataCallback("b"); onDataCallback("a"); onDataCallback("d"); onDataCallback("\r"); }); await waitFor(() => { const writeCalls = mockTerminalInstance.write.mock.calls.map((c: unknown[]) => c[0] as string); expect(writeCalls.some((s) => s.includes("command not found"))).toBe(true); }); }); }); describe("shell selector", () => { it("renders a shell selector dropdown", async () => { render(); await waitFor(() => screen.getByRole("tab")); const shellSelector = screen.getByRole("combobox"); expect(shellSelector).toBeInTheDocument(); }); it("passes selected shell to execPodCmd", async () => { render(); await waitFor(() => screen.getByRole("tab")); const shellSelector = screen.getByRole("combobox"); await userEvent.selectOptions(shellSelector, "sh"); const onDataCallback = getOnDataCallback(); await act(async () => { onDataCallback("p"); onDataCallback("w"); onDataCallback("d"); onDataCallback("\r"); }); await waitFor(() => { expect(execPodCmdMock).toHaveBeenCalledWith( expect.any(String), expect.any(String), expect.any(String), expect.any(String), "pwd", "sh" ); }); }); }); describe("cleanup", () => { it("calls terminal.dispose() on unmount", async () => { const { unmount } = render(); await waitFor(() => screen.getByRole("tab")); unmount(); expect(mockTerminalInstance.dispose).toHaveBeenCalled(); }); }); });