import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import { ProjectOverview } from "../ProjectOverview"; import type { ProjectInfo, ProjectHealth } from "@fusion/core"; // Mock the hooks vi.mock("../../hooks/useProjectHealth", () => ({ useProjectHealth: vi.fn((projectIds: string[]) => ({ healthMap: projectIds.reduce((acc, id) => { acc[id] = { projectId: id, status: "active", activeTaskCount: 5, inFlightAgentCount: 2, totalTasksCompleted: 100, totalTasksFailed: 3, updatedAt: new Date().toISOString(), } as ProjectHealth; return acc; }, {} as Record), loading: false, error: null, refresh: vi.fn(), refreshProject: vi.fn(), })), })); // Mock lucide-react vi.mock("lucide-react", async () => { const actual = await vi.importActual("lucide-react"); return { ...actual, Plus: () => +, LayoutGrid: () => , Filter: () => , ArrowUpDown: () => , Activity: () => , CheckCircle: () => , AlertCircle: () => , Folder: () => 📁, Inbox: () => 📥, }; }); // Mock ProjectCard vi.mock("../ProjectCard", () => ({ ProjectCard: ({ project, onSelect }: { project: ProjectInfo; onSelect: (p: ProjectInfo) => void }) => (
onSelect(project)}> {project.name}
), })); // Mock ProjectGridSkeleton vi.mock("../ProjectGridSkeleton", () => ({ ProjectGridSkeleton: () =>
Loading...
, })); function makeProject(overrides: Partial = {}): ProjectInfo { return { id: "proj_abc123", name: "Test Project", path: "/home/user/projects/test", status: "active", isolationMode: "in-process", createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", lastActivityAt: new Date().toISOString(), ...overrides, }; } const noop = () => {}; describe("ProjectOverview", () => { beforeEach(() => { vi.clearAllMocks(); }); it("renders without crashing with projects", () => { render( ); expect(screen.getByText("Projects")).toBeDefined(); }); it("displays project cards when projects provided", () => { render( ); expect(screen.getByTestId("project-card-proj_1")).toBeDefined(); expect(screen.getByTestId("project-card-proj_2")).toBeDefined(); }); it("shows empty state when no projects", () => { render( ); expect(screen.getByText("No Projects Found")).toBeDefined(); expect(screen.getByText("Add Your First Project")).toBeDefined(); }); it("triggers onAddProject when empty state CTA clicked", () => { const onAddProject = vi.fn(); render( ); fireEvent.click(screen.getByText("Add Your First Project")); expect(onAddProject).toHaveBeenCalled(); }); it("triggers onAddProject when header button clicked", () => { const onAddProject = vi.fn(); render( ); fireEvent.click(screen.getByText("Add Project")); expect(onAddProject).toHaveBeenCalled(); }); it("displays correct stats in header", () => { render( ); // Total projects = 3 - look specifically in stats section const statsSection = screen.getByText("Total").closest(".project-stat__content"); expect(statsSection?.querySelector(".project-stat__value")?.textContent).toBe("3"); }); it("filters projects when clicking filter tabs", async () => { render( ); // Initially shows all projects expect(screen.getByTestId("project-card-proj_1")).toBeDefined(); expect(screen.getByTestId("project-card-proj_2")).toBeDefined(); // Click on "Active" filter fireEvent.click(screen.getByText("Active")); // Should only show active project await waitFor(() => { expect(screen.queryByTestId("project-card-proj_1")).toBeDefined(); }); }); it("shows filter counts on tabs", () => { render( ); // Find the "All" tab and check its count const allTab = screen.getByText("All").closest("button"); expect(allTab?.textContent).toContain("4"); // Active tab should show 2 const activeTab = screen.getByText("Active").closest("button"); expect(activeTab?.textContent).toContain("2"); // Paused tab should show 1 const pausedTab = screen.getByText("Paused").closest("button"); expect(pausedTab?.textContent).toContain("1"); }); it("shows no results message when filter returns empty", () => { render( ); // Click on "Errored" filter - no projects match fireEvent.click(screen.getByText("Errored")); expect(screen.getByText("No projects match the current filter")).toBeDefined(); expect(screen.getByText("Show All Projects")).toBeDefined(); }); it("clears filter when clicking Show All Projects button", () => { render( ); // First filter to errored (empty results) fireEvent.click(screen.getByText("Errored")); expect(screen.getByText("No projects match the current filter")).toBeDefined(); // Click Show All Projects fireEvent.click(screen.getByText("Show All Projects")); // Should be back to showing all expect(screen.getByTestId("project-card-proj_1")).toBeDefined(); }); it("shows loading skeleton when loading prop is true", () => { render( ); expect(screen.getByTestId("project-grid-skeleton")).toBeDefined(); }); it("calls onSelectProject when a project card is clicked", () => { const onSelectProject = vi.fn(); const project = makeProject({ id: "proj_1", name: "Test Project" }); render( ); fireEvent.click(screen.getByTestId("project-card-proj_1")); expect(onSelectProject).toHaveBeenCalledWith(project); }); it("errored tab has special styling when errored projects exist", () => { render( ); // Find the errored filter tab specifically (not the stat label) const erroredTab = screen.getAllByText("Errored").find(el => el.tagName === "BUTTON"); expect(erroredTab?.className).toContain("has-errors"); }); describe("mobile responsive structure", () => { it("renders overview with correct class structure for mobile CSS targets", () => { const { container } = render( ); // Verify container class exists for mobile override targeting expect(container.querySelector(".project-overview")).not.toBeNull(); // Verify header section expect(container.querySelector(".project-overview__header")).not.toBeNull(); // Verify stats section expect(container.querySelector(".project-overview__stats")).not.toBeNull(); // Verify filters section expect(container.querySelector(".project-overview__filters")).not.toBeNull(); // Verify grid container expect(container.querySelector(".project-grid")).not.toBeNull(); }); it("renders stat elements with value/label structure for mobile sizing", () => { const { container } = render( ); // Stats should have the nested structure mobile CSS targets const stats = container.querySelectorAll(".project-stat"); expect(stats.length).toBeGreaterThanOrEqual(3); // Total, Active Tasks, Completed stats.forEach((stat) => { expect(stat.querySelector(".project-stat__value")).not.toBeNull(); expect(stat.querySelector(".project-stat__label")).not.toBeNull(); }); }); it("renders filter tabs with correct classes for mobile stacking", () => { const { container } = render( ); expect(container.querySelector(".project-filter-tabs")).not.toBeNull(); const tabs = container.querySelectorAll(".project-filter-tab"); expect(tabs.length).toBe(4); // All, Active, Paused, Errored tabs.forEach((tab) => { expect(tab.querySelector(".project-filter-count")).not.toBeNull(); }); }); it("renders sort select with aria-label for mobile full-width targeting", () => { const { container } = render( ); expect(container.querySelector(".project-sort")).not.toBeNull(); expect(container.querySelector(".project-sort-select")).not.toBeNull(); expect(screen.getByLabelText("Sort projects")).toBeDefined(); }); }); });