Files
fusion/packages/dashboard/app/hooks/__tests__/useFileBrowser.test.ts
gsxdsm 51e23786c0 fix(dashboard): ensure file/import API calls are project-scoped in tests
Update 4 test files to pass projectId="project-1" to hooks/components
and assert it flows through to the API layer:
- useFileBrowser: fetchFileList receives (taskId, path, projectId)
- useFileEditor: fetchFileContent/saveFileContent receive projectId
- FileBrowser: deleteFile/renameFile/copyFile/moveFile receive projectId
- GitHubImportModal: apiImportGitHubIssue/Pull receive projectId

All 6653 tests pass across 242 test files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 21:40:34 -07:00

186 lines
5.7 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, act, waitFor } from "@testing-library/react";
import { useFileBrowser } from "../useFileBrowser";
import * as api from "../../api";
import type { FileListResponse } from "../../api";
vi.mock("../../api", () => ({
fetchFileList: vi.fn(),
}));
const mockFetchFileList = vi.mocked(api.fetchFileList);
function response(path: string, names: string[]): FileListResponse {
return {
path,
entries: names.map((name) => ({
name,
type: name.includes(".") ? "file" : "directory",
mtime: "2026-01-01T00:00:00.000Z",
...(name.includes(".") ? { size: 123 } : {}),
})),
};
}
describe("useFileBrowser", () => {
beforeEach(() => {
mockFetchFileList.mockReset();
mockFetchFileList.mockResolvedValue(response(".", ["src", "README.md"]));
});
afterEach(() => {
vi.clearAllMocks();
});
it("returns empty entries and loading=false when enabled=false", async () => {
const { result } = renderHook(() => useFileBrowser("FN-001", false));
expect(result.current.entries).toEqual([]);
expect(result.current.loading).toBe(false);
expect(result.current.error).toBeNull();
await new Promise((resolve) => setTimeout(resolve, 10));
expect(mockFetchFileList).not.toHaveBeenCalled();
});
it("returns empty entries and loading=false when taskId is empty", async () => {
const { result } = renderHook(() => useFileBrowser("", true));
expect(result.current.entries).toEqual([]);
expect(result.current.loading).toBe(false);
await new Promise((resolve) => setTimeout(resolve, 10));
expect(mockFetchFileList).not.toHaveBeenCalled();
});
it("fetches file list when enabled and sets loading then entries", async () => {
const { result } = renderHook(() => useFileBrowser("FN-001", true));
await waitFor(() => {
expect(result.current.loading).toBe(true);
});
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(result.current.entries).toHaveLength(2);
});
});
it("populates entries from FileListResponse", async () => {
mockFetchFileList.mockResolvedValueOnce(response(".", ["docs", "notes.md", "index.ts"]));
const { result } = renderHook(() => useFileBrowser("FN-001", true));
await waitFor(() => {
expect(result.current.entries.map((entry) => entry.name)).toEqual(["docs", "notes.md", "index.ts"]);
});
});
it("handles fetch error by setting error message and clearing entries", async () => {
mockFetchFileList.mockRejectedValueOnce(new Error("Failed to load files"));
const { result } = renderHook(() => useFileBrowser("FN-001", true));
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(result.current.error).toBe("Failed to load files");
});
expect(result.current.entries).toEqual([]);
});
it("setPath updates currentPath, clears error, and triggers new fetch", async () => {
mockFetchFileList
.mockRejectedValueOnce(new Error("boom"))
.mockResolvedValueOnce(response("src", ["index.ts"]));
const { result } = renderHook(() => useFileBrowser("FN-001", true, "project-1"));
await waitFor(() => {
expect(result.current.error).toBe("boom");
});
act(() => {
result.current.setPath("src");
});
expect(result.current.currentPath).toBe("src");
expect(result.current.error).toBeNull();
await waitFor(() => {
expect(mockFetchFileList).toHaveBeenLastCalledWith("FN-001", "src", "project-1");
expect(result.current.entries.map((entry) => entry.name)).toEqual(["index.ts"]);
});
});
it("normalizes '.' path to undefined when calling fetchFileList", async () => {
const { result } = renderHook(() => useFileBrowser("FN-001", true, "project-1"));
await waitFor(() => {
expect(mockFetchFileList).toHaveBeenCalledWith("FN-001", undefined, "project-1");
});
act(() => {
result.current.setPath(".");
});
await waitFor(() => {
expect(mockFetchFileList).toHaveBeenLastCalledWith("FN-001", undefined, "project-1");
});
});
it("passes non-dot paths directly to fetchFileList", async () => {
const { result } = renderHook(() => useFileBrowser("FN-001", true, "project-1"));
await waitFor(() => {
expect(mockFetchFileList).toHaveBeenCalled();
});
act(() => {
result.current.setPath("subdir");
});
await waitFor(() => {
expect(mockFetchFileList).toHaveBeenLastCalledWith("FN-001", "subdir", "project-1");
});
});
it("refresh increments refresh key and triggers re-fetch", async () => {
mockFetchFileList
.mockResolvedValueOnce(response(".", ["a.txt"]))
.mockResolvedValueOnce(response(".", ["a.txt", "b.txt"]));
const { result } = renderHook(() => useFileBrowser("FN-001", true));
await waitFor(() => {
expect(result.current.entries.map((entry) => entry.name)).toEqual(["a.txt"]);
});
act(() => {
result.current.refresh();
});
await waitFor(() => {
expect(result.current.entries.map((entry) => entry.name)).toEqual(["a.txt", "b.txt"]);
expect(mockFetchFileList).toHaveBeenCalledTimes(2);
});
});
it("cancels in-flight fetch on unmount", async () => {
let resolveFetch!: (value: FileListResponse) => void;
mockFetchFileList.mockReturnValueOnce(
new Promise<FileListResponse>((resolve) => {
resolveFetch = resolve;
}),
);
const { unmount } = renderHook(() => useFileBrowser("FN-001", true));
unmount();
resolveFetch(response(".", ["late.txt"]));
await new Promise((resolve) => setTimeout(resolve, 10));
expect(mockFetchFileList).toHaveBeenCalledTimes(1);
});
});