tftsr-devops_investigation/tests/unit/Terminal.test.tsx
Shaun Arman 3f4869af01
Some checks failed
PR Review Automation / review (pull_request) Has been cancelled
Test / frontend-typecheck (pull_request) Has been cancelled
Test / rust-clippy (pull_request) Has been cancelled
Test / frontend-tests (pull_request) Has been cancelled
Test / rust-fmt-check (pull_request) Has been cancelled
Test / rust-tests (pull_request) Has been cancelled
feat(kubernetes): implement Lens Desktop v5 feature-parity UI
Complete overhaul of the Kubernetes management page from a basic config
panel into a full Lens-style IDE shell with 26 resource types, real-time
data, and a comprehensive test suite.

Layout & navigation:
- Rewrite KubernetesPage as a Lens v5-style shell: collapsible sidebar
  (Workloads / Services & Networking / Config & Storage / Access Control /
  Cluster), top hotbar with cluster+namespace selectors, Ctrl+K command
  palette
- All 26 resource types now accessible via sidebar navigation (previously 5)

New resource types (Rust + TypeScript + React):
- StorageClasses, NetworkPolicies, ResourceQuotas, LimitRanges
- 4 new Tauri commands registered in generate_handler![]

Component implementations (replacing stubs with real IPC):
- Terminal: full xterm.js with multi-tab sessions and exec_pod IPC
- YamlEditor: Monaco editor with YAML syntax highlighting
- MetricsChart: recharts LineChart/BarChart
- ClusterOverview: live node/pod/deployment/namespace counts
- ClusterDetails: real kubeconfig + node data
- PodDetail, DeploymentDetail, ServiceDetail, ConfigMapDetail, SecretDetail:
  all connected to real IPC data, zero hardcoded values
- CreateResourceModal, EditResourceModal: wired to createResourceCmd /
  editResourceCmd
- RbacViewer: live data from 4 RBAC IPC commands
- RbacEditor: create roles/cluster-roles via YAML editor
- CommandPalette: 12 real navigation commands, keyboard nav

Dependencies added: xterm@5, xterm-addon-fit, xterm-addon-web-links,
@monaco-editor/react@4, recharts@2

Tooling:
- Replace eslint-plugin-react (incompatible with ESLint 10) with
  @eslint-react/eslint-plugin; fix eslint.config.js for flat config
- Fix pre-existing hoisting lint errors in Security.tsx, PortForwardForm.tsx
- Fix eventBus.ts: replace all `any` generics with `unknown`

Tests: 251 passing across 35 test files (was 94/19)
- 16 new test files covering all new and fixed components (TDD)
- npx tsc --noEmit: 0 errors
- cargo clippy -- -D warnings: 0 warnings
- cargo fmt --check: passes
- eslint src/ --max-warnings 0: 0 issues
2026-06-07 16:41:28 -05:00

300 lines
9.5 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);
}),
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();
});
});
});