Files
fusion/packages/dashboard/app/components/__tests__/FileMentionPopup.test.tsx
Fusion 9974cc7019 feat(FN-1947): add file mention search and chat autocomplete
- Add file search API wiring in dashboard client/server with route coverage for /api/files/search
- Introduce useFileMention hook and FileMentionPopup component, integrated into ChatView and QuickChatFAB
- Resolve @file references in chat messages so referenced file content is included in AI context
- Add styling, mobile responsiveness, and focused tests for popup UI, hook behavior, chat resolution, and file services
2026-04-18 07:36:57 -07:00

145 lines
4.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { FileMentionPopup } from "../FileMentionPopup";
import type { FileSearchItem } from "../../hooks/useFileMention";
// Mock lucide-react
vi.mock("lucide-react", () => ({
File: ({ size }: { size: number }) => (
<span data-testid="file-icon">File(size={size})</span>
),
}));
describe("FileMentionPopup", () => {
const defaultProps = {
visible: true,
position: { top: 100, left: 50 },
files: [] as FileSearchItem[],
selectedIndex: 0,
onSelect: vi.fn(),
loading: false,
};
beforeEach(() => {
vi.clearAllMocks();
});
it("returns null when not visible", () => {
const { container } = render(<FileMentionPopup {...defaultProps} visible={false} />);
expect(container.firstChild).toBeNull();
});
it("renders loading state", () => {
render(<FileMentionPopup {...defaultProps} loading={true} />);
const loadingEl = screen.getByTestId("file-mention-loading");
expect(loadingEl).toBeInTheDocument();
});
it("renders empty state when no files", () => {
render(<FileMentionPopup {...defaultProps} loading={false} />);
const emptyEl = screen.getByTestId("file-mention-empty");
expect(emptyEl).toBeInTheDocument();
expect(emptyEl).toHaveTextContent("No files found");
});
it("renders file list with correct items", () => {
const files: FileSearchItem[] = [
{ path: "src/index.ts", name: "index.ts" },
{ path: "src/app.ts", name: "app.ts" },
];
render(<FileMentionPopup {...defaultProps} files={files} loading={false} />);
const items = screen.getAllByRole("option");
expect(items).toHaveLength(2);
expect(items[0]).toHaveTextContent("index.ts");
expect(items[1]).toHaveTextContent("app.ts");
});
it("shows selected state on correct item", () => {
const files: FileSearchItem[] = [
{ path: "src/index.ts", name: "index.ts" },
{ path: "src/app.ts", name: "app.ts" },
];
render(<FileMentionPopup {...defaultProps} files={files} selectedIndex={1} loading={false} />);
const items = screen.getAllByRole("option");
expect(items[0]).not.toHaveClass("file-mention-popup-item--selected");
expect(items[1]).toHaveClass("file-mention-popup-item--selected");
});
it("calls onSelect when item is clicked", () => {
const files: FileSearchItem[] = [
{ path: "src/index.ts", name: "index.ts" },
];
const onSelect = vi.fn();
render(
<FileMentionPopup {...defaultProps} files={files} onSelect={onSelect} loading={false} />,
);
const item = screen.getByRole("option");
item.click();
expect(onSelect).toHaveBeenCalledWith({ path: "src/index.ts", name: "index.ts" });
});
it("renders file icon for each item", () => {
const files: FileSearchItem[] = [
{ path: "src/index.ts", name: "index.ts" },
];
render(<FileMentionPopup {...defaultProps} files={files} loading={false} />);
const icons = screen.getAllByTestId("file-icon");
expect(icons).toHaveLength(1);
});
it("shows directory path for nested files", () => {
const files: FileSearchItem[] = [
{ path: "src/components/Button.tsx", name: "Button.tsx" },
];
render(<FileMentionPopup {...defaultProps} files={files} loading={false} />);
const pathEl = screen.getByText("src/components/");
expect(pathEl).toBeInTheDocument();
});
it("does not show directory path for root files", () => {
const files: FileSearchItem[] = [
{ path: "app.ts", name: "app.ts" },
];
render(<FileMentionPopup {...defaultProps} files={files} loading={false} />);
const pathEl = screen.queryByText(/^src\//);
expect(pathEl).not.toBeInTheDocument();
});
it("renders with correct position styles", () => {
const { container } = render(
<FileMentionPopup {...defaultProps} position={{ top: 200, left: 100 }} />,
);
const popup = container.firstChild as HTMLElement;
expect(popup.style.top).toBe("200px");
expect(popup.style.left).toBe("100px");
});
it("renders correct number of items for multiple files", () => {
const files: FileSearchItem[] = [
{ path: "a.ts", name: "a.ts" },
{ path: "b.ts", name: "b.ts" },
{ path: "c.ts", name: "c.ts" },
];
render(<FileMentionPopup {...defaultProps} files={files} loading={false} />);
const items = screen.getAllByRole("option");
expect(items).toHaveLength(3);
});
});