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
This commit is contained in:
185
packages/dashboard/app/hooks/__tests__/useFileBrowser.test.ts
Normal file
185
packages/dashboard/app/hooks/__tests__/useFileBrowser.test.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user