import { describe, it, expect, vi } from "vitest"; import { render, screen, fireEvent } from "@testing-library/react"; import { ProjectCard } from "../ProjectCard"; import type { RegisteredProject, ProjectHealth } from "@fusion/core"; // Mock lucide-react to avoid SVG rendering issues in test env vi.mock("lucide-react", () => ({ Play: () => , Pause: () => , AlertCircle: () => , Loader2: () => , MoreHorizontal: () => null, Trash2: () => 🗑, Folder: () => 📁, ArrowRight: () => , })); function makeProject(overrides: Partial = {}): RegisteredProject { 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", ...overrides, }; } function makeHealth(overrides: Partial = {}): ProjectHealth { return { projectId: "proj_abc123", status: "active", activeTaskCount: 5, inFlightAgentCount: 2, totalTasksCompleted: 100, totalTasksFailed: 3, updatedAt: "2026-01-01T00:00:00.000Z", ...overrides, }; } const noop = () => {}; describe("ProjectCard", () => { it("renders project name and path", () => { render( ); expect(screen.getByText("My Project")).toBeDefined(); expect(screen.getByText("/path/to/project")).toBeDefined(); }); it("renders node availability rows with path details", () => { render( ); expect(screen.getByText("Remote Worker")).toBeDefined(); expect(screen.getByText("/srv/work")).toBeDefined(); }); it("shows overflow indicator when more than three mappings exist", () => { render( ); expect(screen.getByText("+1 more")).toBeDefined(); }); it("truncates long paths", () => { const longPath = "/very/long/path/to/the/project/directory/that/needs/truncation"; render( ); // Should show truncated version const pathElement = screen.getByText(/\/very\/long\/.*\/truncation/); expect(pathElement).toBeDefined(); }); it("renders active status badge", () => { render( ); expect(screen.getByText("Active")).toBeDefined(); }); it("renders paused status badge", () => { render( ); expect(screen.getByText("Paused")).toBeDefined(); }); it("renders errored status badge", () => { render( ); expect(screen.getByText("Error")).toBeDefined(); }); it("renders initializing status badge with spinner", () => { render( ); expect(screen.getByText("Initializing")).toBeDefined(); expect(screen.getByTestId("loader-icon")).toBeDefined(); }); it("displays health metrics", () => { render( ); expect(screen.getByText("10")).toBeDefined(); expect(screen.getByText("Active Tasks")).toBeDefined(); expect(screen.getByText("3")).toBeDefined(); expect(screen.getByText("Agents")).toBeDefined(); expect(screen.getByText("250")).toBeDefined(); expect(screen.getByText("Completed")).toBeDefined(); }); it("shows 'No health data' when health is null", () => { render( ); expect(screen.getByText("No health data available")).toBeDefined(); }); it("formats relative time for last activity", () => { const recentTime = new Date(Date.now() - 5 * 60000).toISOString(); // 5 minutes ago render( ); expect(screen.getByText("5m ago")).toBeDefined(); }); it("shows 'Never' when no last activity", () => { render( ); expect(screen.getByText("Never")).toBeDefined(); }); it("calls onSelect when card is clicked", () => { const onSelect = vi.fn(); const project = makeProject(); const { container } = render( ); const card = container.querySelector('[data-project-id="proj_abc123"]'); expect(card).not.toBeNull(); fireEvent.click(card!); expect(onSelect).toHaveBeenCalledWith(project); }); it("calls onSelect when Enter key is pressed", () => { const onSelect = vi.fn(); const project = makeProject(); const { container } = render( ); const card = container.querySelector('[data-project-id="proj_abc123"]'); expect(card).not.toBeNull(); fireEvent.keyDown(card!, { key: "Enter" }); expect(onSelect).toHaveBeenCalledWith(project); }); it("calls onPause when pause button is clicked", () => { const onPause = vi.fn(); const project = makeProject({ status: "active" }); render( ); fireEvent.click(screen.getByLabelText("Pause project")); expect(onPause).toHaveBeenCalledWith(project); }); it("calls onResume when resume button is clicked", () => { const onResume = vi.fn(); const project = makeProject({ status: "paused" }); render( ); fireEvent.click(screen.getByLabelText("Resume project")); expect(onResume).toHaveBeenCalledWith(project); }); it("calls onRemove when remove button is clicked", () => { const onRemove = vi.fn(); const project = makeProject(); render( ); // First click arms the button (does not call onRemove) fireEvent.click(screen.getByLabelText("Remove project")); expect(onRemove).not.toHaveBeenCalled(); // Second click confirms and calls onRemove fireEvent.click(screen.getByLabelText("Confirm remove project")); expect(onRemove).toHaveBeenCalledWith(project); }); it("shows armed state on first click of remove button", () => { const onRemove = vi.fn(); const { container } = render( ); // Initially not armed expect(screen.getByLabelText("Remove project")).toBeDefined(); expect(container.querySelector(".is-armed")).toBeNull(); // First click arms the button fireEvent.click(screen.getByLabelText("Remove project")); expect(onRemove).not.toHaveBeenCalled(); // Button now shows confirm state expect(screen.getByLabelText("Confirm remove project")).toBeDefined(); expect(container.querySelector(".is-armed")).not.toBeNull(); }); it("disables pause button when initializing", () => { const { container } = render( ); // Find the pause button by its title attribute const pauseButton = container.querySelector('button[title="Cannot pause while initializing"]'); expect(pauseButton).not.toBeNull(); expect(pauseButton).toBeDisabled(); }); it("disables all buttons when isLoading is true", () => { render( ); expect(screen.getByLabelText("Pause project")).toBeDisabled(); expect(screen.getByLabelText("Open project")).toBeDisabled(); expect(screen.getByLabelText("Remove project")).toBeDisabled(); }); it("adds loading class when isLoading is true", () => { const { container } = render( ); expect(container.querySelector(".project-card-loading")).toBeDefined(); }); it("adds errored class when project status is errored", () => { const { container } = render( ); expect(container.querySelector(".project-card-errored")).toBeDefined(); }); it("prevents event bubbling when clicking action buttons", () => { const onSelect = vi.fn(); const onPause = vi.fn(); render( ); fireEvent.click(screen.getByLabelText("Pause project")); expect(onPause).toHaveBeenCalled(); expect(onSelect).not.toHaveBeenCalled(); }); it("renders with data-project-id attribute", () => { const { container } = render( ); const card = container.querySelector("[data-project-id='proj_test123']"); expect(card).toBeDefined(); }); it("uses health lastActivityAt as fallback when project lastActivityAt is undefined", () => { const healthTime = "2026-01-15T12:00:00.000Z"; render( ); // Should show the health's lastActivityAt formatted (shows as date since it's > 7 days ago) expect(screen.getByText(/Last activity:/)).toBeDefined(); // The formatted date should show (1/15/2026 format in US locale) expect(screen.getByText(/1\//)).toBeDefined(); }); describe("mobile responsive structure", () => { it("renders card with correct class structure for mobile CSS targets", () => { const { container } = render( ); // Verify all mobile-targeted class names exist expect(container.querySelector(".project-card")).not.toBeNull(); expect(container.querySelector(".project-card-header")).not.toBeNull(); expect(container.querySelector(".project-card-health")).not.toBeNull(); expect(container.querySelector(".project-card-footer")).not.toBeNull(); expect(container.querySelector(".project-card-actions")).not.toBeNull(); // Verify action buttons have mobile-targeted classes expect(container.querySelector(".project-card-action")).not.toBeNull(); }); }); });