import { act, fireEvent, render, screen, waitFor, } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { api } from "../api/client"; import { ChatInput } from "./ChatInput"; vi.mock("../api/client", () => ({ api: { listProjectFiles: vi.fn(), }, })); const mockedListProjectFiles = vi.mocked(api.listProjectFiles); const defaultProps = { loading: false, queuedMessages: [], onSubmit: vi.fn(), onCancel: vi.fn(), onRemoveQueuedMessage: vi.fn(), }; beforeEach(() => { vi.clearAllMocks(); mockedListProjectFiles.mockResolvedValue([ "src/main.rs", "src/lib.rs", "frontend/index.html", "README.md", ]); }); describe("File picker overlay (Story 269 AC1)", () => { it("shows file picker overlay when @ is typed", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "@" } }); }); await waitFor(() => { expect(screen.getByTestId("file-picker-overlay")).toBeInTheDocument(); }); }); it("does not show file picker overlay for text without @", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "hello world" } }); }); expect(screen.queryByTestId("file-picker-overlay")).not.toBeInTheDocument(); }); }); describe("File picker fuzzy matching (Story 269 AC2)", () => { it("filters files by query typed after @", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "@main" } }); }); await waitFor(() => { expect(screen.getByTestId("file-picker-overlay")).toBeInTheDocument(); }); // main.rs should be visible, README.md should not expect(screen.getByText("src/main.rs")).toBeInTheDocument(); expect(screen.queryByText("README.md")).not.toBeInTheDocument(); }); it("shows all files when @ is typed with no query", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "@" } }); }); await waitFor(() => { expect(screen.getByTestId("file-picker-overlay")).toBeInTheDocument(); }); // All 4 files should be visible expect(screen.getByText("src/main.rs")).toBeInTheDocument(); expect(screen.getByText("src/lib.rs")).toBeInTheDocument(); expect(screen.getByText("README.md")).toBeInTheDocument(); }); }); describe("File picker selection (Story 269 AC3)", () => { it("clicking a file inserts @path into the message", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "@" } }); }); await waitFor(() => { expect(screen.getByTestId("file-picker-item-0")).toBeInTheDocument(); }); await act(async () => { fireEvent.click(screen.getByTestId("file-picker-item-0")); }); // Picker should be dismissed and the file reference inserted expect(screen.queryByTestId("file-picker-overlay")).not.toBeInTheDocument(); expect((textarea as HTMLTextAreaElement).value).toMatch(/^@\S+/); }); it("Enter key selects highlighted file and inserts it into message", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "@main" } }); }); await waitFor(() => { expect(screen.getByTestId("file-picker-overlay")).toBeInTheDocument(); }); await act(async () => { fireEvent.keyDown(textarea, { key: "Enter" }); }); expect(screen.queryByTestId("file-picker-overlay")).not.toBeInTheDocument(); expect((textarea as HTMLTextAreaElement).value).toContain("@src/main.rs"); }); }); describe("File picker dismiss (Story 269 AC5)", () => { it("Escape key dismisses the file picker", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "@" } }); }); await waitFor(() => { expect(screen.getByTestId("file-picker-overlay")).toBeInTheDocument(); }); await act(async () => { fireEvent.keyDown(textarea, { key: "Escape" }); }); expect(screen.queryByTestId("file-picker-overlay")).not.toBeInTheDocument(); }); }); describe("Multiple @ references (Story 269 AC6)", () => { it("typing @ after a completed reference triggers picker again", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); // First reference await act(async () => { fireEvent.change(textarea, { target: { value: "@main" } }); }); await waitFor(() => { expect(screen.getByTestId("file-picker-overlay")).toBeInTheDocument(); }); // Select file await act(async () => { fireEvent.keyDown(textarea, { key: "Enter" }); }); // Type a second @ await act(async () => { const current = (textarea as HTMLTextAreaElement).value; fireEvent.change(textarea, { target: { value: `${current} @` } }); }); await waitFor(() => { expect(screen.getByTestId("file-picker-overlay")).toBeInTheDocument(); }); }); });