All checks were successful
ci/woodpecker/push/web Pipeline was successful
Implements read-only agent output viewing in the TerminalPanel as a separate tab group alongside interactive PTY terminal sessions. - useAgentStream hook: SSE connection to /api/orchestrator/events with exponential backoff reconnect, per-agent output accumulation, and full lifecycle tracking (spawning→running→completed/error) - AgentTerminal component: read-only <pre>-based output view with ANSI stripping, status indicator (pulse/dot), status badge, elapsed duration, copy-to-clipboard, and error message overlay - TerminalPanel integration: agent tabs appear automatically when agents are active, show colored status dots, dismissable when completed/error, and section divider separates terminal vs agent tabs - 79 new unit tests across useAgentStream, AgentTerminal, and TerminalPanel Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
582 lines
22 KiB
TypeScript
582 lines
22 KiB
TypeScript
/**
|
|
* @file TerminalPanel.test.tsx
|
|
* @description Unit tests for the TerminalPanel component — multi-tab scenarios
|
|
*/
|
|
|
|
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(
|
|
({
|
|
sessionId,
|
|
isVisible,
|
|
sessionStatus,
|
|
}: {
|
|
sessionId: string;
|
|
isVisible: boolean;
|
|
sessionStatus: string;
|
|
}) => (
|
|
<div
|
|
data-testid="mock-xterminal"
|
|
data-session-id={sessionId}
|
|
data-visible={isVisible ? "true" : "false"}
|
|
data-status={sessionStatus}
|
|
/>
|
|
)
|
|
),
|
|
}));
|
|
|
|
// Mock AgentTerminal to avoid complexity in panel tests
|
|
vi.mock("./AgentTerminal", () => ({
|
|
AgentTerminal: vi.fn(
|
|
({ agent }: { agent: { agentId: string; agentType: string; status: string } }) => (
|
|
<div
|
|
data-testid="mock-agent-terminal"
|
|
data-agent-id={agent.agentId}
|
|
data-agent-type={agent.agentType}
|
|
data-status={agent.status}
|
|
/>
|
|
)
|
|
),
|
|
}));
|
|
|
|
// Mock useTerminalSessions
|
|
const mockCreateSession = vi.fn();
|
|
const mockCloseSession = vi.fn();
|
|
const mockRenameSession = vi.fn();
|
|
const mockSetActiveSession = vi.fn();
|
|
const mockSendInput = vi.fn();
|
|
const mockResize = vi.fn();
|
|
const mockRegisterOutputCallback = vi.fn(() => vi.fn());
|
|
|
|
// Mutable state for the mock — tests update these
|
|
let mockSessions = new Map<
|
|
string,
|
|
{
|
|
sessionId: string;
|
|
name: string;
|
|
status: "active" | "exited";
|
|
exitCode?: number;
|
|
}
|
|
>();
|
|
let mockActiveSessionId: string | null = null;
|
|
let mockIsConnected = false;
|
|
let mockConnectionError: string | null = null;
|
|
|
|
vi.mock("@/hooks/useTerminalSessions", () => ({
|
|
useTerminalSessions: vi.fn(() => ({
|
|
sessions: mockSessions,
|
|
activeSessionId: mockActiveSessionId,
|
|
isConnected: mockIsConnected,
|
|
connectionError: mockConnectionError,
|
|
createSession: mockCreateSession,
|
|
closeSession: mockCloseSession,
|
|
renameSession: mockRenameSession,
|
|
setActiveSession: mockSetActiveSession,
|
|
sendInput: mockSendInput,
|
|
resize: mockResize,
|
|
registerOutputCallback: mockRegisterOutputCallback,
|
|
})),
|
|
}));
|
|
|
|
// Mock useAgentStream
|
|
const mockDismissAgent = vi.fn();
|
|
let mockAgents = new Map<
|
|
string,
|
|
{
|
|
agentId: string;
|
|
agentType: string;
|
|
status: "spawning" | "running" | "completed" | "error";
|
|
outputLines: string[];
|
|
startedAt: number;
|
|
}
|
|
>();
|
|
let mockAgentStreamConnected = false;
|
|
|
|
vi.mock("@/hooks/useAgentStream", () => ({
|
|
useAgentStream: vi.fn(() => ({
|
|
agents: mockAgents,
|
|
isConnected: mockAgentStreamConnected,
|
|
connectionError: null,
|
|
dismissAgent: mockDismissAgent,
|
|
})),
|
|
}));
|
|
|
|
import { TerminalPanel } from "./TerminalPanel";
|
|
|
|
// ==========================================
|
|
// Helpers
|
|
// ==========================================
|
|
|
|
function setTwoSessions(): void {
|
|
mockSessions = new Map([
|
|
["session-1", { sessionId: "session-1", name: "Terminal 1", status: "active" }],
|
|
["session-2", { sessionId: "session-2", name: "Terminal 2", status: "active" }],
|
|
]);
|
|
mockActiveSessionId = "session-1";
|
|
}
|
|
|
|
// ==========================================
|
|
// Tests
|
|
// ==========================================
|
|
|
|
describe("TerminalPanel", () => {
|
|
const onClose = vi.fn();
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockSessions = new Map();
|
|
mockActiveSessionId = null;
|
|
mockIsConnected = false;
|
|
mockConnectionError = null;
|
|
mockRegisterOutputCallback.mockReturnValue(vi.fn());
|
|
mockAgents = new Map();
|
|
mockAgentStreamConnected = false;
|
|
});
|
|
|
|
// ==========================================
|
|
// 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("renders empty state when no sessions exist", () => {
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
// No XTerminal instances should be mounted
|
|
expect(screen.queryByTestId("mock-xterminal")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("shows connecting message in empty state when not connected", () => {
|
|
mockIsConnected = false;
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(screen.getByText("Connecting...")).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows creating message in empty state when connected", () => {
|
|
mockIsConnected = true;
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(screen.getByText("Creating terminal...")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Tab bar from sessions
|
|
// ==========================================
|
|
|
|
describe("tab bar", () => {
|
|
it("renders a tab for each session", () => {
|
|
setTwoSessions();
|
|
render((<TerminalPanel open={true} onClose={onClose} 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 session tab as selected", () => {
|
|
setTwoSessions();
|
|
mockActiveSessionId = "session-2";
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(screen.getByRole("tab", { name: "Terminal 2" })).toHaveAttribute(
|
|
"aria-selected",
|
|
"true"
|
|
);
|
|
expect(screen.getByRole("tab", { name: "Terminal 1" })).toHaveAttribute(
|
|
"aria-selected",
|
|
"false"
|
|
);
|
|
});
|
|
|
|
it("calls setActiveSession when a tab is clicked", () => {
|
|
setTwoSessions();
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
fireEvent.click(screen.getByRole("tab", { name: "Terminal 2" }));
|
|
expect(mockSetActiveSession).toHaveBeenCalledWith("session-2");
|
|
});
|
|
|
|
it("has tablist role on the tab bar", () => {
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(screen.getByRole("tablist")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// New tab button
|
|
// ==========================================
|
|
|
|
describe("new tab button", () => {
|
|
it("renders the new tab button", () => {
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(screen.getByRole("button", { name: "New terminal tab" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("calls createSession when new tab button is clicked", () => {
|
|
setTwoSessions();
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
fireEvent.click(screen.getByRole("button", { name: "New terminal tab" }));
|
|
expect(mockCreateSession).toHaveBeenCalledWith(
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
expect.objectContaining({ name: expect.any(String) })
|
|
);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Per-tab close button
|
|
// ==========================================
|
|
|
|
describe("per-tab close button", () => {
|
|
it("renders a close button for each tab", () => {
|
|
setTwoSessions();
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(screen.getByRole("button", { name: "Close Terminal 1" })).toBeInTheDocument();
|
|
expect(screen.getByRole("button", { name: "Close Terminal 2" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("calls closeSession with the correct sessionId when tab close is clicked", () => {
|
|
setTwoSessions();
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
fireEvent.click(screen.getByRole("button", { name: "Close Terminal 1" }));
|
|
expect(mockCloseSession).toHaveBeenCalledWith("session-1");
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Panel close button
|
|
// ==========================================
|
|
|
|
describe("panel close button", () => {
|
|
it("renders the close panel 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 panel 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);
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Multi-tab XTerminal rendering
|
|
// ==========================================
|
|
|
|
describe("multi-tab terminal rendering", () => {
|
|
it("renders an XTerminal for each session", () => {
|
|
setTwoSessions();
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
const terminals = screen.getAllByTestId("mock-xterminal");
|
|
expect(terminals).toHaveLength(2);
|
|
});
|
|
|
|
it("shows the active session terminal as visible", () => {
|
|
setTwoSessions();
|
|
mockActiveSessionId = "session-1";
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
const terminal1 = screen
|
|
.getAllByTestId("mock-xterminal")
|
|
.find((el) => el.getAttribute("data-session-id") === "session-1");
|
|
expect(terminal1).toHaveAttribute("data-visible", "true");
|
|
});
|
|
|
|
it("hides inactive session terminals", () => {
|
|
setTwoSessions();
|
|
mockActiveSessionId = "session-1";
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
const terminal2 = screen
|
|
.getAllByTestId("mock-xterminal")
|
|
.find((el) => el.getAttribute("data-session-id") === "session-2");
|
|
expect(terminal2).toHaveAttribute("data-visible", "false");
|
|
});
|
|
|
|
it("passes sessionStatus to XTerminal", () => {
|
|
mockSessions = new Map([
|
|
[
|
|
"session-1",
|
|
{ sessionId: "session-1", name: "Terminal 1", status: "exited", exitCode: 0 },
|
|
],
|
|
]);
|
|
mockActiveSessionId = "session-1";
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
const terminal = screen.getByTestId("mock-xterminal");
|
|
expect(terminal).toHaveAttribute("data-status", "exited");
|
|
});
|
|
|
|
it("passes isVisible=false to all terminals when panel is closed", () => {
|
|
setTwoSessions();
|
|
const { container } = render(
|
|
(<TerminalPanel open={false} onClose={onClose} token="test-token" />) as ReactElement
|
|
);
|
|
const terminals = container.querySelectorAll('[data-testid="mock-xterminal"]');
|
|
terminals.forEach((terminal) => {
|
|
expect(terminal).toHaveAttribute("data-visible", "false");
|
|
});
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Inline tab rename
|
|
// ==========================================
|
|
|
|
describe("tab rename", () => {
|
|
it("shows a rename input when a tab is double-clicked", () => {
|
|
setTwoSessions();
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
fireEvent.dblClick(screen.getByRole("tab", { name: "Terminal 1" }));
|
|
expect(screen.getByTestId("tab-rename-input")).toBeInTheDocument();
|
|
});
|
|
|
|
it("calls renameSession when rename input loses focus", () => {
|
|
setTwoSessions();
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
fireEvent.dblClick(screen.getByRole("tab", { name: "Terminal 1" }));
|
|
|
|
const input = screen.getByTestId("tab-rename-input");
|
|
fireEvent.change(input, { target: { value: "Custom Shell" } });
|
|
fireEvent.blur(input);
|
|
|
|
expect(mockRenameSession).toHaveBeenCalledWith("session-1", "Custom Shell");
|
|
});
|
|
|
|
it("calls renameSession when Enter is pressed in the rename input", () => {
|
|
setTwoSessions();
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
fireEvent.dblClick(screen.getByRole("tab", { name: "Terminal 1" }));
|
|
|
|
const input = screen.getByTestId("tab-rename-input");
|
|
fireEvent.change(input, { target: { value: "New Name" } });
|
|
fireEvent.keyDown(input, { key: "Enter" });
|
|
|
|
expect(mockRenameSession).toHaveBeenCalledWith("session-1", "New Name");
|
|
});
|
|
|
|
it("cancels rename when Escape is pressed", () => {
|
|
setTwoSessions();
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
fireEvent.dblClick(screen.getByRole("tab", { name: "Terminal 1" }));
|
|
|
|
const input = screen.getByTestId("tab-rename-input");
|
|
fireEvent.change(input, { target: { value: "Abandoned Name" } });
|
|
fireEvent.keyDown(input, { key: "Escape" });
|
|
|
|
expect(mockRenameSession).not.toHaveBeenCalled();
|
|
expect(screen.queryByTestId("tab-rename-input")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Connection error banner
|
|
// ==========================================
|
|
|
|
describe("connection error", () => {
|
|
it("shows a connection error banner when connectionError is set", () => {
|
|
mockConnectionError = "WebSocket connection failed";
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
const alert = screen.getByRole("alert");
|
|
expect(alert).toBeInTheDocument();
|
|
expect(alert).toHaveTextContent(/WebSocket connection failed/);
|
|
});
|
|
|
|
it("does not show the error banner when connectionError is null", () => {
|
|
mockConnectionError = null;
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Accessibility
|
|
// ==========================================
|
|
|
|
describe("accessibility", () => {
|
|
it("has aria-hidden=true 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).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");
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Auto-create session
|
|
// ==========================================
|
|
|
|
describe("auto-create first session", () => {
|
|
it("calls createSession when connected and no sessions exist", () => {
|
|
mockIsConnected = true;
|
|
mockSessions = new Map();
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(mockCreateSession).toHaveBeenCalledWith(
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
expect.objectContaining({ name: expect.any(String) })
|
|
);
|
|
});
|
|
|
|
it("does not call createSession when sessions already exist", () => {
|
|
mockIsConnected = true;
|
|
setTwoSessions();
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(mockCreateSession).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not call createSession when not connected", () => {
|
|
mockIsConnected = false;
|
|
mockSessions = new Map();
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(mockCreateSession).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not call createSession when panel is closed", () => {
|
|
mockIsConnected = true;
|
|
mockSessions = new Map();
|
|
render((<TerminalPanel open={false} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(mockCreateSession).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
// ==========================================
|
|
// Agent tab integration
|
|
// ==========================================
|
|
|
|
describe("agent tab integration", () => {
|
|
function setOneAgent(status: "spawning" | "running" | "completed" | "error" = "running"): void {
|
|
mockAgents = new Map([
|
|
[
|
|
"agent-1",
|
|
{
|
|
agentId: "agent-1",
|
|
agentType: "worker",
|
|
status,
|
|
outputLines: ["Hello from agent\n"],
|
|
startedAt: Date.now() - 3000,
|
|
},
|
|
],
|
|
]);
|
|
}
|
|
|
|
it("renders an agent tab when an agent is active", () => {
|
|
setOneAgent("running");
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(screen.getAllByTestId("agent-tab")).toHaveLength(1);
|
|
});
|
|
|
|
it("renders no agent tabs when agents map is empty", () => {
|
|
mockAgents = new Map();
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(screen.queryByTestId("agent-tab")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("agent tab button has the agent type as label", () => {
|
|
setOneAgent("running");
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(screen.getByRole("tab", { name: "Agent: worker" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("agent tab has role=tab", () => {
|
|
setOneAgent("running");
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(screen.getByRole("tab", { name: "Agent: worker" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows dismiss button for completed agents", () => {
|
|
setOneAgent("completed");
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(screen.getByRole("button", { name: "Dismiss worker agent" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows dismiss button for error agents", () => {
|
|
setOneAgent("error");
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(screen.getByRole("button", { name: "Dismiss worker agent" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("does not show dismiss button for running agents", () => {
|
|
setOneAgent("running");
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(
|
|
screen.queryByRole("button", { name: "Dismiss worker agent" })
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("does not show dismiss button for spawning agents", () => {
|
|
setOneAgent("spawning");
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(
|
|
screen.queryByRole("button", { name: "Dismiss worker agent" })
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("calls dismissAgent when dismiss button is clicked", () => {
|
|
setOneAgent("completed");
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
fireEvent.click(screen.getByRole("button", { name: "Dismiss worker agent" }));
|
|
expect(mockDismissAgent).toHaveBeenCalledWith("agent-1");
|
|
});
|
|
|
|
it("renders AgentTerminal when agent tab is active", () => {
|
|
setOneAgent("running");
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
// Click the agent tab to make it active
|
|
fireEvent.click(screen.getByRole("tab", { name: "Agent: worker" }));
|
|
// AgentTerminal should be rendered (mock shows mock-agent-terminal)
|
|
expect(screen.getByTestId("mock-agent-terminal")).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows a divider between terminal and agent tabs", () => {
|
|
setTwoSessions();
|
|
setOneAgent("running");
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
// The divider div is aria-hidden; check it's present in the DOM
|
|
const tablist = screen.getByRole("tablist");
|
|
const divider = tablist.querySelector('[aria-hidden="true"][style*="width: 1"]');
|
|
expect(divider).toBeInTheDocument();
|
|
});
|
|
|
|
it("agent tabs show correct data-agent-status", () => {
|
|
setOneAgent("running");
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
const tab = screen.getByTestId("agent-tab");
|
|
expect(tab).toHaveAttribute("data-agent-status", "running");
|
|
});
|
|
|
|
it("empty state not shown when agents exist but no terminal sessions", () => {
|
|
mockSessions = new Map();
|
|
setOneAgent("running");
|
|
mockIsConnected = false;
|
|
render((<TerminalPanel open={true} onClose={onClose} token="test-token" />) as ReactElement);
|
|
expect(screen.queryByText("Connecting...")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|