feat(web): integrate xterm.js with WebSocket terminal backend (#518)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #518.
This commit is contained in:
195
apps/web/src/components/terminal/TerminalPanel.test.tsx
Normal file
195
apps/web/src/components/terminal/TerminalPanel.test.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* @file TerminalPanel.test.tsx
|
||||
* @description Unit tests for the TerminalPanel component
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
// ==========================================
|
||||
// Mocks
|
||||
// ==========================================
|
||||
|
||||
// Mock XTerminal to avoid xterm.js DOM dependencies in panel tests
|
||||
vi.mock("./XTerminal", () => ({
|
||||
XTerminal: vi.fn(({ token, isVisible }: { token: string; isVisible: boolean }) => (
|
||||
<div
|
||||
data-testid="mock-xterminal"
|
||||
data-token={token}
|
||||
data-visible={isVisible ? "true" : "false"}
|
||||
/>
|
||||
)),
|
||||
}));
|
||||
|
||||
import { TerminalPanel } from "./TerminalPanel";
|
||||
|
||||
// ==========================================
|
||||
// Tests
|
||||
// ==========================================
|
||||
|
||||
describe("TerminalPanel", () => {
|
||||
const onClose = vi.fn();
|
||||
const onTabChange = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Rendering
|
||||
// ==========================================
|
||||
|
||||
describe("rendering", () => {
|
||||
it("renders the terminal panel", () => {
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
expect(screen.getByRole("region", { name: "Terminal panel" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders with height 280 when open", () => {
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
const panel = screen.getByRole("region", { name: "Terminal panel" });
|
||||
expect(panel).toHaveStyle({ height: "280px" });
|
||||
});
|
||||
|
||||
it("renders with height 0 when closed", () => {
|
||||
const { container } = render(
|
||||
(<TerminalPanel open={false} onClose={onClose} token="test-token" />) as ReactElement
|
||||
);
|
||||
const panel = container.querySelector('[role="region"][aria-label="Terminal panel"]');
|
||||
expect(panel).toHaveStyle({ height: "0px" });
|
||||
});
|
||||
|
||||
it("passes isVisible=true to XTerminal when open", () => {
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
const xterm = screen.getByTestId("mock-xterminal");
|
||||
expect(xterm).toHaveAttribute("data-visible", "true");
|
||||
});
|
||||
|
||||
it("passes isVisible=false to XTerminal when closed", () => {
|
||||
const { container } = render(
|
||||
(<TerminalPanel open={false} onClose={onClose} token="test-token" />) as ReactElement
|
||||
);
|
||||
// Use container query since the element is inside an aria-hidden region
|
||||
const xterm = container.querySelector('[data-testid="mock-xterminal"]');
|
||||
expect(xterm).toHaveAttribute("data-visible", "false");
|
||||
});
|
||||
|
||||
it("passes token to XTerminal", () => {
|
||||
render(
|
||||
(<TerminalPanel open={true} onClose={onClose} token="my-auth-token" />) as ReactElement
|
||||
);
|
||||
const xterm = screen.getByTestId("mock-xterminal");
|
||||
expect(xterm).toHaveAttribute("data-token", "my-auth-token");
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Tab bar
|
||||
// ==========================================
|
||||
|
||||
describe("tab bar", () => {
|
||||
it("renders default tabs when none provided", () => {
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
expect(screen.getByRole("tab", { name: "main" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders custom tabs", () => {
|
||||
const tabs = [
|
||||
{ id: "tab1", label: "Terminal 1" },
|
||||
{ id: "tab2", label: "Terminal 2" },
|
||||
];
|
||||
render(
|
||||
(
|
||||
<TerminalPanel open={true} onClose={onClose} tabs={tabs} token="test-token" />
|
||||
) as ReactElement
|
||||
);
|
||||
expect(screen.getByRole("tab", { name: "Terminal 1" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("tab", { name: "Terminal 2" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("marks the active tab as selected", () => {
|
||||
const tabs = [
|
||||
{ id: "tab1", label: "Tab 1" },
|
||||
{ id: "tab2", label: "Tab 2" },
|
||||
];
|
||||
render(
|
||||
(
|
||||
<TerminalPanel
|
||||
open={true}
|
||||
onClose={onClose}
|
||||
tabs={tabs}
|
||||
activeTab="tab2"
|
||||
token="test-token"
|
||||
/>
|
||||
) as ReactElement
|
||||
);
|
||||
const tab2 = screen.getByRole("tab", { name: "Tab 2" });
|
||||
expect(tab2).toHaveAttribute("aria-selected", "true");
|
||||
});
|
||||
|
||||
it("calls onTabChange when a tab is clicked", () => {
|
||||
const tabs = [
|
||||
{ id: "tab1", label: "Tab 1" },
|
||||
{ id: "tab2", label: "Tab 2" },
|
||||
];
|
||||
render(
|
||||
(
|
||||
<TerminalPanel
|
||||
open={true}
|
||||
onClose={onClose}
|
||||
tabs={tabs}
|
||||
onTabChange={onTabChange}
|
||||
token="test-token"
|
||||
/>
|
||||
) as ReactElement
|
||||
);
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Tab 2" }));
|
||||
expect(onTabChange).toHaveBeenCalledWith("tab2");
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Close button
|
||||
// ==========================================
|
||||
|
||||
describe("close button", () => {
|
||||
it("renders the close button", () => {
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
expect(screen.getByRole("button", { name: "Close terminal" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onClose when close button is clicked", () => {
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Close terminal" }));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// Accessibility
|
||||
// ==========================================
|
||||
|
||||
describe("accessibility", () => {
|
||||
it("has aria-hidden=true when closed", () => {
|
||||
const { container } = render(
|
||||
(<TerminalPanel open={false} onClose={onClose} token="test-token" />) as ReactElement
|
||||
);
|
||||
// When aria-hidden=true, testing-library role queries ignore the element's aria-label.
|
||||
// Use a direct DOM query to verify the attribute.
|
||||
const panel = container.querySelector('[role="region"][aria-label="Terminal panel"]');
|
||||
expect(panel).toHaveAttribute("aria-hidden", "true");
|
||||
});
|
||||
|
||||
it("has aria-hidden=false when open", () => {
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
const panel = screen.getByRole("region", { name: "Terminal panel" });
|
||||
expect(panel).toHaveAttribute("aria-hidden", "false");
|
||||
});
|
||||
|
||||
it("has tablist role on the tab bar", () => {
|
||||
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
||||
expect(screen.getByRole("tablist")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user