import { fireEvent, render, screen, within } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { ChatList } from "@/components/ChatList"; import type { ChatSummary } from "@/lib/types"; function session(overrides: Partial): ChatSummary { const chatId = overrides.chatId ?? "chat"; return { key: `websocket:${chatId}`, channel: "websocket", chatId, createdAt: "2026-05-20T10:00:00Z", updatedAt: "2026-05-20T10:00:00Z", preview: "", ...overrides, }; } describe("ChatList", () => { it("groups WebUI chats by workspace project while preserving in-project sorting and activity", () => { const sessions = [ session({ chatId: "zeta", title: "Zeta task", updatedAt: "2026-05-20T12:00:00Z", workspaceScope: { project_path: "/Users/me/nanobot", project_name: "nanobot", access_mode: "restricted", }, }), session({ chatId: "alpha", title: "Alpha task", updatedAt: "2026-05-20T11:00:00Z", workspaceScope: { project_path: "/Users/me/nanobot", project_name: "nanobot", access_mode: "restricted", }, }), session({ chatId: "bench", title: "Bench task", updatedAt: "2026-05-21T09:00:00Z", workspaceScope: { project_path: "/Users/me/nanobot-bench", project_name: "nanobot-bench", access_mode: "full", }, }), ]; render( , ); const nanobotSection = screen.getByRole("region", { name: "nanobot" }); const nanobotText = nanobotSection.textContent ?? ""; expect(screen.getByRole("region", { name: "nanobot-bench" })).toBeInTheDocument(); expect(within(nanobotSection).getByText("Alpha task")).toBeInTheDocument(); expect(within(nanobotSection).getByText("Zeta task")).toBeInTheDocument(); expect(nanobotText.indexOf("Alpha task")).toBeLessThan(nanobotText.indexOf("Zeta task")); expect(within(nanobotSection).getByLabelText("Agent running")).toBeInTheDocument(); expect(screen.queryByText("Today")).not.toBeInTheDocument(); }); it("keeps default workspace chats in the Chats section instead of a project folder", () => { const sessions = [ session({ chatId: "default", title: "Default workspace chat", updatedAt: "2026-05-21T10:00:00Z", workspaceScope: { project_path: "/Users/me/.nanobot/workspace", project_name: "workspace", access_mode: "restricted", }, }), session({ chatId: "project", title: "Project chat", updatedAt: "2026-05-21T11:00:00Z", workspaceScope: { project_path: "/Users/me/nanobot", project_name: "nanobot", access_mode: "restricted", }, }), ]; render( , ); expect(screen.getByText("Projects")).toBeInTheDocument(); expect(screen.getByRole("region", { name: "nanobot" })).toBeInTheDocument(); expect(screen.queryByRole("region", { name: "workspace" })).not.toBeInTheDocument(); const chatsSection = screen.getByRole("region", { name: "Chats" }); expect(within(chatsSection).getByText("Default workspace chat")).toBeInTheDocument(); expect(within(chatsSection).queryByText("Project chat")).not.toBeInTheDocument(); }); it("can collapse a project group and keeps project rename separate from chat titles", async () => { const onToggleGroup = vi.fn(); const onRequestRenameProject = vi.fn(); const onNewChatInProject = vi.fn(); const sessions = [ session({ chatId: "alpha", title: "Alpha task", workspaceScope: { project_path: "/Users/me/nanobot", project_name: "nanobot", access_mode: "restricted", }, }), ]; render( , ); const projectSection = screen.getByRole("region", { name: "Photos" }); fireEvent.click(within(projectSection).getByRole("button", { name: "Photos" })); expect(onToggleGroup).toHaveBeenCalledWith("project:/Users/me/nanobot"); expect(within(projectSection).queryByText("Alpha task")).not.toBeInTheDocument(); fireEvent.click( within(projectSection).getByRole("button", { name: "Start a new chat in Photos" }), ); expect(onNewChatInProject).toHaveBeenCalledWith("/Users/me/nanobot", "Photos"); expect(onToggleGroup).toHaveBeenCalledTimes(1); fireEvent.pointerDown( within(projectSection).getByLabelText("Chat actions for Photos"), { button: 0 }, ); fireEvent.click(await screen.findByRole("menuitem", { name: "Rename" })); expect(onRequestRenameProject).toHaveBeenCalledWith("/Users/me/nanobot", "Photos"); }); it("hides the completed dot for the active chat", () => { const sessions = [ session({ chatId: "active", title: "Active task", }), session({ chatId: "done", title: "Done task", }), ]; render( , ); expect(screen.getAllByLabelText("Agent finished")).toHaveLength(1); }); it("folds long default workspace chats and can show all", () => { const sessions = Array.from({ length: 10 }, (_, index) => session({ chatId: `chat-${index}`, title: `Chat ${index}`, updatedAt: `2026-05-21T10:${String(index).padStart(2, "0")}:00Z`, workspaceScope: { project_path: "/Users/me/.nanobot/workspace", project_name: "workspace", access_mode: "restricted", }, }), ); const onToggleGroup = vi.fn(); const baseProps = { sessions, activeKey: null, onSelect: vi.fn(), onRequestDelete: vi.fn(), onTogglePin: vi.fn(), onRequestRename: vi.fn(), onToggleArchive: vi.fn(), onToggleGroup, defaultWorkspacePath: "/Users/me/.nanobot/workspace", }; const { rerender } = render(); const chatsSection = screen.getByRole("region", { name: "Chats" }); expect(within(chatsSection).getByText("Chat 9")).toBeInTheDocument(); expect(within(chatsSection).getByText("Chat 2")).toBeInTheDocument(); expect(within(chatsSection).queryByText("Chat 1")).not.toBeInTheDocument(); expect(within(chatsSection).queryByRole("button", { name: "Show all" })).not.toBeInTheDocument(); fireEvent.click(within(chatsSection).getByRole("button", { name: "2 hidden chats" })); expect(onToggleGroup).toHaveBeenCalledWith("workspace:chats"); rerender( , ); expect(within(chatsSection).getByText("Chat 0")).toBeInTheDocument(); expect(within(chatsSection).getByRole("button", { name: "Show less" })).toBeInTheDocument(); }); });