feat(KB-502): add dashboard multi-project hooks and project API routes
- Add project API routes for multi-project backend integration - Create useProjects hook for listing and managing registered projects - Create useCurrentProject hook for current project selection and switching - Create useProjectHealth hook for real-time project health metrics - Create useActivityLog hook with ActivityLogModal integration - Add comprehensive test coverage for all hooks (useProjects, useCurrentProject, useActivityLog) - Add test setup utilities for React Query mocking
This commit is contained in:
178
packages/dashboard/app/hooks/useCurrentProject.test.ts
Normal file
178
packages/dashboard/app/hooks/useCurrentProject.test.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { renderHook, waitFor, act } from "@testing-library/react";
|
||||
import { useCurrentProject } from "./useCurrentProject";
|
||||
import type { ProjectInfo } from "../api";
|
||||
|
||||
describe("useCurrentProject", () => {
|
||||
const mockProjects: ProjectInfo[] = [
|
||||
{
|
||||
id: "proj_1",
|
||||
name: "Project One",
|
||||
path: "/path/one",
|
||||
status: "active",
|
||||
isolationMode: "in-process",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "proj_2",
|
||||
name: "Project Two",
|
||||
path: "/path/two",
|
||||
status: "paused",
|
||||
isolationMode: "child-process",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("initializes with null when no saved project and no available projects", async () => {
|
||||
const { result } = renderHook(() => useCurrentProject([]));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.currentProject).toBeNull();
|
||||
});
|
||||
|
||||
it("defaults to first active project when projects available but no selection", async () => {
|
||||
const { result } = renderHook(() => useCurrentProject(mockProjects));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
// Should default to first active project
|
||||
await waitFor(() => {
|
||||
expect(result.current.currentProject?.id).toBe("proj_1");
|
||||
});
|
||||
});
|
||||
|
||||
it("loads saved project from localStorage", async () => {
|
||||
localStorage.setItem("kb-dashboard-current-project", JSON.stringify(mockProjects[0]));
|
||||
|
||||
const { result } = renderHook(() => useCurrentProject(mockProjects));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
// After validation, it should have the saved project
|
||||
await waitFor(() => {
|
||||
expect(result.current.currentProject).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults to first active project when no selection", async () => {
|
||||
const { result } = renderHook(() => useCurrentProject(mockProjects));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
// Should default to first active project
|
||||
await waitFor(() => {
|
||||
expect(result.current.currentProject?.id).toBe("proj_1");
|
||||
});
|
||||
});
|
||||
|
||||
it("clears selection when project no longer exists", async () => {
|
||||
const unregisteredProject: ProjectInfo = {
|
||||
id: "proj_old",
|
||||
name: "Old Project",
|
||||
path: "/old/path",
|
||||
status: "active",
|
||||
isolationMode: "in-process",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
localStorage.setItem("kb-dashboard-current-project", JSON.stringify(unregisteredProject));
|
||||
|
||||
const { result } = renderHook(() => useCurrentProject(mockProjects));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
// Should clear and default to first active
|
||||
await waitFor(() => {
|
||||
expect(result.current.currentProject?.id).toBe("proj_1");
|
||||
});
|
||||
});
|
||||
|
||||
it("setCurrentProject updates selection and saves to localStorage", async () => {
|
||||
const { result } = renderHook(() => useCurrentProject(mockProjects));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.setCurrentProject(mockProjects[1]);
|
||||
});
|
||||
|
||||
expect(result.current.currentProject?.id).toBe("proj_2");
|
||||
expect(localStorage.getItem("kb-dashboard-current-project")).toContain("proj_2");
|
||||
});
|
||||
|
||||
it("clearCurrentProject removes selection and re-defaults when projects available", async () => {
|
||||
localStorage.setItem("kb-dashboard-current-project", JSON.stringify(mockProjects[1]));
|
||||
|
||||
const { result } = renderHook(() => useCurrentProject(mockProjects));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
// After loading, we should have proj_2 from localStorage
|
||||
await waitFor(() => {
|
||||
expect(result.current.currentProject?.id).toBe("proj_2");
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.clearCurrentProject();
|
||||
});
|
||||
|
||||
// With available projects, it re-defaults to first active and saves to localStorage
|
||||
await waitFor(() => {
|
||||
expect(result.current.currentProject?.id).toBe("proj_1");
|
||||
});
|
||||
|
||||
// After re-defaulting, localStorage should have the default project
|
||||
expect(localStorage.getItem("kb-dashboard-current-project")).toContain("proj_1");
|
||||
});
|
||||
|
||||
it("handles localStorage errors gracefully", async () => {
|
||||
// Mock localStorage to throw
|
||||
const originalSetItem = localStorage.setItem;
|
||||
localStorage.setItem = vi.fn(() => {
|
||||
throw new Error("Storage error");
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useCurrentProject(mockProjects));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.setCurrentProject(mockProjects[0]);
|
||||
});
|
||||
|
||||
// Should still update state even if localStorage fails
|
||||
expect(result.current.currentProject?.id).toBe("proj_1");
|
||||
|
||||
// Restore
|
||||
localStorage.setItem = originalSetItem;
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user