import { act, fireEvent, render, screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { ChatInput } from "./ChatInput"; vi.mock("../api/client", () => ({ api: { listProjectFiles: vi.fn().mockResolvedValue([]), }, })); const defaultProps = { loading: false, queuedMessages: [], onSubmit: vi.fn(), onCancel: vi.fn(), onRemoveQueuedMessage: vi.fn(), }; beforeEach(() => { vi.clearAllMocks(); }); describe("Slash command picker overlay (Story 438 AC1)", () => { it("shows slash command picker when / is typed at position 0", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "/" } }); }); expect(screen.getByTestId("slash-command-picker")).toBeInTheDocument(); }); it("does not show slash command picker for plain text", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "hello" } }); }); expect( screen.queryByTestId("slash-command-picker"), ).not.toBeInTheDocument(); }); it("does not show slash command picker when / is not at position 0", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "hello /world" } }); }); expect( screen.queryByTestId("slash-command-picker"), ).not.toBeInTheDocument(); }); }); describe("Slash command list (Story 438 AC2)", () => { it("lists slash commands with name and description", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "/" } }); }); expect(screen.getByTestId("slash-command-picker")).toBeInTheDocument(); // First command should be /help expect(screen.getByTestId("slash-command-item-0")).toBeInTheDocument(); expect(screen.getByTestId("slash-command-item-0")).toHaveTextContent( "/help", ); }); }); describe("Slash command fuzzy filter (Story 438 AC3)", () => { it("filters commands when typing after /", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "/hel" } }); }); expect(screen.getByTestId("slash-command-picker")).toBeInTheDocument(); // /help should match "hel" expect(screen.getByTestId("slash-command-item-0")).toHaveTextContent( "/help", ); // /rebuild should not be visible (no match for "hel") const items = screen.queryAllByTestId(/^slash-command-item-/); const texts = items.map((el) => el.textContent ?? ""); expect(texts.some((t) => t.includes("/rebuild"))).toBe(false); }); it("shows no picker when query matches nothing", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "/zzzzz" } }); }); expect( screen.queryByTestId("slash-command-picker"), ).not.toBeInTheDocument(); }); }); describe("Slash command keyboard navigation (Story 438 AC4)", () => { it("ArrowDown navigates to next item", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "/" } }); }); const item0 = screen.getByTestId("slash-command-item-0"); expect(item0).toHaveStyle({ background: "#2d4a6e" }); await act(async () => { fireEvent.keyDown(textarea, { key: "ArrowDown" }); }); const item1 = screen.getByTestId("slash-command-item-1"); expect(item1).toHaveStyle({ background: "#2d4a6e" }); }); it("ArrowUp stays at 0 when already at top", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "/" } }); }); await act(async () => { fireEvent.keyDown(textarea, { key: "ArrowUp" }); }); const item0 = screen.getByTestId("slash-command-item-0"); expect(item0).toHaveStyle({ background: "#2d4a6e" }); }); it("Enter selects the highlighted command and inserts it", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "/hel" } }); }); await act(async () => { fireEvent.keyDown(textarea, { key: "Enter" }); }); expect( screen.queryByTestId("slash-command-picker"), ).not.toBeInTheDocument(); expect((textarea as HTMLTextAreaElement).value).toBe("/help "); }); it("Tab selects the highlighted command and inserts it", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "/hel" } }); }); await act(async () => { fireEvent.keyDown(textarea, { key: "Tab" }); }); expect( screen.queryByTestId("slash-command-picker"), ).not.toBeInTheDocument(); expect((textarea as HTMLTextAreaElement).value).toBe("/help "); }); it("Escape dismisses the picker", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "/" } }); }); expect(screen.getByTestId("slash-command-picker")).toBeInTheDocument(); await act(async () => { fireEvent.keyDown(textarea, { key: "Escape" }); }); expect( screen.queryByTestId("slash-command-picker"), ).not.toBeInTheDocument(); }); }); describe("Slash command selection inserts with trailing space (Story 438 AC5)", () => { it("clicking a command inserts / with trailing space", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "/" } }); }); await act(async () => { fireEvent.click(screen.getByTestId("slash-command-item-0")); }); expect( screen.queryByTestId("slash-command-picker"), ).not.toBeInTheDocument(); const val = (textarea as HTMLTextAreaElement).value; expect(val).toMatch(/^\/\w+ $/); }); it("selection inserts only the base command (no argument placeholders)", async () => { render(); const textarea = screen.getByPlaceholderText("Send a message..."); await act(async () => { fireEvent.change(textarea, { target: { value: "/ass" } }); }); await act(async () => { fireEvent.keyDown(textarea, { key: "Enter" }); }); expect((textarea as HTMLTextAreaElement).value).toBe("/assign "); }); });