219 lines
6.5 KiB
TypeScript
219 lines
6.5 KiB
TypeScript
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { render, screen } from "@testing-library/react";
|
|
import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react";
|
|
|
|
interface MockButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
children: ReactNode;
|
|
}
|
|
|
|
interface MockContainerProps extends HTMLAttributes<HTMLElement> {
|
|
children: ReactNode;
|
|
}
|
|
|
|
type MockConnectionStatus = "connected" | "connecting" | "error";
|
|
type MockRole = "user" | "assistant" | "tool" | "system";
|
|
|
|
interface MockMessage {
|
|
id: string;
|
|
role: MockRole;
|
|
content: string;
|
|
timestamp: string;
|
|
}
|
|
|
|
interface MockSession {
|
|
id: string;
|
|
status: string;
|
|
}
|
|
|
|
interface MockSessionStreamResult {
|
|
messages: MockMessage[];
|
|
status: MockConnectionStatus;
|
|
error: string | null;
|
|
}
|
|
|
|
interface MockSessionsResult {
|
|
sessions: MockSession[];
|
|
loading: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
interface MockPanelControlsProps {
|
|
sessionId: string;
|
|
status: string;
|
|
onStatusChange?: (nextStatus: string) => void;
|
|
}
|
|
|
|
interface MockBargeInInputProps {
|
|
sessionId: string;
|
|
}
|
|
|
|
const mockUseSessionStream = vi.fn<(sessionId: string) => MockSessionStreamResult>();
|
|
const mockUseSessions = vi.fn<() => MockSessionsResult>();
|
|
const mockPanelControls = vi.fn<(props: MockPanelControlsProps) => React.JSX.Element>();
|
|
const mockBargeInInput = vi.fn<(props: MockBargeInInputProps) => React.JSX.Element>();
|
|
|
|
vi.mock("date-fns", () => ({
|
|
formatDistanceToNow: (): string => "moments ago",
|
|
}));
|
|
|
|
vi.mock("@/hooks/useMissionControl", () => ({
|
|
useSessionStream: (sessionId: string): MockSessionStreamResult => mockUseSessionStream(sessionId),
|
|
useSessions: (): MockSessionsResult => mockUseSessions(),
|
|
}));
|
|
|
|
vi.mock("@/components/mission-control/PanelControls", () => ({
|
|
PanelControls: (props: MockPanelControlsProps): React.JSX.Element => mockPanelControls(props),
|
|
}));
|
|
|
|
vi.mock("@/components/mission-control/BargeInInput", () => ({
|
|
BargeInInput: (props: MockBargeInInputProps): React.JSX.Element => mockBargeInInput(props),
|
|
}));
|
|
|
|
vi.mock("@/components/ui/button", () => ({
|
|
Button: ({ children, ...props }: MockButtonProps): React.JSX.Element => (
|
|
<button {...props}>{children}</button>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/ui/badge", () => ({
|
|
Badge: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
|
|
<span {...props}>{children}</span>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/ui/card", () => ({
|
|
Card: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
|
|
<section {...props}>{children}</section>
|
|
),
|
|
CardHeader: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
|
|
<header {...props}>{children}</header>
|
|
),
|
|
CardContent: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
|
|
<div {...props}>{children}</div>
|
|
),
|
|
CardTitle: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
|
|
<h2 {...props}>{children}</h2>
|
|
),
|
|
}));
|
|
|
|
vi.mock("@/components/ui/scroll-area", () => ({
|
|
ScrollArea: ({ children, ...props }: MockContainerProps): React.JSX.Element => (
|
|
<div {...props}>{children}</div>
|
|
),
|
|
}));
|
|
|
|
import { OrchestratorPanel } from "./OrchestratorPanel";
|
|
|
|
beforeAll((): void => {
|
|
Object.defineProperty(window.HTMLElement.prototype, "scrollIntoView", {
|
|
configurable: true,
|
|
value: vi.fn(),
|
|
});
|
|
});
|
|
|
|
describe("OrchestratorPanel", (): void => {
|
|
beforeEach((): void => {
|
|
vi.clearAllMocks();
|
|
vi.stubGlobal("fetch", vi.fn());
|
|
|
|
mockUseSessionStream.mockReturnValue({
|
|
messages: [],
|
|
status: "connecting",
|
|
error: null,
|
|
});
|
|
|
|
mockUseSessions.mockReturnValue({
|
|
sessions: [],
|
|
loading: false,
|
|
error: null,
|
|
});
|
|
|
|
mockPanelControls.mockImplementation(
|
|
({ status }: MockPanelControlsProps): React.JSX.Element => (
|
|
<div data-testid="panel-controls">status:{status}</div>
|
|
)
|
|
);
|
|
|
|
mockBargeInInput.mockImplementation(
|
|
({ sessionId }: MockBargeInInputProps): React.JSX.Element => (
|
|
<textarea aria-label="barge-input" data-session-id={sessionId} />
|
|
)
|
|
);
|
|
});
|
|
|
|
afterEach((): void => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("renders a selectable empty state when no session is provided", (): void => {
|
|
render(<OrchestratorPanel />);
|
|
|
|
expect(screen.getByText("Select an agent to view its stream")).toBeInTheDocument();
|
|
expect(screen.queryByText("Session: session-1")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("renders connection indicator and panel controls for an active session", (): void => {
|
|
mockUseSessionStream.mockReturnValue({
|
|
messages: [],
|
|
status: "connected",
|
|
error: null,
|
|
});
|
|
|
|
mockUseSessions.mockReturnValue({
|
|
sessions: [{ id: "session-1", status: "paused" }],
|
|
loading: false,
|
|
error: null,
|
|
});
|
|
|
|
render(<OrchestratorPanel sessionId="session-1" />);
|
|
|
|
expect(screen.getByText("Connected")).toBeInTheDocument();
|
|
expect(screen.getByText("Session: session-1")).toBeInTheDocument();
|
|
expect(screen.getByText("Waiting for messages...")).toBeInTheDocument();
|
|
expect(screen.getByTestId("panel-controls")).toHaveTextContent("status:paused");
|
|
});
|
|
|
|
it("renders stream messages with role and content", (): void => {
|
|
mockUseSessionStream.mockReturnValue({
|
|
status: "connected",
|
|
error: null,
|
|
messages: [
|
|
{
|
|
id: "msg-1",
|
|
role: "assistant",
|
|
content: "Mission accepted.",
|
|
timestamp: "2026-03-07T18:42:00.000Z",
|
|
},
|
|
],
|
|
});
|
|
|
|
render(<OrchestratorPanel sessionId="session-2" />);
|
|
|
|
expect(screen.getByText("assistant")).toBeInTheDocument();
|
|
expect(screen.getByText("Mission accepted.")).toBeInTheDocument();
|
|
expect(screen.getByText("moments ago")).toBeInTheDocument();
|
|
expect(screen.getByLabelText("barge-input")).toHaveAttribute("data-session-id", "session-2");
|
|
});
|
|
|
|
it("renders stream error text when the session has no messages", (): void => {
|
|
mockUseSessionStream.mockReturnValue({
|
|
messages: [],
|
|
status: "error",
|
|
error: "Mission Control stream disconnected.",
|
|
});
|
|
|
|
render(<OrchestratorPanel sessionId="session-3" />);
|
|
|
|
expect(screen.getByText("Error")).toBeInTheDocument();
|
|
expect(screen.getByText("Mission Control stream disconnected.")).toBeInTheDocument();
|
|
});
|
|
|
|
it("respects close button disabled state in panel actions", (): void => {
|
|
const onClose = vi.fn<() => void>();
|
|
|
|
render(<OrchestratorPanel onClose={onClose} closeDisabled />);
|
|
|
|
expect(screen.getByRole("button", { name: "Remove panel" })).toBeDisabled();
|
|
});
|
|
});
|