Files
syndarix/frontend/tests/components/projects/ProjectCard.test.tsx
Felipe Cardoso 731a188a76 feat(frontend): wire useProjects hook to SDK and enhance MSW handlers
- Regenerate API SDK with 77 endpoints (up from 61)
- Update useProjects hook to use SDK's listProjects function
- Add comprehensive project mock data for demo mode
- Add project CRUD handlers to MSW overrides
- Map API response to frontend ProjectListItem format
- Fix test files with required slug and autonomyLevel properties

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 02:22:44 +01:00

108 lines
3.7 KiB
TypeScript

/**
* ProjectCard Component Tests
*/
import { render, screen, fireEvent } from '@testing-library/react';
import { ProjectCard, ProjectCardSkeleton } from '@/components/projects/ProjectCard';
import type { ProjectListItem } from '@/lib/api/hooks/useProjects';
describe('ProjectCard', () => {
const mockProject: ProjectListItem = {
id: 'proj-1',
name: 'Test Project',
slug: 'test-project',
description: 'This is a test project description',
status: 'active',
complexity: 'medium',
progress: 65,
openIssues: 5,
activeAgents: 3,
currentSprint: 'Sprint 2',
lastActivity: '5 minutes ago',
createdAt: '2025-01-01T00:00:00Z',
owner: { id: 'user-1', name: 'Test User' },
tags: ['frontend', 'react', 'typescript'],
autonomyLevel: 'milestone',
};
it('renders project name', () => {
render(<ProjectCard project={mockProject} />);
expect(screen.getByText('Test Project')).toBeInTheDocument();
});
it('renders project description', () => {
render(<ProjectCard project={mockProject} />);
expect(screen.getByText('This is a test project description')).toBeInTheDocument();
});
it('renders project status badge', () => {
render(<ProjectCard project={mockProject} />);
expect(screen.getByText('Active')).toBeInTheDocument();
});
it('renders current sprint', () => {
render(<ProjectCard project={mockProject} />);
// Sprint not shown in current card design, but progress is
expect(screen.getByText('65%')).toBeInTheDocument();
});
it('renders project metrics', () => {
render(<ProjectCard project={mockProject} />);
expect(screen.getByText('3')).toBeInTheDocument(); // agents
expect(screen.getByText('5')).toBeInTheDocument(); // issues
});
it('renders last activity time', () => {
render(<ProjectCard project={mockProject} />);
expect(screen.getByText('5 minutes ago')).toBeInTheDocument();
});
it('renders project tags', () => {
render(<ProjectCard project={mockProject} />);
expect(screen.getByText('frontend')).toBeInTheDocument();
expect(screen.getByText('react')).toBeInTheDocument();
expect(screen.getByText('typescript')).toBeInTheDocument();
});
it('calls onClick when card is clicked', () => {
const onClick = jest.fn();
render(<ProjectCard project={mockProject} onClick={onClick} />);
// Click the card (first button, which is the card itself)
const buttons = screen.getAllByRole('button');
fireEvent.click(buttons[0]);
expect(onClick).toHaveBeenCalledTimes(1);
});
it('renders action menu when onAction is provided', () => {
const onAction = jest.fn();
render(<ProjectCard project={mockProject} onAction={onAction} />);
// Menu button should exist with sr-only text
const menuButtons = screen.getAllByRole('button');
const menuButton = menuButtons.find((btn) => btn.querySelector('.sr-only'));
expect(menuButton).toBeDefined();
expect(menuButton!.querySelector('.sr-only')).toHaveTextContent('Project actions');
});
it('does not render action menu when onAction is not provided', () => {
render(<ProjectCard project={mockProject} />);
// Only the card itself should be a button
const buttons = screen.getAllByRole('button');
expect(buttons.length).toBe(1);
});
it('applies custom className', () => {
const { container } = render(<ProjectCard project={mockProject} className="custom-class" />);
expect(container.querySelector('.custom-class')).toBeInTheDocument();
});
});
describe('ProjectCardSkeleton', () => {
it('renders skeleton elements', () => {
const { container } = render(<ProjectCardSkeleton />);
expect(container.querySelectorAll('.animate-pulse').length).toBeGreaterThan(0);
});
});