1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
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(<FileTree />);
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(<FileTree />);
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(<FileTree />);
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(<FileTree />);
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(<FileTree />);
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(<FileTree />);
await waitFor(() =>
expect(screen.getByText("permission denied")).toBeDefined(),
);
});
});
|