import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ChatSummary } from "@/lib/types"; const connectSpy = vi.fn(); const refreshSpy = vi.fn(); const createChatSpy = vi.fn().mockResolvedValue("chat-1"); const deleteChatSpy = vi.fn(); let mockSessions: ChatSummary[] = []; vi.mock("@/hooks/useSessions", async (importOriginal) => { const React = await import("react"); const actual = await importOriginal(); return { ...actual, useSessions: () => { const [sessions, setSessions] = React.useState(mockSessions); return { sessions, loading: false, error: null, refresh: refreshSpy, createChat: createChatSpy, deleteChat: async (key: string) => { await deleteChatSpy(key); setSessions((prev: ChatSummary[]) => prev.filter((s) => s.key !== key)); }, }; }, }; }); vi.mock("@/hooks/useTheme", () => ({ useTheme: () => ({ theme: "light" as const, toggle: vi.fn(), }), })); vi.mock("@/lib/bootstrap", () => ({ fetchBootstrap: vi.fn().mockResolvedValue({ token: "tok", ws_path: "/", expires_in: 300, }), deriveWsUrl: vi.fn(() => "ws://test"), })); vi.mock("@/lib/nanobot-client", () => { class MockClient { status = "idle" as const; defaultChatId: string | null = null; connect = connectSpy; onStatus = () => () => {}; onError = () => () => {}; onChat = () => () => {}; sendMessage = vi.fn(); newChat = vi.fn(); attach = vi.fn(); close = vi.fn(); updateUrl = vi.fn(); } return { NanobotClient: MockClient }; }); import App from "@/App"; describe("App layout", () => { beforeEach(() => { mockSessions = []; connectSpy.mockClear(); refreshSpy.mockReset(); createChatSpy.mockClear(); deleteChatSpy.mockReset(); vi.stubGlobal( "fetch", vi.fn().mockResolvedValue({ ok: false, status: 404, }), ); }); it("keeps sidebar layout out of the main thread width contract", async () => { const { container } = render(); await waitFor(() => expect(connectSpy).toHaveBeenCalled()); const main = container.querySelector("main"); expect(main).toBeInTheDocument(); expect(main).not.toHaveAttribute("style"); const asideClassNames = Array.from(container.querySelectorAll("aside")).map( (el) => el.className, ); expect(asideClassNames.some((cls) => cls.includes("lg:block"))).toBe(true); }); it("switches to the next session when deleting the active chat", async () => { mockSessions = [ { key: "websocket:chat-a", channel: "websocket", chatId: "chat-a", createdAt: "2026-04-16T10:00:00Z", updatedAt: "2026-04-16T10:00:00Z", preview: "First chat", }, { key: "websocket:chat-b", channel: "websocket", chatId: "chat-b", createdAt: "2026-04-16T11:00:00Z", updatedAt: "2026-04-16T11:00:00Z", preview: "Second chat", }, ]; render(); await waitFor(() => expect(connectSpy).toHaveBeenCalled()); await waitFor(() => expect(screen.getByRole("button", { name: /^First chat$/ })).toBeInTheDocument(), ); fireEvent.pointerDown(screen.getByLabelText("Chat actions for First chat"), { button: 0, }); fireEvent.click(await screen.findByRole("menuitem", { name: "Delete" })); await waitFor(() => expect(screen.getByText('Delete “First chat”?')).toBeInTheDocument(), ); fireEvent.click(screen.getByRole("button", { name: "Delete" })); await waitFor(() => expect(deleteChatSpy).toHaveBeenCalledWith("websocket:chat-a"), ); await waitFor(() => expect( screen.getByRole("button", { name: /^Second chat$/ }), ).toBeInTheDocument(), ); expect(screen.queryByText('Delete “First chat”?')).not.toBeInTheDocument(); expect(document.body.style.pointerEvents).not.toBe("none"); }, 15_000); it("opens the Cursor-style settings view from the sidebar", async () => { vi.stubGlobal( "fetch", vi.fn(async (input: RequestInfo | URL) => { if (String(input).includes("/api/settings")) { return { ok: true, status: 200, json: async () => ({ agent: { model: "openai/gpt-4o", provider: "auto", resolved_provider: "openai", has_api_key: true, }, providers: [ { name: "auto", label: "Auto" }, { name: "openai", label: "OpenAI" }, ], runtime: { config_path: "/tmp/config.json", }, requires_restart: false, }), }; } return { ok: false, status: 404, json: async () => ({}) }; }), ); render(); await waitFor(() => expect(connectSpy).toHaveBeenCalled()); fireEvent.click(screen.getByRole("button", { name: "Settings" })); expect(await screen.findByRole("heading", { name: "General" })).toBeInTheDocument(); expect(screen.getByText("AI")).toBeInTheDocument(); expect(screen.getByDisplayValue("openai/gpt-4o")).toBeInTheDocument(); }); });