Files
headquarter/apps/web/src/pages/ProjectsPage.test.tsx
T
Developer b21456093e feat: add confirmation dialogs for workspace and session deletion
- Replace inline project/workspace delete confirmation with ConfirmDialog
- Add ConfirmDialog before deleting active sessions from dashboard
- Update ProjectsPage and DashboardPage tests for modal flow
- Fix dashboard test mock paths (git-repositories, tool-types)

Quality gates: typecheck, lint, build pass; targeted tests 12/12 pass
2026-06-15 11:50:09 +00:00

210 lines
6.2 KiB
TypeScript

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";
const renderWithRouter = (ui: React.ReactElement) =>
render(<MemoryRouter>{ui}</MemoryRouter>);
const mockProjects = [
{
id: "proj-1",
name: "Alpha Project",
description: "First project",
owner_id: "user-1",
default_ssh_key_id: null,
},
{
id: "proj-2",
name: "Beta Project",
description: null,
owner_id: "user-1",
default_ssh_key_id: null,
},
];
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
describe("ProjectsPage", () => {
it("renders loading state initially", () => {
vi.spyOn(projectsApi, "listProjects").mockImplementation(
() => new Promise(() => {}),
);
renderWithRouter(<ProjectsPage />);
expect(screen.getByText(/loading projects/i)).toBeInTheDocument();
});
it("renders project list after loading", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
renderWithRouter(<ProjectsPage />);
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([]);
renderWithRouter(<ProjectsPage />);
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"));
renderWithRouter(<ProjectsPage />);
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]);
renderWithRouter(<ProjectsPage />);
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([]);
renderWithRouter(<ProjectsPage />);
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("renders settings link for each project", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
renderWithRouter(<ProjectsPage />);
await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
});
const alphaCard = screen
.getByText("Alpha Project")
.closest(".project-card") as HTMLElement | null;
if (!alphaCard) throw new Error("Card not found");
const settingsLink = within(alphaCard).getByRole("link", {
name: /settings/i,
});
expect(settingsLink).toBeInTheDocument();
expect(settingsLink).toHaveAttribute("href", "/projects/proj-1/settings");
});
it("renders open workspace link as rightmost action", async () => {
vi.spyOn(projectsApi, "listProjects").mockResolvedValue(mockProjects);
renderWithRouter(<ProjectsPage />);
await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
});
const alphaCard = screen
.getByText("Alpha Project")
.closest(".project-card") as HTMLElement | null;
if (!alphaCard) throw new Error("Card not found");
const actions = alphaCard.querySelector(".project-actions");
if (!actions) throw new Error("Actions container not found");
const workspaceLink = within(alphaCard).getByRole("link", {
name: /open workspace/i,
});
expect(workspaceLink).toBeInTheDocument();
expect(workspaceLink).toHaveAttribute("href", "/projects/proj-1");
// Verify it's the last action in the container
const allActions = actions.querySelectorAll("a, button");
const lastAction = allActions[allActions.length - 1];
expect(lastAction).toBe(workspaceLink);
});
it("shows delete confirmation dialog and deletes project", async () => {
const listMock = vi
.spyOn(projectsApi, "listProjects")
.mockResolvedValue(mockProjects);
const deleteMock = vi
.spyOn(projectsApi, "deleteProject")
.mockResolvedValue(undefined);
renderWithRouter(<ProjectsPage />);
await waitFor(() => {
expect(screen.getByText("Alpha Project")).toBeInTheDocument();
});
const alphaCard = screen
.getByText("Alpha Project")
.closest(".project-card") as HTMLElement | null;
if (!alphaCard) throw new Error("Card not found");
fireEvent.click(within(alphaCard).getByRole("button", { name: /delete/i }));
const dialog = screen.getByRole("dialog");
expect(dialog).toBeInTheDocument();
expect(within(dialog).getByText(/delete workspace/i)).toBeInTheDocument();
expect(within(dialog).getByText(/are you sure/i)).toBeInTheDocument();
fireEvent.click(within(dialog).getByRole("button", { name: /delete/i }));
await waitFor(() => {
expect(deleteMock).toHaveBeenCalledWith("proj-1");
});
expect(listMock).toHaveBeenCalledTimes(2);
});
});