import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { api } from "./api/client"; vi.mock("./api/client", () => { const api = { getCurrentProject: vi.fn(), getKnownProjects: vi.fn(), getHomeDirectory: vi.fn(), openProject: vi.fn(), closeProject: vi.fn(), forgetKnownProject: vi.fn(), listDirectoryAbsolute: vi.fn(), getOllamaModels: vi.fn(), getAnthropicApiKeyExists: vi.fn(), getAnthropicModels: vi.fn(), getModelPreference: vi.fn(), setModelPreference: vi.fn(), cancelChat: vi.fn(), setAnthropicApiKey: vi.fn(), }; class ChatWebSocket { connect() {} close() {} sendChat() {} cancel() {} } return { api, ChatWebSocket }; }); vi.mock("./api/workflow", () => { return { workflowApi: { getAcceptance: vi.fn().mockResolvedValue({ can_accept: false, reasons: [], warning: null, summary: { total: 0, passed: 0, failed: 0 }, missing_categories: [], }), getReviewQueueAll: vi.fn().mockResolvedValue({ stories: [] }), getUpcomingStories: vi.fn().mockResolvedValue({ stories: [] }), recordTests: vi.fn(), ensureAcceptance: vi.fn(), getReviewQueue: vi.fn(), collectCoverage: vi.fn(), recordCoverage: vi.fn(), getStoryTodos: vi.fn().mockResolvedValue({ stories: [] }), }, }; }); const mockedApi = vi.mocked(api); describe("App", () => { beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); mockedApi.getCurrentProject.mockResolvedValue(null); mockedApi.getKnownProjects.mockResolvedValue([]); mockedApi.getHomeDirectory.mockResolvedValue("/home/user"); mockedApi.listDirectoryAbsolute.mockResolvedValue([]); mockedApi.getOllamaModels.mockResolvedValue([]); mockedApi.getAnthropicApiKeyExists.mockResolvedValue(false); mockedApi.getAnthropicModels.mockResolvedValue([]); mockedApi.getModelPreference.mockResolvedValue(null); }); async function renderApp() { const { default: App } = await import("./App"); return render(); } it("calls getCurrentProject() on mount", async () => { await renderApp(); await waitFor(() => { expect(mockedApi.getCurrentProject).toHaveBeenCalledTimes(1); }); }); it("skips selection screen and shows workspace when server already has a project open", async () => { mockedApi.getCurrentProject.mockResolvedValue("/home/user/myproject"); await renderApp(); await waitFor(() => { expect( screen.queryByPlaceholderText(/\/path\/to\/project/i), ).not.toBeInTheDocument(); }); }); it("renders the selection screen when no project is open", async () => { await renderApp(); await waitFor(() => { expect( screen.getByPlaceholderText(/\/path\/to\/project/i), ).toBeInTheDocument(); }); }); it("populates path input with home directory", async () => { mockedApi.getHomeDirectory.mockResolvedValue("/Users/dave"); await renderApp(); await waitFor(() => { const input = screen.getByPlaceholderText( /\/path\/to\/project/i, ) as HTMLInputElement; expect(input.value).toBe("/Users/dave/"); }); }); it("opens project and shows chat view", async () => { mockedApi.openProject.mockResolvedValue("/home/user/myproject"); await renderApp(); await waitFor(() => { expect( screen.getByPlaceholderText(/\/path\/to\/project/i), ).toBeInTheDocument(); }); const input = screen.getByPlaceholderText( /\/path\/to\/project/i, ) as HTMLInputElement; await userEvent.clear(input); await userEvent.type(input, "/home/user/myproject"); const openButton = screen.getByRole("button", { name: /open project/i }); await userEvent.click(openButton); await waitFor(() => { expect(mockedApi.openProject).toHaveBeenCalledWith( "/home/user/myproject", ); }); }); it("shows error when openProject fails", async () => { mockedApi.openProject.mockRejectedValue(new Error("Path does not exist")); await renderApp(); await waitFor(() => { expect( screen.getByPlaceholderText(/\/path\/to\/project/i), ).toBeInTheDocument(); }); const input = screen.getByPlaceholderText( /\/path\/to\/project/i, ) as HTMLInputElement; await userEvent.clear(input); await userEvent.type(input, "/bad/path"); const openButton = screen.getByRole("button", { name: /open project/i }); await userEvent.click(openButton); await waitFor(() => { expect(screen.getByText(/Path does not exist/)).toBeInTheDocument(); }); }); it("shows known projects list", async () => { mockedApi.getKnownProjects.mockResolvedValue([ "/home/user/project1", "/home/user/project2", ]); await renderApp(); await waitFor(() => { expect(screen.getByTitle("/home/user/project1")).toBeInTheDocument(); expect(screen.getByTitle("/home/user/project2")).toBeInTheDocument(); }); }); it("shows error when path input is empty", async () => { await renderApp(); await waitFor(() => { expect( screen.getByPlaceholderText(/\/path\/to\/project/i), ).toBeInTheDocument(); }); const input = screen.getByPlaceholderText( /\/path\/to\/project/i, ) as HTMLInputElement; await userEvent.clear(input); const openButton = screen.getByRole("button", { name: /open project/i }); await userEvent.click(openButton); await waitFor(() => { expect( screen.getByText(/Please enter a project path/i), ).toBeInTheDocument(); }); }); it("calls forgetKnownProject and removes project from list", async () => { mockedApi.getKnownProjects.mockResolvedValue(["/home/user/project1"]); mockedApi.forgetKnownProject.mockResolvedValue(true); await renderApp(); await waitFor(() => { expect(screen.getByTitle("/home/user/project1")).toBeInTheDocument(); }); const forgetButton = screen.getByRole("button", { name: /Forget project1/i, }); await userEvent.click(forgetButton); await waitFor(() => { expect(mockedApi.forgetKnownProject).toHaveBeenCalledWith( "/home/user/project1", ); expect( screen.queryByTitle("/home/user/project1"), ).not.toBeInTheDocument(); }); }); it("closes project and returns to selection screen", async () => { mockedApi.openProject.mockResolvedValue("/home/user/myproject"); mockedApi.closeProject.mockResolvedValue(true); await renderApp(); await waitFor(() => { expect( screen.getByPlaceholderText(/\/path\/to\/project/i), ).toBeInTheDocument(); }); const input = screen.getByPlaceholderText( /\/path\/to\/project/i, ) as HTMLInputElement; await userEvent.clear(input); await userEvent.type(input, "/home/user/myproject"); const openButton = screen.getByRole("button", { name: /open project/i }); await userEvent.click(openButton); await waitFor(() => { expect(mockedApi.openProject).toHaveBeenCalledWith( "/home/user/myproject", ); }); // Chat view should appear with close button const closeButton = await waitFor(() => screen.getByText("✕")); await userEvent.click(closeButton); await waitFor(() => { expect(mockedApi.closeProject).toHaveBeenCalled(); expect( screen.getByPlaceholderText(/\/path\/to\/project/i), ).toBeInTheDocument(); }); }); it("handles ArrowDown and ArrowUp keyboard navigation when suggestions are visible", async () => { mockedApi.listDirectoryAbsolute.mockResolvedValue([ { name: "projects", kind: "dir" }, { name: "documents", kind: "dir" }, ]); await renderApp(); // Wait for suggestions to appear after debounce await waitFor( () => { expect(screen.getByText(/projects\//)).toBeInTheDocument(); }, { timeout: 2000 }, ); const input = screen.getByPlaceholderText(/\/path\/to\/project/i); // ArrowDown with matchList present — moves selection forward fireEvent.keyDown(input, { key: "ArrowDown" }); // ArrowUp with matchList present — moves selection backward fireEvent.keyDown(input, { key: "ArrowUp" }); }); it("handles Tab keyboard navigation to accept suggestion", async () => { mockedApi.listDirectoryAbsolute.mockResolvedValue([ { name: "myrepo", kind: "dir" }, ]); await renderApp(); await waitFor( () => { expect(screen.getByText(/myrepo\//)).toBeInTheDocument(); }, { timeout: 2000 }, ); const input = screen.getByPlaceholderText(/\/path\/to\/project/i); // Tab with matchList present — accepts the selected match fireEvent.keyDown(input, { key: "Tab" }); }); it("handles Escape key to close suggestions", async () => { mockedApi.listDirectoryAbsolute.mockResolvedValue([ { name: "workspace", kind: "dir" }, ]); await renderApp(); await waitFor( () => { expect(screen.getByText(/workspace\//)).toBeInTheDocument(); }, { timeout: 2000 }, ); const input = screen.getByPlaceholderText(/\/path\/to\/project/i); // Escape closes suggestions fireEvent.keyDown(input, { key: "Escape" }); await waitFor(() => { expect(screen.queryByText(/workspace\//)).not.toBeInTheDocument(); }); }); it("handles Enter key to trigger project open", async () => { mockedApi.openProject.mockResolvedValue("/home/user/myproject"); await renderApp(); await waitFor(() => { expect( screen.getByPlaceholderText(/\/path\/to\/project/i), ).toBeInTheDocument(); }); const input = screen.getByPlaceholderText( /\/path\/to\/project/i, ) as HTMLInputElement; await userEvent.clear(input); await userEvent.type(input, "/home/user/myproject"); fireEvent.keyDown(input, { key: "Enter" }); await waitFor(() => { expect(mockedApi.openProject).toHaveBeenCalledWith( "/home/user/myproject", ); }); }); });