156 lines
5.4 KiB
TypeScript
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"
|
|
);
|
|
});
|
|
});
|