import "@testing-library/jest-dom/vitest"; import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import { afterEach, describe, expect, it, vi } from "vitest"; import { ProjectsPage } from "./ProjectsPage"; import * as projectsApi from "../api/projects"; import { SessionsProvider } from "../state/sessions"; import type { ProjectWithRepos } from "../types"; vi.mock("../api/sessions", () => ({ getUserSessions: vi.fn().mockResolvedValue([]), })); const mockProjects: ProjectWithRepos[] = [ { id: "proj-1", name: "Alpha Project", description: "First project", owner_id: "user-1", default_ssh_key_id: null, repositories: [], created_at: "2026-07-01T00:00:00Z", }, { id: "proj-2", name: "Beta Project", description: null, owner_id: "user-1", default_ssh_key_id: null, repositories: [], created_at: "2026-07-01T00:00:00Z", }, ]; afterEach(() => { cleanup(); vi.restoreAllMocks(); }); describe("ProjectsPage", () => { it("renders loading state initially", () => { vi.spyOn(projectsApi, "listProjects").mockImplementation(() => new Promise(() => {})); render( ); expect(screen.getByText(/loading projects/i)).toBeInTheDocument(); }); it("renders project list after loading", async () => { vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects); render( ); await waitFor(() => { expect(screen.getByText("Alpha Project")).toBeInTheDocument(); }); expect(screen.getByText("Beta Project")).toBeInTheDocument(); expect(screen.getByText("First project")).toBeInTheDocument(); }); it("renders empty state when no projects", async () => { vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]); render( ); await waitFor(() => { expect(screen.getByText(/no projects yet/i)).toBeInTheDocument(); }); }); it("renders error state with retry button", async () => { vi.spyOn(projectsApi, "listProjects").mockRejectedValue(new Error("fail")); render( ); await waitFor(() => { expect(screen.getByText(/failed to load projects/i)).toBeInTheDocument(); }); expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument(); }); it("opens create dialog and submits new project", async () => { const listMock = vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]); const createMock = vi.spyOn(projectsApi, "createProject").mockResolvedValue(mockProjects[0]); render( ); await waitFor(() => { expect(screen.getByText(/no projects yet/i)).toBeInTheDocument(); }); fireEvent.click(screen.getByRole("button", { name: /new project/i })); expect(screen.getByRole("dialog")).toBeInTheDocument(); fireEvent.change(screen.getByPlaceholderText(/project name/i), { target: { value: "Gamma Project" }, }); fireEvent.change(screen.getByPlaceholderText(/optional description/i), { target: { value: "A new project" }, }); fireEvent.click(screen.getByRole("button", { name: /create/i })); await waitFor(() => { expect(createMock).toHaveBeenCalledWith({ name: "Gamma Project", description: "A new project", }); }); expect(listMock).toHaveBeenCalledTimes(2); }); it("shows validation error when name is empty", async () => { vi.spyOn(projectsApi, "listProjects").mockResolvedValue([]); render( ); await waitFor(() => { expect(screen.getByText(/no projects yet/i)).toBeInTheDocument(); }); fireEvent.click(screen.getByRole("button", { name: /new project/i })); fireEvent.click(screen.getByRole("button", { name: /create/i })); expect(screen.getByText(/project name is required/i)).toBeInTheDocument(); }); it("opens edit dialog and saves changes", async () => { const listMock = vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects); const updateMock = vi.spyOn(projectsApi, "updateProject").mockResolvedValue(mockProjects[0]); render( ); await waitFor(() => { expect(screen.getByText("Alpha Project")).toBeInTheDocument(); }); const alphaCard = screen.getByText("Alpha Project").closest(".project-list-item") as HTMLElement | null; if (!alphaCard) throw new Error("Card not found"); fireEvent.click(within(alphaCard).getByRole("button", { name: /edit/i })); expect(screen.getByRole("dialog")).toBeInTheDocument(); const nameInput = screen.getByDisplayValue("Alpha Project"); fireEvent.change(nameInput, { target: { value: "Alpha Updated" } }); fireEvent.click(screen.getByRole("button", { name: /save/i })); await waitFor(() => { expect(updateMock).toHaveBeenCalledWith("proj-1", { name: "Alpha Updated", description: "First project", }); }); expect(listMock).toHaveBeenCalledTimes(2); }); it("shows delete confirmation and deletes project", async () => { const listMock = vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects); const deleteMock = vi.spyOn(projectsApi, "deleteProject").mockResolvedValue(undefined); render( ); await waitFor(() => { expect(screen.getByText("Alpha Project")).toBeInTheDocument(); }); const alphaCard = screen.getByText("Alpha Project").closest(".project-list-item") as HTMLElement | null; if (!alphaCard) throw new Error("Card not found"); fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i })); expect(within(alphaCard).getByText(/are you sure/i)).toBeInTheDocument(); fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i })); await waitFor(() => { expect(deleteMock).toHaveBeenCalledWith("proj-1"); }); expect(listMock).toHaveBeenCalledTimes(2); }); });