import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { FileEntry } from "../api"; import { resetWorkspace, useWorkspace } from "../workspace"; import { FileTree } from "./FileTree"; const { listFilesMock } = vi.hoisted(() => ({ listFilesMock: vi.fn() })); vi.mock("../api", () => ({ listFiles: listFilesMock, })); const rootEntries: FileEntry[] = [ { name: "src", path: "/w/src", isDir: true, size: 0 }, { name: "README.md", path: "/w/README.md", isDir: false, size: 7 }, ]; describe("FileTree", () => { beforeEach(() => { resetWorkspace(); listFilesMock.mockReset(); }); it("lists the workspace root", async () => { listFilesMock.mockResolvedValue({ path: "/w", entries: rootEntries }); render(); await waitFor(() => expect(screen.getByText("README.md")).toBeDefined()); expect(screen.getByText(/src/)).toBeDefined(); expect(listFilesMock.mock.calls[0]).toEqual([]); }); it("expands a directory lazily", async () => { listFilesMock.mockImplementation((path?: string) => Promise.resolve( path === "/w/src" ? { path: "/w/src", entries: [ { name: "main.go", path: "/w/src/main.go", isDir: false, size: 3, }, ], } : { path: "/w", entries: rootEntries }, ), ); render(); await waitFor(() => expect(screen.getByText(/src/)).toBeDefined()); fireEvent.click(screen.getByText(/src/)); await waitFor(() => expect(screen.getByText("main.go")).toBeDefined()); expect(listFilesMock).toHaveBeenCalledWith("/w/src"); }); it("opens a file in preview on click and in editor on double-click", async () => { listFilesMock.mockResolvedValue({ path: "/w", entries: rootEntries }); render(); await waitFor(() => expect(screen.getByText("README.md")).toBeDefined()); fireEvent.click(screen.getByText("README.md")); expect(useWorkspace.getState()).toMatchObject({ tab: "preview", previewPath: "/w/README.md", }); fireEvent.dblClick(screen.getByText("README.md")); expect(useWorkspace.getState()).toMatchObject({ tab: "editor", editorPath: "/w/README.md", }); }); it("shows Material icons for files and directories", async () => { listFilesMock.mockResolvedValue({ path: "/w", entries: rootEntries }); const { container } = render(); await waitFor(() => expect(screen.getByText("README.md")).toBeDefined()); const icons = Array.from( container.querySelectorAll("img.filetree-icon"), ).map((img) => img.getAttribute("src")); expect(icons).toHaveLength(2); for (const src of icons) { expect(src).toMatch(/^\/material-icons\/.+\.svg$/); } }); it("highlights the file open in the preview", async () => { listFilesMock.mockResolvedValue({ path: "/w", entries: rootEntries }); const { container } = render(); await waitFor(() => expect(screen.getByText("README.md")).toBeDefined()); expect(container.querySelector(".filetree-active")).toBeNull(); fireEvent.click(screen.getByText("README.md")); await waitFor(() => expect(container.querySelector(".filetree-active")).not.toBeNull(), ); }); it("surfaces API errors", async () => { listFilesMock.mockRejectedValue(new Error("permission denied")); render(); await waitFor(() => expect(screen.getByText("permission denied")).toBeDefined(), ); }); });