test(web): add Mission Control frontend tests for phase 2 gate
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:
161
apps/web/src/components/mission-control/PanelControls.test.tsx
Normal file
161
apps/web/src/components/mission-control/PanelControls.test.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { afterEach, 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;
|
||||
}
|
||||
|
||||
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/badge", () => ({
|
||||
Badge: ({ children, ...props }: MockBadgeProps): React.JSX.Element => (
|
||||
<span {...props}>{children}</span>
|
||||
),
|
||||
}));
|
||||
|
||||
import { PanelControls } from "./PanelControls";
|
||||
|
||||
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>);
|
||||
}
|
||||
|
||||
describe("PanelControls", (): void => {
|
||||
beforeEach((): void => {
|
||||
vi.clearAllMocks();
|
||||
vi.stubGlobal("fetch", vi.fn());
|
||||
mockApiPost.mockResolvedValue({ message: "ok" });
|
||||
});
|
||||
|
||||
afterEach((): void => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("renders action buttons with correct disabled state for active sessions", (): void => {
|
||||
renderWithQueryClient(<PanelControls sessionId="session-1" status="active" />);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Pause session" })).toBeEnabled();
|
||||
expect(screen.getByRole("button", { name: "Resume session" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Gracefully kill session" })).toBeEnabled();
|
||||
expect(screen.getByRole("button", { name: "Force kill session" })).toBeEnabled();
|
||||
});
|
||||
|
||||
it("disables all action buttons when session is already killed", (): void => {
|
||||
renderWithQueryClient(<PanelControls sessionId="session-2" status="killed" />);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Pause session" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Resume session" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Gracefully kill session" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Force kill session" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("pauses a running session and reports the next status", async (): Promise<void> => {
|
||||
const onStatusChange = vi.fn<(status: string) => void>();
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderWithQueryClient(
|
||||
<PanelControls
|
||||
sessionId="session with space"
|
||||
status="active"
|
||||
onStatusChange={onStatusChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Pause session" }));
|
||||
|
||||
await waitFor((): void => {
|
||||
expect(mockApiPost).toHaveBeenCalledWith(
|
||||
"/api/mission-control/sessions/session%20with%20space/pause",
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
expect(onStatusChange).toHaveBeenCalledWith("paused");
|
||||
});
|
||||
|
||||
it("asks for graceful kill confirmation before submitting", async (): Promise<void> => {
|
||||
const onStatusChange = vi.fn<(status: string) => void>();
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderWithQueryClient(
|
||||
<PanelControls sessionId="session-4" status="active" onStatusChange={onStatusChange} />
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Gracefully kill session" }));
|
||||
|
||||
expect(
|
||||
screen.getByText("Gracefully stop this agent after it finishes the current step?")
|
||||
).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Confirm" }));
|
||||
|
||||
await waitFor((): void => {
|
||||
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/session-4/kill", {
|
||||
force: false,
|
||||
});
|
||||
});
|
||||
|
||||
expect(onStatusChange).toHaveBeenCalledWith("killed");
|
||||
});
|
||||
|
||||
it("sends force kill after confirmation", async (): Promise<void> => {
|
||||
const onStatusChange = vi.fn<(status: string) => void>();
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderWithQueryClient(
|
||||
<PanelControls sessionId="session-5" status="paused" onStatusChange={onStatusChange} />
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Force kill session" }));
|
||||
|
||||
expect(screen.getByText("This will hard-kill the agent immediately.")).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Confirm" }));
|
||||
|
||||
await waitFor((): void => {
|
||||
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/session-5/kill", {
|
||||
force: true,
|
||||
});
|
||||
});
|
||||
|
||||
expect(onStatusChange).toHaveBeenCalledWith("killed");
|
||||
});
|
||||
|
||||
it("shows an error badge when an action fails", async (): Promise<void> => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
mockApiPost.mockRejectedValueOnce(new Error("unable to pause"));
|
||||
|
||||
renderWithQueryClient(<PanelControls sessionId="session-6" status="active" />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Pause session" }));
|
||||
|
||||
await waitFor((): void => {
|
||||
expect(screen.getByText("unable to pause")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user