import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { MessageComposer } from "../MessageComposer"; import * as apiModule from "../../api"; import type { Agent } from "../../api"; // Mock the API module vi.mock("../../api", () => ({ sendMessage: vi.fn(), })); // Mock lucide-react icons vi.mock("lucide-react", () => ({ X: () => X, Send: () => Send, Loader2: ({ className }: { className?: string }) => ( Loader ), Bot: () => Bot, AlertCircle: () => Alert, })); const mockSendMessage = vi.mocked(apiModule.sendMessage); const mockAgents: Agent[] = [ { id: "agent-001", name: "Test Agent", role: "executor", state: "idle", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {}, }, ]; const defaultProps = { onSend: vi.fn(), onCancel: vi.fn(), addToast: vi.fn(), }; describe("MessageComposer", () => { beforeEach(() => { vi.clearAllMocks(); mockSendMessage.mockResolvedValue({ id: "msg-new", fromId: "dashboard", fromType: "user", toId: "agent-001", toType: "agent", content: "Test message", type: "user-to-agent", read: false, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); }); it("renders the composer with header", () => { render(); expect(screen.getByText("New Message")).toBeDefined(); }); it("shows agent dropdown when agents are provided", () => { render(); const select = screen.getByTestId("message-composer-recipient"); expect(select).toBeDefined(); expect(select.tagName).toBe("SELECT"); }); it("shows text input when no agents provided", () => { render(); const input = screen.getByTestId("message-composer-recipient"); expect(input.tagName).toBe("INPUT"); }); it("disables send button when content is empty", () => { render(); const sendBtn = screen.getByTestId("message-composer-send"); expect(sendBtn.hasAttribute("disabled")).toBe(true); }); it("enables send button when recipient and content are filled", () => { render(); // Select agent fireEvent.change(screen.getByTestId("message-composer-recipient"), { target: { value: "agent-001" }, }); // Type content fireEvent.change(screen.getByTestId("message-composer-content"), { target: { value: "Hello agent!" }, }); const sendBtn = screen.getByTestId("message-composer-send"); expect(sendBtn.hasAttribute("disabled")).toBe(false); }); it("shows character count", () => { render(); expect(screen.getByTestId("message-composer-charcount")).toBeDefined(); expect(screen.getByTestId("message-composer-charcount").textContent).toContain("0/2000"); }); it("updates character count when typing", () => { render(); const textarea = screen.getByTestId("message-composer-content"); fireEvent.change(textarea, { target: { value: "Hello" } }); expect(screen.getByTestId("message-composer-charcount").textContent).toContain("5/2000"); }); it("calls onSend when message is sent successfully", async () => { render(); fireEvent.change(screen.getByTestId("message-composer-recipient"), { target: { value: "agent-001" }, }); fireEvent.change(screen.getByTestId("message-composer-content"), { target: { value: "Hello agent!" }, }); fireEvent.click(screen.getByTestId("message-composer-send")); await waitFor(() => { expect(mockSendMessage).toHaveBeenCalledWith( { toId: "agent-001", toType: "agent", content: "Hello agent!", type: "user-to-agent", }, undefined, ); }); expect(defaultProps.onSend).toHaveBeenCalledOnce(); }); it("shows error when send fails", async () => { mockSendMessage.mockRejectedValue(new Error("Network error")); render(); fireEvent.change(screen.getByTestId("message-composer-recipient"), { target: { value: "agent-001" }, }); fireEvent.change(screen.getByTestId("message-composer-content"), { target: { value: "Hello agent!" }, }); fireEvent.click(screen.getByTestId("message-composer-send")); await waitFor(() => { expect(screen.getByTestId("message-composer-error")).toBeDefined(); }); expect(screen.getByTestId("message-composer-error").textContent).toContain("Network error"); }); it("calls onCancel when clicking cancel button", () => { render(); fireEvent.click(screen.getByTestId("message-composer-cancel")); expect(defaultProps.onCancel).toHaveBeenCalledOnce(); }); it("calls onCancel when clicking cancel footer button", () => { render(); fireEvent.click(screen.getByTestId("message-composer-cancel-btn")); expect(defaultProps.onCancel).toHaveBeenCalledOnce(); }); it("pre-fills recipient when provided", () => { render( , ); // When recipient is pre-filled, it shows a fixed label instead of dropdown expect(screen.getByText("agent-001")).toBeDefined(); }); it("shows loading state while sending", async () => { mockSendMessage.mockImplementation(() => new Promise(() => {})); // Never resolves render(); fireEvent.change(screen.getByTestId("message-composer-recipient"), { target: { value: "agent-001" }, }); fireEvent.change(screen.getByTestId("message-composer-content"), { target: { value: "Hello agent!" }, }); fireEvent.click(screen.getByTestId("message-composer-send")); await waitFor(() => { expect(screen.getByTestId("icon-loader")).toBeDefined(); }); }); it("passes projectId to sendMessage", async () => { render(); fireEvent.change(screen.getByTestId("message-composer-recipient"), { target: { value: "agent-001" }, }); fireEvent.change(screen.getByTestId("message-composer-content"), { target: { value: "Hello!" }, }); fireEvent.click(screen.getByTestId("message-composer-send")); await waitFor(() => { expect(mockSendMessage).toHaveBeenCalledWith( expect.anything(), "proj-1", ); }); }); });