Files
kandev/apps/web/components/task/chat/chat-input-area.test.tsx
T
2026-06-27 16:11:39 +01:00

177 lines
4.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
act,
cleanup,
fireEvent,
render,
renderHook,
screen,
waitFor,
} from "@testing-library/react";
const archiveAndSwitchMock = vi.fn();
const toastMock = vi.fn();
const handleSendMessageMock = vi.fn();
const taskPRsByTaskId = vi.hoisted(() => ({
value: {
"task-1": [{ pr_number: 42, state: "merged" }],
} as Record<string, Array<{ pr_number: number; state: string }>>,
}));
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (state: typeof mockState) => unknown) => selector(mockState),
useAppStoreApi: () => ({ getState: () => mockState }),
}));
vi.mock("@/hooks/use-task-actions", () => ({
useArchiveAndSwitchTask: () => archiveAndSwitchMock,
}));
vi.mock("@/components/toast-provider", () => ({
useToast: () => ({ toast: toastMock }),
}));
vi.mock("@/components/github/pr-status-chip", () => ({
PRStatusChip: () => null,
}));
vi.mock("@/components/task/share/share-button", () => ({
ShareButton: () => null,
shareableSessionStateClient: () => false,
}));
vi.mock("@/components/task/chat/chat-input-container", () => ({
ChatInputContainer: () => null,
}));
vi.mock("@/components/task/chat/todo-indicator", () => ({
TodoIndicator: () => null,
}));
vi.mock("@/hooks/use-keyboard-shortcut", () => ({
useKeyboardShortcut: () => undefined,
}));
vi.mock("@/hooks/use-message-handler", () => ({
buildTaskMentionsContext: vi.fn(),
useMessageHandler: () => ({ handleSendMessage: handleSendMessageMock }),
}));
vi.mock("@/hooks/domains/kanban/use-plan-actions", () => ({
usePlanActions: () => ({
implementPlanHandler: vi.fn(),
proceedStepName: null,
proceed: vi.fn(),
isMoving: false,
}),
}));
vi.mock("@/hooks/domains/session/use-executor-environment-availability", () => ({
useExecutorEnvironmentAvailability: () => ({
unavailable: false,
status: null,
}),
}));
vi.mock("@/lib/ws/connection", () => ({
getWebSocketClient: () => ({ send: vi.fn() }),
}));
vi.mock("@/lib/local-storage", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/local-storage")>()),
markPRClosedBannerDismissed: vi.fn(),
markPRMergedBannerDismissed: vi.fn(),
wasPRClosedBannerDismissed: () => false,
wasPRMergedBannerDismissed: () => false,
}));
const mockState = {
taskPRs: {
get byTaskId() {
return taskPRsByTaskId.value;
},
},
};
import { PRMergedBanner, useSubmitHandler } from "./chat-input-area";
beforeEach(() => {
archiveAndSwitchMock.mockResolvedValue(undefined);
handleSendMessageMock.mockResolvedValue(undefined);
taskPRsByTaskId.value = {
"task-1": [{ pr_number: 42, state: "merged" }],
};
});
afterEach(() => {
cleanup();
vi.restoreAllMocks();
vi.clearAllMocks();
});
describe("PRMergedBanner", () => {
it("archives without showing a success toast", async () => {
render(<PRMergedBanner taskId="task-1" />);
fireEvent.click(screen.getByTestId("pr-merged-archive-button"));
await waitFor(() => expect(archiveAndSwitchMock).toHaveBeenCalledWith("task-1"));
expect(toastMock).not.toHaveBeenCalled();
});
it("keeps the failure toast when archive fails", async () => {
archiveAndSwitchMock.mockRejectedValueOnce(new Error("archive failed"));
render(<PRMergedBanner taskId="task-1" />);
fireEvent.click(screen.getByTestId("pr-merged-archive-button"));
await waitFor(() =>
expect(toastMock).toHaveBeenCalledWith({
description: "Failed to archive task",
variant: "error",
}),
);
});
});
describe("useSubmitHandler", () => {
function panelState(overrides = {}) {
return {
resolvedSessionId: "session-1",
taskId: "task-1",
sessionModel: null,
activeModel: null,
isAgentBusy: false,
activeDocument: null,
planComments: [],
pendingPRFeedback: [],
contextFiles: [],
prompts: [],
markCommentsSent: vi.fn(),
clearSessionPlanComments: vi.fn(),
handleClearPRFeedback: vi.fn(),
clearEphemeral: vi.fn(),
addContextFile: vi.fn(),
planModeEnabled: false,
...overrides,
} as never;
}
it("shows a toast when sending fails", async () => {
vi.spyOn(console, "error").mockImplementation(() => undefined);
handleSendMessageMock.mockRejectedValueOnce(new Error("WebSocket request timed out"));
const { result } = renderHook(() => useSubmitHandler(panelState()));
await act(async () => {
await result.current.handleSubmit("hello");
});
expect(toastMock).toHaveBeenCalledWith({
title: "Message send status unknown",
description:
"The connection dropped or timed out. Refresh the task to confirm whether it went through.",
variant: "error",
});
});
});