Files
huskies/frontend/src/components/ChatInputSlashCommand.test.tsx
T

241 lines
7.0 KiB
TypeScript

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(<ChatInput {...defaultProps} />);
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(<ChatInput {...defaultProps} />);
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(<ChatInput {...defaultProps} />);
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(<ChatInput {...defaultProps} />);
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(<ChatInput {...defaultProps} />);
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(<ChatInput {...defaultProps} />);
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(<ChatInput {...defaultProps} />);
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(<ChatInput {...defaultProps} />);
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(<ChatInput {...defaultProps} />);
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(<ChatInput {...defaultProps} />);
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(<ChatInput {...defaultProps} />);
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 /<command> with trailing space", async () => {
render(<ChatInput {...defaultProps} />);
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(<ChatInput {...defaultProps} />);
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 ");
});
});