b21456093e
- 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
210 lines
6.2 KiB
TypeScript
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);
|
|
});
|
|
});
|