import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { DirectoryPicker } from "../DirectoryPicker"; // Mock lucide-react vi.mock("lucide-react", async () => { const actual = await vi.importActual("lucide-react"); return { ...actual, Folder: ({ size, ...props }: any) => πŸ“, FolderOpen: ({ size, ...props }: any) => πŸ“‚, ChevronRight: ({ size, ...props }: any) => β†’, ChevronUp: ({ size, ...props }: any) => ↑, Loader2: ({ size, ...props }: any) => ⟳, Eye: ({ size, ...props }: any) => πŸ‘, EyeOff: ({ size, ...props }: any) => πŸ™ˆ, AlertCircle: ({ size, ...props }: any) => ⚠, }; }); // Mock the API vi.mock("../../api", () => ({ browseDirectory: vi.fn(), })); import { browseDirectory } from "../../api"; const mockBrowseDirectory = vi.mocked(browseDirectory); describe("DirectoryPicker", () => { beforeEach(() => { vi.clearAllMocks(); mockBrowseDirectory.mockResolvedValue({ currentPath: "/home/user", parentPath: "/home", entries: [ { name: "projects", path: "/home/user/projects", hasChildren: true }, { name: "Documents", path: "/home/user/Documents", hasChildren: true }, { name: "empty-dir", path: "/home/user/empty-dir", hasChildren: false }, ], }); }); it("renders with value and placeholder", () => { render( ); const input = screen.getByPlaceholderText("Select a directory") as HTMLInputElement; expect(input.value).toBe("/some/path"); expect(input.classList.contains("input")).toBe(true); const browseButton = screen.getByRole("button", { name: "Browse directories" }); expect(browseButton.classList.contains("btn")).toBe(true); expect(browseButton.classList.contains("btn-secondary")).toBe(true); expect(browseButton.classList.contains("btn-sm")).toBe(true); }); it("calls onChange when typing in the input", () => { const onChange = vi.fn(); render(); const input = screen.getByPlaceholderText("/path/to/your/project"); fireEvent.change(input, { target: { value: "/new/path" } }); expect(onChange).toHaveBeenCalledWith("/new/path"); }); it("opens browser panel and fetches entries on Browse click", async () => { render(); fireEvent.click(screen.getByText("Browse")); await waitFor(() => { expect(mockBrowseDirectory).toHaveBeenCalled(); }); await waitFor(() => { expect(screen.getByText("projects")).toBeDefined(); expect(screen.getByText("Documents")).toBeDefined(); expect(screen.getByText("empty-dir")).toBeDefined(); }); }); it("navigates into a directory on click", async () => { render(); fireEvent.click(screen.getByText("Browse")); await waitFor(() => { expect(screen.getByText("projects")).toBeDefined(); }); // Now click on "projects" to navigate into it mockBrowseDirectory.mockResolvedValueOnce({ currentPath: "/home/user/projects", parentPath: "/home/user", entries: [ { name: "my-app", path: "/home/user/projects/my-app", hasChildren: true }, ], }); fireEvent.click(screen.getByText("projects")); await waitFor(() => { expect(mockBrowseDirectory).toHaveBeenCalledWith("/home/user/projects", false, undefined, undefined); }); }); it("calls onChange when Select is clicked", async () => { const onChange = vi.fn(); render(); fireEvent.click(screen.getByText("Browse")); await waitFor(() => { expect(screen.getByText("projects")).toBeDefined(); }); const selectButton = screen.getByRole("button", { name: "Select" }); expect(selectButton.classList.contains("btn")).toBe(true); expect(selectButton.classList.contains("btn-primary")).toBe(true); expect(selectButton.classList.contains("directory-picker-select-btn")).toBe(true); fireEvent.click(selectButton); expect(onChange).toHaveBeenCalledWith("/home/user"); }); it("shows loading state while fetching", async () => { let resolvePromise: (val: any) => void; mockBrowseDirectory.mockImplementation( () => new Promise((resolve) => { resolvePromise = resolve; }) ); render(); fireEvent.click(screen.getByText("Browse")); await waitFor(() => { expect(screen.getByText("Loading…")).toBeDefined(); }); // Resolve to clean up resolvePromise!({ currentPath: "/home/user", parentPath: "/home", entries: [], }); await waitFor(() => { expect(screen.getByText("No subdirectories")).toBeDefined(); }); }); it("shows error state on fetch failure", async () => { mockBrowseDirectory.mockRejectedValueOnce(new Error("Permission denied")); render(); fireEvent.click(screen.getByText("Browse")); await waitFor(() => { expect(screen.getByText("Permission denied")).toBeDefined(); }); }); it("closes browser on second Browse click", async () => { render(); fireEvent.click(screen.getByText("Browse")); await waitFor(() => { expect(screen.getByText("projects")).toBeDefined(); }); fireEvent.click(screen.getByText("Browse")); expect(screen.queryByText("projects")).toBeNull(); }); it("browses remote node when nodeId is provided", async () => { render( ); fireEvent.click(screen.getByText("Browse")); await waitFor(() => { expect(mockBrowseDirectory).toHaveBeenCalledWith(undefined, false, "remote-1", "local-1"); }); }); it("falls back to local browsing when nodeId matches localNodeId", async () => { render( ); fireEvent.click(screen.getByText("Browse")); await waitFor(() => { expect(mockBrowseDirectory).toHaveBeenCalledWith(undefined, false, "local-1", "local-1"); }); }); it("navigating directories passes nodeId", async () => { render( ); fireEvent.click(screen.getByText("Browse")); await waitFor(() => { expect(screen.getByText("projects")).toBeDefined(); }); // Navigate into projects directory mockBrowseDirectory.mockResolvedValueOnce({ currentPath: "/home/user/projects", parentPath: "/home/user", entries: [ { name: "my-app", path: "/home/user/projects/my-app", hasChildren: true }, ], }); fireEvent.click(screen.getByText("projects")); await waitFor(() => { expect(mockBrowseDirectory).toHaveBeenCalledWith("/home/user/projects", false, "remote-1", "local-1"); }); }); });