test(web): MS23-P2-009 Mission Control frontend tests
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
This commit is contained in:
205
apps/web/src/components/mission-control/AuditLogDrawer.test.tsx
Normal file
205
apps/web/src/components/mission-control/AuditLogDrawer.test.tsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react";
|
||||||
|
|
||||||
|
interface MockButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MockBadgeProps extends HTMLAttributes<HTMLElement> {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuditLogEntry {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
sessionId: string;
|
||||||
|
provider: string;
|
||||||
|
action: string;
|
||||||
|
content: string | null;
|
||||||
|
metadata: unknown;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuditLogResponse {
|
||||||
|
items: AuditLogEntry[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockApiGet = vi.fn<(endpoint: string) => Promise<AuditLogResponse>>();
|
||||||
|
|
||||||
|
vi.mock("@/lib/api/client", () => ({
|
||||||
|
apiGet: (endpoint: string): Promise<AuditLogResponse> => mockApiGet(endpoint),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/ui/button", () => ({
|
||||||
|
Button: ({ children, ...props }: MockButtonProps): React.JSX.Element => (
|
||||||
|
<button {...props}>{children}</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/ui/badge", () => ({
|
||||||
|
Badge: ({ children, ...props }: MockBadgeProps): React.JSX.Element => (
|
||||||
|
<span {...props}>{children}</span>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { AuditLogDrawer } from "./AuditLogDrawer";
|
||||||
|
|
||||||
|
function renderWithQueryClient(ui: React.JSX.Element): ReturnType<typeof render> {
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: { retry: false },
|
||||||
|
mutations: { retry: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseWith(items: AuditLogEntry[], page: number, pages: number): AuditLogResponse {
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
total: items.length,
|
||||||
|
page,
|
||||||
|
pages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("AuditLogDrawer", (): void => {
|
||||||
|
beforeEach((): void => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockApiGet.mockResolvedValue(responseWith([], 1, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens from trigger text and renders empty state", async (): Promise<void> => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
renderWithQueryClient(<AuditLogDrawer trigger="Audit" />);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Audit" }));
|
||||||
|
|
||||||
|
await waitFor((): void => {
|
||||||
|
expect(screen.getByText("Audit Log")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("No audit entries found.")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders audit entries with action, session id, and payload", async (): Promise<void> => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
mockApiGet.mockResolvedValue(
|
||||||
|
responseWith(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "entry-1",
|
||||||
|
userId: "operator-1",
|
||||||
|
sessionId: "1234567890abcdef",
|
||||||
|
provider: "internal",
|
||||||
|
action: "inject",
|
||||||
|
content: "Run diagnostics",
|
||||||
|
metadata: { payload: { ignored: true } },
|
||||||
|
createdAt: "2026-03-07T19:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
renderWithQueryClient(<AuditLogDrawer trigger="Audit" />);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Audit" }));
|
||||||
|
|
||||||
|
await waitFor((): void => {
|
||||||
|
expect(screen.getByText("inject")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("12345678")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Run diagnostics")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports pagination and metadata payload summary", async (): Promise<void> => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
mockApiGet.mockImplementation((endpoint: string): Promise<AuditLogResponse> => {
|
||||||
|
const query = endpoint.split("?")[1] ?? "";
|
||||||
|
const params = new URLSearchParams(query);
|
||||||
|
const page = Number(params.get("page") ?? "1");
|
||||||
|
|
||||||
|
if (page === 1) {
|
||||||
|
return Promise.resolve({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "entry-page-1",
|
||||||
|
userId: "operator-2",
|
||||||
|
sessionId: "abcdefgh12345678",
|
||||||
|
provider: "internal",
|
||||||
|
action: "pause",
|
||||||
|
content: "",
|
||||||
|
metadata: { payload: { reason: "hold" } },
|
||||||
|
createdAt: "2026-03-07T19:01:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 2,
|
||||||
|
page: 1,
|
||||||
|
pages: 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "entry-page-2",
|
||||||
|
userId: "operator-3",
|
||||||
|
sessionId: "zzzz111122223333",
|
||||||
|
provider: "internal",
|
||||||
|
action: "kill",
|
||||||
|
content: null,
|
||||||
|
metadata: { payload: { force: true } },
|
||||||
|
createdAt: "2026-03-07T19:02:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 2,
|
||||||
|
page: 2,
|
||||||
|
pages: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
renderWithQueryClient(<AuditLogDrawer trigger="Audit" />);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Audit" }));
|
||||||
|
|
||||||
|
await waitFor((): void => {
|
||||||
|
expect(screen.getByText("Page 1 of 2")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("reason=hold")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Next" }));
|
||||||
|
|
||||||
|
await waitFor((): void => {
|
||||||
|
expect(screen.getByText("Page 2 of 2")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("force=true")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes sessionId filter in query string", async (): Promise<void> => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
renderWithQueryClient(<AuditLogDrawer trigger="Audit" sessionId="session 7" />);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Audit" }));
|
||||||
|
|
||||||
|
await waitFor((): void => {
|
||||||
|
expect(mockApiGet).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstCall = mockApiGet.mock.calls[0];
|
||||||
|
const endpoint = firstCall?.[0] ?? "";
|
||||||
|
|
||||||
|
expect(endpoint).toContain("sessionId=session+7");
|
||||||
|
});
|
||||||
|
});
|
||||||
170
apps/web/src/components/mission-control/KillAllDialog.test.tsx
Normal file
170
apps/web/src/components/mission-control/KillAllDialog.test.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import type {
|
||||||
|
ButtonHTMLAttributes,
|
||||||
|
InputHTMLAttributes,
|
||||||
|
LabelHTMLAttributes,
|
||||||
|
ReactNode,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
interface MockButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MockInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MockLabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MockSession {
|
||||||
|
id: string;
|
||||||
|
providerId: string;
|
||||||
|
providerType: string;
|
||||||
|
status: "active" | "paused";
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockApiPost = vi.fn<(endpoint: string, body?: unknown) => Promise<{ message: string }>>();
|
||||||
|
|
||||||
|
vi.mock("@/lib/api/client", () => ({
|
||||||
|
apiPost: (endpoint: string, body?: unknown): Promise<{ message: string }> =>
|
||||||
|
mockApiPost(endpoint, body),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/ui/button", () => ({
|
||||||
|
Button: ({ children, ...props }: MockButtonProps): React.JSX.Element => (
|
||||||
|
<button {...props}>{children}</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/ui/input", () => ({
|
||||||
|
Input: ({ ...props }: MockInputProps): React.JSX.Element => <input {...props} />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/ui/label", () => ({
|
||||||
|
Label: ({ children, ...props }: MockLabelProps): React.JSX.Element => (
|
||||||
|
<label {...props}>{children}</label>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { KillAllDialog } from "./KillAllDialog";
|
||||||
|
|
||||||
|
function makeSession(overrides: Partial<MockSession>): MockSession {
|
||||||
|
return {
|
||||||
|
id: "session-1",
|
||||||
|
providerId: "internal",
|
||||||
|
providerType: "internal",
|
||||||
|
status: "active",
|
||||||
|
createdAt: new Date("2026-03-07T10:00:00.000Z"),
|
||||||
|
updatedAt: new Date("2026-03-07T10:01:00.000Z"),
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("KillAllDialog", (): void => {
|
||||||
|
beforeEach((): void => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockApiPost.mockResolvedValue({ message: "killed" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders trigger button and requires exact confirmation text", async (): Promise<void> => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
render(<KillAllDialog sessions={[makeSession({})]} />);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Kill All" }));
|
||||||
|
|
||||||
|
const confirmInput = screen.getByLabelText("Type KILL ALL to confirm");
|
||||||
|
const confirmButton = screen.getByRole("button", { name: "Kill All Agents" });
|
||||||
|
|
||||||
|
expect(confirmButton).toBeDisabled();
|
||||||
|
|
||||||
|
await user.type(confirmInput, "kill all");
|
||||||
|
expect(confirmButton).toBeDisabled();
|
||||||
|
|
||||||
|
await user.clear(confirmInput);
|
||||||
|
await user.type(confirmInput, "KILL ALL");
|
||||||
|
|
||||||
|
expect(confirmButton).toBeEnabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("kills only internal sessions by default and invokes completion callback", async (): Promise<void> => {
|
||||||
|
const onComplete = vi.fn<() => void>();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<KillAllDialog
|
||||||
|
sessions={[
|
||||||
|
makeSession({ id: "internal-1", providerType: "internal" }),
|
||||||
|
makeSession({ id: "external-1", providerType: "external" }),
|
||||||
|
]}
|
||||||
|
onComplete={onComplete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Kill All" }));
|
||||||
|
await user.type(screen.getByLabelText("Type KILL ALL to confirm"), "KILL ALL");
|
||||||
|
await user.click(screen.getByRole("button", { name: "Kill All Agents" }));
|
||||||
|
|
||||||
|
await waitFor((): void => {
|
||||||
|
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/internal-1/kill", {
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockApiPost).not.toHaveBeenCalledWith("/api/mission-control/sessions/external-1/kill", {
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
expect(onComplete).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("kills all providers when all scope is selected", async (): Promise<void> => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<KillAllDialog
|
||||||
|
sessions={[
|
||||||
|
makeSession({ id: "internal-2", providerType: "internal" }),
|
||||||
|
makeSession({ id: "external-2", providerType: "external" }),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Kill All" }));
|
||||||
|
await user.click(screen.getByRole("radio", { name: /All providers \(2\)/ }));
|
||||||
|
await user.type(screen.getByLabelText("Type KILL ALL to confirm"), "KILL ALL");
|
||||||
|
await user.click(screen.getByRole("button", { name: "Kill All Agents" }));
|
||||||
|
|
||||||
|
await waitFor((): void => {
|
||||||
|
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/internal-2/kill", {
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/external-2/kill", {
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows empty-scope warning when internal sessions are unavailable", async (): Promise<void> => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<KillAllDialog
|
||||||
|
sessions={[
|
||||||
|
makeSession({ id: "external-only", providerId: "ext", providerType: "external" }),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Kill All" }));
|
||||||
|
await user.type(screen.getByLabelText("Type KILL ALL to confirm"), "KILL ALL");
|
||||||
|
|
||||||
|
expect(screen.getByText("No sessions in the selected scope.")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: "Kill All Agents" })).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
||||||
|
import { MAX_PANEL_COUNT, MissionControlPanel, type PanelConfig } from "./MissionControlPanel";
|
||||||
|
|
||||||
|
interface MockButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MockOrchestratorPanelProps {
|
||||||
|
sessionId?: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
closeDisabled?: boolean;
|
||||||
|
onExpand?: () => void;
|
||||||
|
expanded?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockOrchestratorPanel = vi.fn<(props: MockOrchestratorPanelProps) => React.JSX.Element>();
|
||||||
|
|
||||||
|
vi.mock("@/components/mission-control/OrchestratorPanel", () => ({
|
||||||
|
OrchestratorPanel: (props: MockOrchestratorPanelProps): React.JSX.Element =>
|
||||||
|
mockOrchestratorPanel(props),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/ui/button", () => ({
|
||||||
|
Button: ({ children, ...props }: MockButtonProps): React.JSX.Element => (
|
||||||
|
<button {...props}>{children}</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function buildPanels(count: number): PanelConfig[] {
|
||||||
|
return Array.from({ length: count }, (_, index) => ({
|
||||||
|
sessionId: `session-${String(index + 1)}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("MissionControlPanel", (): void => {
|
||||||
|
beforeEach((): void => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
mockOrchestratorPanel.mockImplementation(
|
||||||
|
({ sessionId, closeDisabled, expanded }: MockOrchestratorPanelProps): React.JSX.Element => (
|
||||||
|
<div
|
||||||
|
data-testid="orchestrator-panel"
|
||||||
|
data-session-id={sessionId ?? ""}
|
||||||
|
data-close-disabled={String(closeDisabled ?? false)}
|
||||||
|
data-expanded={String(expanded ?? false)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the panel grid and default heading", (): void => {
|
||||||
|
render(
|
||||||
|
<MissionControlPanel
|
||||||
|
panels={[{}]}
|
||||||
|
onAddPanel={vi.fn<() => void>()}
|
||||||
|
onRemovePanel={vi.fn<(index: number) => void>()}
|
||||||
|
onExpandPanel={vi.fn<(index: number) => void>()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole("heading", { name: "Panels" })).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByTestId("orchestrator-panel")).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onAddPanel when the add button is clicked", (): void => {
|
||||||
|
const onAddPanel = vi.fn<() => void>();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MissionControlPanel
|
||||||
|
panels={[{}]}
|
||||||
|
onAddPanel={onAddPanel}
|
||||||
|
onRemovePanel={vi.fn<(index: number) => void>()}
|
||||||
|
onExpandPanel={vi.fn<(index: number) => void>()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Add panel" }));
|
||||||
|
|
||||||
|
expect(onAddPanel).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables add panel at the configured maximum", (): void => {
|
||||||
|
render(
|
||||||
|
<MissionControlPanel
|
||||||
|
panels={buildPanels(MAX_PANEL_COUNT)}
|
||||||
|
onAddPanel={vi.fn<() => void>()}
|
||||||
|
onRemovePanel={vi.fn<(index: number) => void>()}
|
||||||
|
onExpandPanel={vi.fn<(index: number) => void>()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const addButton = screen.getByRole("button", { name: "Add panel" });
|
||||||
|
|
||||||
|
expect(addButton).toBeDisabled();
|
||||||
|
expect(addButton).toHaveAttribute("title", "Maximum of 6 panels");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes closeDisabled=false when more than one panel exists", (): void => {
|
||||||
|
render(
|
||||||
|
<MissionControlPanel
|
||||||
|
panels={buildPanels(2)}
|
||||||
|
onAddPanel={vi.fn<() => void>()}
|
||||||
|
onRemovePanel={vi.fn<(index: number) => void>()}
|
||||||
|
onExpandPanel={vi.fn<(index: number) => void>()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderedPanels = screen.getAllByTestId("orchestrator-panel");
|
||||||
|
expect(renderedPanels).toHaveLength(2);
|
||||||
|
|
||||||
|
for (const panel of renderedPanels) {
|
||||||
|
expect(panel).toHaveAttribute("data-close-disabled", "false");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders only the expanded panel in focused mode", (): void => {
|
||||||
|
render(
|
||||||
|
<MissionControlPanel
|
||||||
|
panels={[{ sessionId: "session-1" }, { sessionId: "session-2", expanded: true }]}
|
||||||
|
onAddPanel={vi.fn<() => void>()}
|
||||||
|
onRemovePanel={vi.fn<(index: number) => void>()}
|
||||||
|
onExpandPanel={vi.fn<(index: number) => void>()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderedPanels = screen.getAllByTestId("orchestrator-panel");
|
||||||
|
|
||||||
|
expect(renderedPanels).toHaveLength(1);
|
||||||
|
expect(renderedPanels[0]).toHaveAttribute("data-session-id", "session-2");
|
||||||
|
expect(renderedPanels[0]).toHaveAttribute("data-expanded", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles Escape key by toggling expanded panel", async (): Promise<void> => {
|
||||||
|
const onExpandPanel = vi.fn<(index: number) => void>();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MissionControlPanel
|
||||||
|
panels={[{ sessionId: "session-1", expanded: true }, { sessionId: "session-2" }]}
|
||||||
|
onAddPanel={vi.fn<() => void>()}
|
||||||
|
onRemovePanel={vi.fn<(index: number) => void>()}
|
||||||
|
onExpandPanel={onExpandPanel}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.keyDown(window, { key: "Escape" });
|
||||||
|
|
||||||
|
await waitFor((): void => {
|
||||||
|
expect(onExpandPanel).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user