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(),
);
});
});