tftsr-devops_investigation/tests/unit/Terminal.test.tsx
Shaun Arman 37db7d6c6c fix(ui): critical UI fixes - logs, menus, dark mode, YAML
Replace LogsModal with LogStreamPanel in PodList for streaming logs
Add smart positioning to ResourceActionMenu to flip when near bottom
Fix dark mode text visibility by applying class to html element
Fix YAML editor loading race condition

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-06-09 13:33:37 -05:00

412 lines
13 KiB
TypeScript

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);
}),
onSelectionChange: vi.fn(),
getSelection: vi.fn(() => "selected text"),
loadAddon: vi.fn(),
options: {} as Record<string, unknown>,
};
// 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<T extends (...args: unknown[]) => unknown = (...args: unknown[]) => unknown> =
T & ReturnType<typeof vi.fn>;
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(<Terminal {...defaultProps} />);
});
it("shows 'Select a pod to connect' when no pod/container is provided", () => {
render(<Terminal {...defaultProps} />);
expect(screen.getByText(/select a pod to connect/i)).toBeInTheDocument();
});
it("does not show a tab bar when there are no sessions", () => {
render(<Terminal {...defaultProps} />);
expect(screen.queryByRole("tab")).not.toBeInTheDocument();
});
});
describe("session management", () => {
it("shows tab bar when a session is auto-created from props", async () => {
render(<Terminal {...withPodProps} />);
await waitFor(() => {
expect(screen.getByRole("tab")).toBeInTheDocument();
});
});
it("tab label contains pod/container name", async () => {
render(<Terminal {...withPodProps} />);
await waitFor(() => screen.getByRole("tab"));
expect(screen.getByRole("tab").textContent).toContain("nginx-abc");
});
it("clicking '+' button adds a new session tab", async () => {
render(<Terminal {...withPodProps} />);
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(<Terminal {...withPodProps} />);
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(<Terminal {...withPodProps} />);
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(<Terminal {...withPodProps} />);
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(<Terminal {...withPodProps} />);
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(<Terminal {...withPodProps} />);
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(<Terminal {...withPodProps} />);
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(<Terminal {...withPodProps} />);
await waitFor(() => screen.getByRole("tab"));
const shellSelector = screen.getByRole("combobox");
expect(shellSelector).toBeInTheDocument();
});
it("passes selected shell to execPodCmd", async () => {
render(<Terminal {...withPodProps} />);
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(<Terminal {...withPodProps} />);
await waitFor(() => screen.getByRole("tab"));
unmount();
expect(mockTerminalInstance.dispose).toHaveBeenCalled();
});
});
describe("terminal configuration", () => {
beforeEach(() => {
localStorage.clear();
});
it("renders settings button in tab bar", async () => {
render(<Terminal {...withPodProps} />);
await waitFor(() => screen.getByRole("tab"));
const settingsBtn = screen.queryByRole("button", { name: /settings/i });
expect(settingsBtn).toBeInTheDocument();
});
it("opens settings dialog when settings button is clicked", async () => {
render(<Terminal {...withPodProps} />);
await waitFor(() => screen.getByRole("tab"));
const settingsBtn = screen.getByRole("button", { name: /settings/i });
await userEvent.click(settingsBtn);
await waitFor(() => {
expect(screen.getByText(/terminal settings/i)).toBeInTheDocument();
});
});
it("shows copy-on-select toggle in settings dialog", async () => {
render(<Terminal {...withPodProps} />);
await waitFor(() => screen.getByRole("tab"));
const settingsBtn = screen.getByRole("button", { name: /settings/i });
await userEvent.click(settingsBtn);
await waitFor(() => {
expect(screen.getByLabelText(/copy on select/i)).toBeInTheDocument();
});
});
it("shows font family input in settings dialog", async () => {
render(<Terminal {...withPodProps} />);
await waitFor(() => screen.getByRole("tab"));
const settingsBtn = screen.getByRole("button", { name: /settings/i });
await userEvent.click(settingsBtn);
await waitFor(() => {
expect(screen.getByLabelText(/font family/i)).toBeInTheDocument();
});
});
it("shows font size input in settings dialog", async () => {
render(<Terminal {...withPodProps} />);
await waitFor(() => screen.getByRole("tab"));
const settingsBtn = screen.getByRole("button", { name: /settings/i });
await userEvent.click(settingsBtn);
await waitFor(() => {
expect(screen.getByLabelText(/font size/i)).toBeInTheDocument();
});
});
it("persists settings to localStorage when changed", async () => {
render(<Terminal {...withPodProps} />);
await waitFor(() => screen.getByRole("tab"));
const settingsBtn = screen.getByRole("button", { name: /settings/i });
await userEvent.click(settingsBtn);
await waitFor(() => screen.getByLabelText(/copy on select/i));
const copyOnSelectCheckbox = screen.getByLabelText(/copy on select/i) as HTMLInputElement;
await userEvent.click(copyOnSelectCheckbox);
await waitFor(() => {
const stored = localStorage.getItem("terminal-settings");
expect(stored).toBeTruthy();
const parsed = JSON.parse(stored || "{}");
expect(parsed.copyOnSelect).toBeDefined();
});
});
it("loads settings from localStorage on mount", async () => {
localStorage.setItem(
"terminal-settings",
JSON.stringify({
copyOnSelect: true,
fontFamily: "Courier New",
fontSize: 16,
})
);
render(<Terminal {...withPodProps} />);
await waitFor(() => screen.getByRole("tab"));
const settingsBtn = screen.getByRole("button", { name: /settings/i });
await userEvent.click(settingsBtn);
await waitFor(() => {
const copyOnSelectCheckbox = screen.getByLabelText(/copy on select/i) as HTMLInputElement;
expect(copyOnSelectCheckbox.checked).toBe(true);
const fontFamilyInput = screen.getByLabelText(/font family/i) as HTMLInputElement;
expect(fontFamilyInput.value).toBe("Courier New");
const fontSizeInput = screen.getByLabelText(/font size/i) as HTMLInputElement;
expect(fontSizeInput.value).toBe("16");
});
});
});
});