Files
fusion/packages/dashboard/app/components/__tests__/ChatView.rooms.test.tsx
Fusion 2278ceff31 feat(FN-3879): add chat room management in ChatView
- Add useChatRooms hook with room lifecycle state and API integration
- Wire ChatView UI for room selection/creation and update room-focused tests
- Tokenize ChatView and TaskCard color usage to align with dashboard design tokens
- Update architecture docs and include FN-3879 changesets for published CLI package

Fusion-Task-Id: FN-3879
2026-05-09 13:21:47 -07:00

136 lines
7.2 KiB
TypeScript

import { render, screen, waitFor } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { userEvent } from "@testing-library/user-event";
import { ChatView } from "../ChatView";
import * as useChatModule from "../../hooks/useChat";
import * as useChatRoomsModule from "../../hooks/useChatRooms";
import * as headerModule from "../Header";
import type { UseChatRoomsResult } from "../../hooks/useChatRooms";
vi.mock("../../hooks/useChat", () => ({ useChat: vi.fn() }));
vi.mock("../../hooks/useChatRooms", () => ({ useChatRooms: vi.fn() }));
vi.mock("../Header", () => ({ useViewportMode: vi.fn() }));
vi.mock("../../api", () => ({
fetchModels: vi.fn().mockResolvedValue({ models: [], favoriteProviders: [], favoriteModels: [] }),
fetchAgents: vi.fn().mockResolvedValue([{ id: "agent-1", name: "Alpha", role: "executor", state: "idle", metadata: {}, createdAt: "", updatedAt: "" }]),
fetchDiscoveredSkills: vi.fn().mockResolvedValue([]),
updateGlobalSettings: vi.fn(),
searchFiles: vi.fn().mockResolvedValue({ files: [] }),
}));
const mockUseChat = vi.mocked(useChatModule.useChat);
const mockUseChatRooms = vi.mocked(useChatRoomsModule.useChatRooms);
const mockUseViewportMode = vi.mocked(headerModule.useViewportMode);
function buildRoomsMock(overrides: Partial<UseChatRoomsResult> = {}): UseChatRoomsResult {
return {
rooms: [],
roomsLoading: false,
roomsError: null,
activeRoom: null,
activeRoomMembers: [],
messages: [],
messagesLoading: false,
selectRoom: vi.fn(),
createRoom: vi.fn().mockResolvedValue({ id: "room-2", name: "product", slug: "product", description: null, projectId: "proj-1", createdBy: null, status: "active", createdAt: "", updatedAt: "" }),
deleteRoom: vi.fn().mockResolvedValue(undefined),
sendRoomMessage: vi.fn().mockResolvedValue(undefined),
refreshRooms: vi.fn().mockResolvedValue(undefined),
...overrides,
};
}
beforeEach(() => {
vi.clearAllMocks();
mockUseViewportMode.mockReturnValue("desktop");
mockUseChat.mockReturnValue({
sessions: [], activeSession: null, sessionsLoading: false, messages: [], messagesLoading: false,
isStreaming: false, streamingText: "", streamingThinking: "", streamingToolCalls: [],
selectSession: vi.fn(), createSession: vi.fn(), archiveSession: vi.fn(), deleteSession: vi.fn(),
sendMessage: vi.fn(), stopStreaming: vi.fn(), pendingMessage: "", clearPendingMessage: vi.fn(),
loadMoreMessages: vi.fn(), hasMoreMessages: false, searchQuery: "", setSearchQuery: vi.fn(),
filteredSessions: [], refreshSessions: vi.fn(), agentsMap: new Map(),
} as any);
});
describe("ChatView rooms", () => {
it("lists rooms and selects one", async () => {
const roomsMock = buildRoomsMock({
rooms: [{ id: "room-1", name: "engineering", slug: "engineering", description: null, projectId: "proj-1", createdBy: null, status: "active", createdAt: "", updatedAt: "2026-05-09T00:00:00.000Z" }],
activeRoom: { id: "room-1", name: "engineering", slug: "engineering", description: null, projectId: "proj-1", createdBy: null, status: "active", createdAt: "", updatedAt: "2026-05-09T00:00:00.000Z" },
messages: [{ id: "msg-1", roomId: "room-1", role: "user", content: "hello", thinkingOutput: null, metadata: null, senderAgentId: null, mentions: [], createdAt: "2026-05-09T00:00:00.000Z" }],
});
mockUseChatRooms.mockReturnValue(roomsMock);
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
await userEvent.click(screen.getByTestId("chat-room-item-engineering"));
expect(roomsMock.selectRoom).toHaveBeenCalledWith("room-1");
expect(screen.getByTestId("chat-room-item-engineering")).toBeInTheDocument();
expect(screen.getByText("hello")).toBeInTheDocument();
});
it("create room modal submits via rooms hook", async () => {
const roomsMock = buildRoomsMock({ rooms: [] });
mockUseChatRooms.mockReturnValue(roomsMock);
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
await userEvent.click(screen.getByTestId("chat-create-room-btn"));
await userEvent.type(screen.getByLabelText("Room name"), "product");
await userEvent.click(await screen.findByRole("button", { name: /Alpha/i }));
await userEvent.click(screen.getByRole("dialog", { name: "Create room" }).querySelector(".btn.btn-primary") as HTMLButtonElement);
await waitFor(() => {
expect(roomsMock.createRoom).toHaveBeenCalledWith({ name: "product", memberAgentIds: ["agent-1"] });
});
});
it("mobile selection hides sidebar and supports back button", async () => {
mockUseViewportMode.mockReturnValue("mobile");
const roomsMock = buildRoomsMock({
rooms: [{ id: "room-1", name: "engineering", slug: "engineering", description: null, projectId: "proj-1", createdBy: null, status: "active", createdAt: "", updatedAt: "2026-05-09T00:00:00.000Z" }],
activeRoom: { id: "room-1", name: "engineering", slug: "engineering", description: null, projectId: "proj-1", createdBy: null, status: "active", createdAt: "", updatedAt: "2026-05-09T00:00:00.000Z" },
});
mockUseChatRooms.mockReturnValue(roomsMock);
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
await userEvent.click(screen.getByTestId("chat-room-item-engineering"));
await waitFor(() => expect(screen.getByTestId("chat-back-btn")).toBeInTheDocument());
});
it("sending room message calls hook and clears input", async () => {
const roomsMock = buildRoomsMock({
activeRoom: { id: "room-1", name: "engineering", slug: "engineering", description: null, projectId: "proj-1", createdBy: null, status: "active", createdAt: "", updatedAt: "2026-05-09T00:00:00.000Z" },
});
mockUseChatRooms.mockReturnValue(roomsMock);
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
await userEvent.type(screen.getByTestId("chat-input"), "hello room");
await userEvent.click(screen.getByTestId("chat-send-btn"));
await waitFor(() => expect(roomsMock.sendRoomMessage).toHaveBeenCalledWith("hello room"));
await waitFor(() => expect(screen.getByTestId("chat-input")).toHaveValue(""));
});
it("renders newly appended messages from hook updates", async () => {
const state = buildRoomsMock({
activeRoom: { id: "room-1", name: "engineering", slug: "engineering", description: null, projectId: "proj-1", createdBy: null, status: "active", createdAt: "", updatedAt: "2026-05-09T00:00:00.000Z" },
messages: [],
});
mockUseChatRooms.mockImplementation(() => state);
const { rerender } = render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
state.messages = [{ id: "msg-2", roomId: "room-1", role: "assistant", content: "reply", thinkingOutput: null, metadata: null, senderAgentId: "agent-1", mentions: [], createdAt: "2026-05-09T00:00:00.000Z" }];
rerender(<ChatView addToast={vi.fn()} projectId="proj-1" />);
expect(screen.getByText("reply")).toBeInTheDocument();
});
});