Files
fusion/packages/dashboard/app/hooks/__tests__/useFileBrowser.test.ts
gsxdsm 8ef31a81c4 test(FN-1282): expand dashboard hook and modal coverage
- Add tests for useAgents and useProjectHealth hooks, including loading, success, and error flows
- Add useFileBrowser and useFileEditor hook tests for file navigation, save behavior, and edge cases
- Add component tests for AgentImportModal and AgentGenerationModal interaction paths
- Add MissionInterviewModal tests covering question flow, submission, and state transitions
- Stabilize dashboard test suite cleanup and mocking patterns to reduce flakiness
2026-04-08 10:48:42 -07:00

186 lines
5.6 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));
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");
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));
await waitFor(() => {
expect(mockFetchFileList).toHaveBeenCalledWith("FN-001", undefined);
});
act(() => {
result.current.setPath(".");
});
await waitFor(() => {
expect(mockFetchFileList).toHaveBeenLastCalledWith("FN-001", undefined);
});
});
it("passes non-dot paths directly to fetchFileList", async () => {
const { result } = renderHook(() => useFileBrowser("FN-001", true));
await waitFor(() => {
expect(mockFetchFileList).toHaveBeenCalled();
});
act(() => {
result.current.setPath("subdir");
});
await waitFor(() => {
expect(mockFetchFileList).toHaveBeenLastCalledWith("FN-001", "subdir");
});
});
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);
});
});