Files
stack/apps/web/src/components/mission-control/BargeInInput.test.tsx
Jason Woltje f0aa3b5a75
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
test(web): add Mission Control frontend tests for phase 2 gate
2026-03-07 15:25:39 -06:00

156 lines
5.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import * as MosaicUi from "@mosaic/ui";
import type { ButtonHTMLAttributes, ReactNode } from "react";
interface MockButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
}
const mockApiPost = vi.fn<(endpoint: string, body?: unknown) => Promise<{ message?: string }>>();
const mockShowToast = vi.fn<(message: string, variant?: string) => void>();
const useToastSpy = vi.spyOn(MosaicUi, "useToast");
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>
),
}));
import { BargeInInput } from "./BargeInInput";
describe("BargeInInput", (): void => {
beforeEach((): void => {
vi.clearAllMocks();
vi.stubGlobal("fetch", vi.fn());
mockApiPost.mockResolvedValue({ message: "ok" });
useToastSpy.mockReturnValue({
showToast: mockShowToast,
removeToast: vi.fn(),
} as ReturnType<typeof MosaicUi.useToast>);
});
afterEach((): void => {
vi.unstubAllGlobals();
});
it("renders input controls and keeps send disabled for empty content", (): void => {
render(<BargeInInput sessionId="session-1" />);
expect(screen.getByLabelText("Inject message")).toBeInTheDocument();
expect(screen.getByRole("checkbox", { name: "Pause before send" })).not.toBeChecked();
expect(screen.getByRole("button", { name: "Send" })).toBeDisabled();
});
it("sends a trimmed message and clears the textarea", async (): Promise<void> => {
const onSent = vi.fn<() => void>();
const user = userEvent.setup();
render(<BargeInInput sessionId="session-1" onSent={onSent} />);
const textarea = screen.getByLabelText("Inject message");
await user.type(textarea, " execute plan ");
await user.click(screen.getByRole("button", { name: "Send" }));
await waitFor((): void => {
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/session-1/inject", {
content: "execute plan",
});
});
expect(onSent).toHaveBeenCalledTimes(1);
expect(textarea).toHaveValue("");
});
it("pauses and resumes the session around injection when checkbox is enabled", async (): Promise<void> => {
const user = userEvent.setup();
render(<BargeInInput sessionId="session-2" />);
await user.click(screen.getByRole("checkbox", { name: "Pause before send" }));
await user.type(screen.getByLabelText("Inject message"), "hello world");
await user.click(screen.getByRole("button", { name: "Send" }));
await waitFor((): void => {
expect(mockApiPost).toHaveBeenCalledTimes(3);
});
const calls = mockApiPost.mock.calls as [string, unknown?][];
expect(calls[0]).toEqual(["/api/mission-control/sessions/session-2/pause", undefined]);
expect(calls[1]).toEqual([
"/api/mission-control/sessions/session-2/inject",
{ content: "hello world" },
]);
expect(calls[2]).toEqual(["/api/mission-control/sessions/session-2/resume", undefined]);
});
it("submits with Enter and does not submit on Shift+Enter", async (): Promise<void> => {
const user = userEvent.setup();
render(<BargeInInput sessionId="session-3" />);
const textarea = screen.getByLabelText("Inject message");
await user.type(textarea, "first");
fireEvent.keyDown(textarea, { key: "Enter", code: "Enter", shiftKey: true });
expect(mockApiPost).not.toHaveBeenCalled();
fireEvent.keyDown(textarea, { key: "Enter", code: "Enter", shiftKey: false });
await waitFor((): void => {
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/session-3/inject", {
content: "first",
});
});
});
it("shows an inline error and toast when injection fails", async (): Promise<void> => {
const user = userEvent.setup();
mockApiPost.mockRejectedValueOnce(new Error("Injection failed"));
render(<BargeInInput sessionId="session-4" />);
await user.type(screen.getByLabelText("Inject message"), "help");
await user.click(screen.getByRole("button", { name: "Send" }));
await waitFor((): void => {
expect(screen.getByRole("alert")).toHaveTextContent("Injection failed");
});
expect(mockShowToast).toHaveBeenCalledWith("Injection failed", "error");
});
it("reports resume failures after a successful send", async (): Promise<void> => {
const user = userEvent.setup();
mockApiPost
.mockResolvedValueOnce({ message: "paused" })
.mockResolvedValueOnce({ message: "sent" })
.mockRejectedValueOnce(new Error("resume failed"));
render(<BargeInInput sessionId="session-5" />);
await user.click(screen.getByRole("checkbox", { name: "Pause before send" }));
await user.type(screen.getByLabelText("Inject message"), "deploy now");
await user.click(screen.getByRole("button", { name: "Send" }));
await waitFor((): void => {
expect(screen.getByRole("alert")).toHaveTextContent(
"Message sent, but failed to resume session: resume failed"
);
});
expect(mockShowToast).toHaveBeenCalledWith(
"Message sent, but failed to resume session: resume failed",
"error"
);
});
});