feat(FN-989): add inter-agent messaging system with mailbox UI and CLI commands
- Add Message types (Message, MessageThread, MessageRecipient) and exports to @fusion/core - Create MessageStore with full CRUD: send, read, delete, inbox, threads, and search - Add messages table migration (schema v12) with SQLite full-text search support - Add REST API routes for messaging (CRUD, search, broadcast, unread count) - Add frontend API client functions for all messaging endpoints - Build MailboxModal and MessageComposer dashboard components with header integration - Add CLI message commands (inbox, send, read, delete) with rich output formatting - Add comprehensive test coverage for MessageStore, CLI commands, and UI components - Update documentation (CLI STANDALONE.md, dashboard README) with messaging usage
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { MailboxModal } from "../MailboxModal";
|
||||
import * as apiModule from "../../api";
|
||||
import type { Agent } from "../../api";
|
||||
import type { Message } from "@fusion/core";
|
||||
|
||||
// Mock the API module
|
||||
vi.mock("../../api", () => ({
|
||||
fetchInbox: vi.fn(),
|
||||
fetchOutbox: vi.fn(),
|
||||
fetchUnreadCount: vi.fn(),
|
||||
fetchAgentMailbox: vi.fn(),
|
||||
markMessageRead: vi.fn(),
|
||||
markAllMessagesRead: vi.fn(),
|
||||
deleteMessage: vi.fn(),
|
||||
fetchConversation: vi.fn(),
|
||||
sendMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock lucide-react icons
|
||||
vi.mock("lucide-react", () => ({
|
||||
X: () => <span data-testid="icon-x">X</span>,
|
||||
Mail: () => <span data-testid="icon-mail">Mail</span>,
|
||||
Send: () => <span data-testid="icon-send">Send</span>,
|
||||
Inbox: () => <span data-testid="icon-inbox">Inbox</span>,
|
||||
Bot: () => <span data-testid="icon-bot">Bot</span>,
|
||||
Trash2: () => <span data-testid="icon-trash">Trash</span>,
|
||||
Check: () => <span data-testid="icon-check">Check</span>,
|
||||
CheckCheck: () => <span data-testid="icon-checkcheck">CheckCheck</span>,
|
||||
Loader2: ({ className }: { className?: string }) => (
|
||||
<span data-testid="icon-loader" className={className}>Loader</span>
|
||||
),
|
||||
RefreshCw: () => <span data-testid="icon-refresh">Refresh</span>,
|
||||
MessageSquare: () => <span data-testid="icon-message">Message</span>,
|
||||
User: () => <span data-testid="icon-user">User</span>,
|
||||
AlertCircle: () => <span data-testid="icon-alert">Alert</span>,
|
||||
}));
|
||||
|
||||
const mockFetchInbox = vi.mocked(apiModule.fetchInbox);
|
||||
const mockFetchOutbox = vi.mocked(apiModule.fetchOutbox);
|
||||
const mockFetchUnreadCount = vi.mocked(apiModule.fetchUnreadCount);
|
||||
const mockFetchAgentMailbox = vi.mocked(apiModule.fetchAgentMailbox);
|
||||
const mockMarkMessageRead = vi.mocked(apiModule.markMessageRead);
|
||||
const mockMarkAllMessagesRead = vi.mocked(apiModule.markAllMessagesRead);
|
||||
const mockDeleteMessage = vi.mocked(apiModule.deleteMessage);
|
||||
const mockFetchConversation = vi.mocked(apiModule.fetchConversation);
|
||||
|
||||
const mockAgents: Agent[] = [
|
||||
{
|
||||
id: "agent-001",
|
||||
name: "Test Agent 1",
|
||||
role: "executor",
|
||||
state: "idle",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
metadata: {},
|
||||
},
|
||||
{
|
||||
id: "agent-002",
|
||||
name: "Test Agent 2",
|
||||
role: "triage",
|
||||
state: "active",
|
||||
taskId: "FN-001",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
metadata: {},
|
||||
},
|
||||
];
|
||||
|
||||
const mockMessage: Message = {
|
||||
id: "msg-001",
|
||||
fromId: "agent-001",
|
||||
fromType: "agent",
|
||||
toId: "dashboard",
|
||||
toType: "user",
|
||||
content: "Hello, this is a test message from the agent.",
|
||||
type: "agent-to-user",
|
||||
read: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const mockReadMessage: Message = {
|
||||
...mockMessage,
|
||||
id: "msg-002",
|
||||
read: true,
|
||||
content: "This message has been read already.",
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
isOpen: true,
|
||||
onClose: vi.fn(),
|
||||
addToast: vi.fn(),
|
||||
agents: mockAgents,
|
||||
};
|
||||
|
||||
describe("MailboxModal", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockFetchInbox.mockResolvedValue({ messages: [mockMessage, mockReadMessage], total: 2, unreadCount: 1 });
|
||||
mockFetchOutbox.mockResolvedValue({ messages: [], total: 0 });
|
||||
mockFetchUnreadCount.mockResolvedValue({ unreadCount: 1 });
|
||||
mockFetchConversation.mockResolvedValue([mockMessage]);
|
||||
mockMarkMessageRead.mockResolvedValue({ ...mockMessage, read: true });
|
||||
mockMarkAllMessagesRead.mockResolvedValue({ markedAsRead: 1 });
|
||||
mockDeleteMessage.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("renders nothing when isOpen is false", () => {
|
||||
render(<MailboxModal {...defaultProps} isOpen={false} />);
|
||||
expect(screen.queryByTestId("mailbox-modal")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders the modal when isOpen is true", () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
expect(screen.getByTestId("mailbox-modal")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows the Mailbox title with unread count badge", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
expect(screen.getByText("Mailbox")).toBeDefined();
|
||||
// Wait for inbox to load which sets unreadCount
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-unread-badge")).toBeDefined();
|
||||
});
|
||||
expect(screen.getByTestId("mailbox-unread-badge").textContent).toBe("1");
|
||||
});
|
||||
|
||||
it("renders all three tabs", () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
expect(screen.getByTestId("mailbox-tab-inbox")).toBeDefined();
|
||||
expect(screen.getByTestId("mailbox-tab-outbox")).toBeDefined();
|
||||
expect(screen.getByTestId("mailbox-tab-agents")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows inbox tab as active by default", () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
const inboxTab = screen.getByTestId("mailbox-tab-inbox");
|
||||
expect(inboxTab.classList.contains("active")).toBe(true);
|
||||
});
|
||||
|
||||
it("loads inbox on mount", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(mockFetchInbox).toHaveBeenCalledWith({ limit: 50 }, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows inbox messages after loading", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-inbox-list")).toBeDefined();
|
||||
});
|
||||
// Should show both messages
|
||||
expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
|
||||
expect(screen.getByTestId("mailbox-item-msg-002")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows unread dot for unread messages", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-unread-dot-msg-001")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not show unread dot for read messages", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-item-msg-002")).toBeDefined();
|
||||
});
|
||||
expect(screen.queryByTestId("mailbox-unread-dot-msg-002")).toBeNull();
|
||||
});
|
||||
|
||||
it("switches to outbox tab on click", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
const outboxTab = screen.getByTestId("mailbox-tab-outbox");
|
||||
fireEvent.click(outboxTab);
|
||||
await waitFor(() => {
|
||||
expect(mockFetchOutbox).toHaveBeenCalledWith({ limit: 50 }, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows empty state for empty outbox", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
fireEvent.click(screen.getByTestId("mailbox-tab-outbox"));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-outbox-empty")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("switches to agents tab on click", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
fireEvent.click(screen.getByTestId("mailbox-tab-agents"));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-agents")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows agent buttons in agents tab", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
fireEvent.click(screen.getByTestId("mailbox-tab-agents"));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-agent-btn-agent-001")).toBeDefined();
|
||||
expect(screen.getByTestId("mailbox-agent-btn-agent-002")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("loads agent mailbox when agent is selected", async () => {
|
||||
mockFetchAgentMailbox.mockResolvedValue({
|
||||
ownerId: "agent-001",
|
||||
ownerType: "agent",
|
||||
unreadCount: 0,
|
||||
messages: [],
|
||||
});
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
fireEvent.click(screen.getByTestId("mailbox-tab-agents"));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-agent-btn-agent-001")).toBeDefined();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("mailbox-agent-btn-agent-001"));
|
||||
await waitFor(() => {
|
||||
expect(mockFetchAgentMailbox).toHaveBeenCalledWith("agent-001", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it("opens message detail when clicking a message", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("mailbox-item-msg-001"));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-message-detail")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("marks message as read when opening unread message", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("mailbox-item-msg-001"));
|
||||
await waitFor(() => {
|
||||
expect(mockMarkMessageRead).toHaveBeenCalledWith("msg-001", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows back button in message detail", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("mailbox-item-msg-001"));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-back-to-list")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("returns to list when clicking back button", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("mailbox-item-msg-001"));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-back-to-list")).toBeDefined();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("mailbox-back-to-list"));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("mailbox-message-detail")).toBeNull();
|
||||
expect(screen.getByTestId("mailbox-inbox-list")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows mark all read button when there are unread messages", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-mark-all-read")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("calls markAllMessagesRead when clicking mark all read", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-mark-all-read")).toBeDefined();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("mailbox-mark-all-read"));
|
||||
await waitFor(() => {
|
||||
expect(mockMarkAllMessagesRead).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it("deletes message when clicking delete in detail view", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("mailbox-item-msg-001"));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-delete")).toBeDefined();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("mailbox-delete"));
|
||||
await waitFor(() => {
|
||||
expect(mockDeleteMessage).toHaveBeenCalledWith("msg-001", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows compose FAB in inbox tab", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-compose-fab")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not show compose FAB in agents tab", async () => {
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
fireEvent.click(screen.getByTestId("mailbox-tab-agents"));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("mailbox-compose-fab")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows loading skeleton while loading", async () => {
|
||||
mockFetchInbox.mockImplementation(() => new Promise(() => {})); // Never resolves
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-skeleton")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows empty inbox state when no messages", async () => {
|
||||
mockFetchInbox.mockResolvedValue({ messages: [], total: 0, unreadCount: 0 });
|
||||
render(<MailboxModal {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-inbox-empty")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("calls onClose when clicking close button", async () => {
|
||||
const onClose = vi.fn();
|
||||
render(<MailboxModal {...defaultProps} onClose={onClose} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-close")).toBeDefined();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("mailbox-close"));
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("passes projectId to API calls", async () => {
|
||||
render(<MailboxModal {...defaultProps} projectId="proj-1" />);
|
||||
await waitFor(() => {
|
||||
expect(mockFetchInbox).toHaveBeenCalledWith({ limit: 50 }, "proj-1");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,204 @@
|
||||
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: () => <span data-testid="icon-x">X</span>,
|
||||
Send: () => <span data-testid="icon-send">Send</span>,
|
||||
Loader2: ({ className }: { className?: string }) => (
|
||||
<span data-testid="icon-loader" className={className}>Loader</span>
|
||||
),
|
||||
Bot: () => <span data-testid="icon-bot">Bot</span>,
|
||||
AlertCircle: () => <span data-testid="icon-alert">Alert</span>,
|
||||
}));
|
||||
|
||||
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(<MessageComposer {...defaultProps} />);
|
||||
expect(screen.getByText("New Message")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows agent dropdown when agents are provided", () => {
|
||||
render(<MessageComposer {...defaultProps} agents={mockAgents} />);
|
||||
const select = screen.getByTestId("message-composer-recipient");
|
||||
expect(select).toBeDefined();
|
||||
expect(select.tagName).toBe("SELECT");
|
||||
});
|
||||
|
||||
it("shows text input when no agents provided", () => {
|
||||
render(<MessageComposer {...defaultProps} />);
|
||||
const input = screen.getByTestId("message-composer-recipient");
|
||||
expect(input.tagName).toBe("INPUT");
|
||||
});
|
||||
|
||||
it("disables send button when content is empty", () => {
|
||||
render(<MessageComposer {...defaultProps} agents={mockAgents} />);
|
||||
const sendBtn = screen.getByTestId("message-composer-send");
|
||||
expect(sendBtn.hasAttribute("disabled")).toBe(true);
|
||||
});
|
||||
|
||||
it("enables send button when recipient and content are filled", () => {
|
||||
render(<MessageComposer {...defaultProps} agents={mockAgents} />);
|
||||
// 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(<MessageComposer {...defaultProps} />);
|
||||
expect(screen.getByTestId("message-composer-charcount")).toBeDefined();
|
||||
expect(screen.getByTestId("message-composer-charcount").textContent).toContain("0/2000");
|
||||
});
|
||||
|
||||
it("updates character count when typing", () => {
|
||||
render(<MessageComposer {...defaultProps} />);
|
||||
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(<MessageComposer {...defaultProps} agents={mockAgents} />);
|
||||
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(<MessageComposer {...defaultProps} agents={mockAgents} />);
|
||||
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(<MessageComposer {...defaultProps} />);
|
||||
fireEvent.click(screen.getByTestId("message-composer-cancel"));
|
||||
expect(defaultProps.onCancel).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("calls onCancel when clicking cancel footer button", () => {
|
||||
render(<MessageComposer {...defaultProps} />);
|
||||
fireEvent.click(screen.getByTestId("message-composer-cancel-btn"));
|
||||
expect(defaultProps.onCancel).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("pre-fills recipient when provided", () => {
|
||||
render(
|
||||
<MessageComposer
|
||||
{...defaultProps}
|
||||
recipient={{ id: "agent-001", type: "agent" }}
|
||||
/>,
|
||||
);
|
||||
// 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(<MessageComposer {...defaultProps} agents={mockAgents} />);
|
||||
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(<MessageComposer {...defaultProps} agents={mockAgents} projectId="proj-1" />);
|
||||
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",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user