Files
fusion/packages/dashboard/app/hooks/useCurrentProject.test.ts
gsxdsm 47c805cb18 fix(FN-949): prevent bounce-back on Projects nav and show in mobile overflow
clearCurrentProject was triggering auto-reselect in useCurrentProject,
making the Projects button ineffective for single-project users. Added
explicit-clear flag to suppress auto-select. Also changed mobile overflow
menu to show Projects for single-project users (was gated to 2+ projects).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 20:30:24 -07:00

179 lines
5.2 KiB
TypeScript

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();
});
// After explicit clear, should stay null (no auto-select) so user can view overview
await waitFor(() => {
expect(result.current.currentProject).toBeNull();
});
// localStorage should be cleared
expect(localStorage.getItem("kb-dashboard-current-project")).toBeNull();
});
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;
});
});