diff --git a/web-ui/__tests__/components/AgentCard.test.tsx b/web-ui/__tests__/components/AgentCard.test.tsx index ca8a091d..287518e0 100644 --- a/web-ui/__tests__/components/AgentCard.test.tsx +++ b/web-ui/__tests__/components/AgentCard.test.tsx @@ -1,56 +1,440 @@ /** * AgentCard Component Tests - * Feature: 013-context-panel-integration (Phase 6) + * Consolidated from: + * - src/components/AgentCard.test.tsx (cf-8ip: Phase 5.1) + * - __tests__/components/AgentCard.test.tsx (013-context-panel-integration Phase 6) */ +import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; import AgentCard, { Agent } from '@/components/AgentCard'; -describe('AgentCard - Navigation (User Story 4)', () => { - const mockAgent: Agent = { - id: 'agent-001', - type: 'backend', - status: 'busy', - tasksCompleted: 5, - }; - - /** - * T028 [P] [US4]: AgentCard accepts onClick prop and calls it - * RED: This test will FAIL until onAgentClick prop is verified - */ - it('accepts onAgentClick prop and calls it when clicked', () => { - const onAgentClickMock = jest.fn(); - const { container } = render( - - ); - - const card = container.firstChild as HTMLElement; - fireEvent.click(card); - - expect(onAgentClickMock).toHaveBeenCalledTimes(1); - expect(onAgentClickMock).toHaveBeenCalledWith('agent-001'); - }); - - /** - * T031 [P] [US4]: AgentCard shows cursor-pointer when clickable - * RED: This test will FAIL until cursor-pointer class is applied - */ - it('shows cursor-pointer when onAgentClick provided', () => { - const onAgentClickMock = jest.fn(); - const { container } = render( - - ); - - const card = container.firstChild as HTMLElement; - expect(card).toHaveClass('cursor-pointer'); - }); - - it('renders without onClick callback', () => { - const { container } = render(); - - const card = container.firstChild as HTMLElement; - expect(card).toBeInTheDocument(); - // Should still have cursor-pointer since AgentCard is always clickable - expect(card).toHaveClass('cursor-pointer'); +describe('AgentCard Component', () => { + const mockOnAgentClick = jest.fn(); + + beforeEach(() => { + mockOnAgentClick.mockClear(); + }); + + describe('Display Agent Information', () => { + it('should display agent ID', () => { + const agent: Agent = { + id: 'backend-worker-001', + type: 'backend-worker', + status: 'idle', + tasksCompleted: 0, + }; + + render(); + + expect(screen.getByText('backend-worker-001')).toBeInTheDocument(); + }); + + it('should display formatted agent type', () => { + const agent: Agent = { + id: 'frontend-specialist-001', + type: 'frontend-specialist', + status: 'idle', + tasksCompleted: 5, + }; + + render(); + + expect(screen.getByText('Frontend Specialist')).toBeInTheDocument(); + }); + + it('should display tasks completed counter', () => { + const agent: Agent = { + id: 'test-engineer-001', + type: 'test-engineer', + status: 'busy', + currentTask: 15, + tasksCompleted: 7, + }; + + render(); + + expect(screen.getByText('Tasks Completed')).toBeInTheDocument(); + expect(screen.getByText('7')).toBeInTheDocument(); + }); + }); + + describe('Status Colors', () => { + it('should show green for idle status', () => { + const agent: Agent = { + id: 'agent-001', + type: 'backend', + status: 'idle', + tasksCompleted: 0, + }; + + const { container } = render(); + + const card = container.firstChild as HTMLElement; + expect(card).toHaveClass('bg-secondary', 'border-border', 'text-secondary-foreground'); + expect(screen.getByText('Idle')).toBeInTheDocument(); + }); + + it('should show yellow for busy status', () => { + const agent: Agent = { + id: 'agent-002', + type: 'backend', + status: 'busy', + currentTask: 10, + tasksCompleted: 3, + }; + + const { container } = render(); + + const card = container.firstChild as HTMLElement; + expect(card).toHaveClass('bg-primary/20', 'border-border', 'text-foreground'); + expect(screen.getByText('Working')).toBeInTheDocument(); + }); + + it('should show red for blocked status', () => { + const agent: Agent = { + id: 'agent-003', + type: 'backend', + status: 'blocked', + blockedBy: [5, 6], + tasksCompleted: 1, + }; + + const { container } = render(); + + const card = container.firstChild as HTMLElement; + expect(card).toHaveClass('bg-destructive/10', 'border-destructive', 'text-destructive-foreground'); + expect(screen.getByText('Blocked')).toBeInTheDocument(); + }); + }); + + describe('Current Task Display', () => { + it('should show current task when agent is busy', () => { + const agent: Agent = { + id: 'agent-004', + type: 'backend-worker', + status: 'busy', + currentTask: 42, + tasksCompleted: 5, + }; + + render(); + + expect(screen.getByText('Current Task:')).toBeInTheDocument(); + expect(screen.getByText('Task #42')).toBeInTheDocument(); + }); + + it('should NOT show current task when agent is idle', () => { + const agent: Agent = { + id: 'agent-005', + type: 'backend', + status: 'idle', + tasksCompleted: 2, + }; + + render(); + + expect(screen.queryByText('Current Task:')).not.toBeInTheDocument(); + expect(screen.getByText('Ready for work')).toBeInTheDocument(); + }); + + it('should NOT show current task when agent is blocked', () => { + const agent: Agent = { + id: 'agent-006', + type: 'frontend', + status: 'blocked', + blockedBy: [10], + tasksCompleted: 3, + }; + + render(); + + expect(screen.queryByText('Current Task:')).not.toBeInTheDocument(); + }); + }); + + describe('Blocked Status Display', () => { + it('should show blocked by information when blocked by single task', () => { + const agent: Agent = { + id: 'agent-007', + type: 'test', + status: 'blocked', + blockedBy: [15], + tasksCompleted: 1, + }; + + render(); + + expect(screen.getByText('Blocked By:')).toBeInTheDocument(); + expect(screen.getByText('Task #15')).toBeInTheDocument(); + }); + + it('should show count when blocked by multiple tasks', () => { + const agent: Agent = { + id: 'agent-008', + type: 'backend', + status: 'blocked', + blockedBy: [10, 11, 12], + tasksCompleted: 2, + }; + + render(); + + expect(screen.getByText('Blocked By:')).toBeInTheDocument(); + expect(screen.getByText('3 tasks')).toBeInTheDocument(); + }); + + it('should NOT show blocked by section when not blocked', () => { + const agent: Agent = { + id: 'agent-009', + type: 'backend', + status: 'idle', + tasksCompleted: 0, + }; + + render(); + + expect(screen.queryByText('Blocked By:')).not.toBeInTheDocument(); + }); + }); + + describe('Agent Type Badges', () => { + it('should show backend badge with correct icon', () => { + const agent: Agent = { + id: 'agent-010', + type: 'backend-worker', + status: 'idle', + tasksCompleted: 0, + }; + + render(); + + const badge = screen.getByText('Backend Worker').parentElement; + expect(badge).toHaveClass('bg-primary/10', 'text-primary-foreground'); + expect(screen.getByText('โš™๏ธ')).toBeInTheDocument(); + }); + + it('should show frontend badge with correct icon', () => { + const agent: Agent = { + id: 'agent-011', + type: 'frontend-specialist', + status: 'idle', + tasksCompleted: 0, + }; + + render(); + + const badge = screen.getByText('Frontend Specialist').parentElement; + expect(badge).toHaveClass('bg-secondary', 'text-secondary-foreground'); + expect(screen.getByText('๐ŸŽจ')).toBeInTheDocument(); + }); + + it('should show test badge with correct icon', () => { + const agent: Agent = { + id: 'agent-012', + type: 'test-engineer', + status: 'idle', + tasksCompleted: 0, + }; + + render(); + + const badge = screen.getByText('Test Engineer').parentElement; + expect(badge).toHaveClass('bg-secondary', 'text-secondary-foreground'); + expect(screen.getByText('๐Ÿงช')).toBeInTheDocument(); + }); + + it('should show default badge for unknown agent type', () => { + const agent: Agent = { + id: 'agent-013', + type: 'custom-agent', + status: 'idle', + tasksCompleted: 0, + }; + + render(); + + const badge = screen.getByText('Custom Agent').parentElement; + expect(badge).toHaveClass('bg-muted', 'text-foreground'); + expect(screen.getByText('๐Ÿค–')).toBeInTheDocument(); + }); + }); + + describe('Click Interaction', () => { + it('should call onAgentClick when card is clicked', () => { + const agent: Agent = { + id: 'agent-014', + type: 'backend', + status: 'idle', + tasksCompleted: 0, + }; + + const { container } = render(); + + const card = container.firstChild as HTMLElement; + fireEvent.click(card); + + expect(mockOnAgentClick).toHaveBeenCalledTimes(1); + expect(mockOnAgentClick).toHaveBeenCalledWith('agent-014'); + }); + + it('should handle optional onAgentClick callback', () => { + const agent: Agent = { + id: 'agent-015', + type: 'backend', + status: 'idle', + tasksCompleted: 0, + }; + + const { container } = render(); + + const card = container.firstChild as HTMLElement; + // Should not throw error when onAgentClick is undefined + expect(() => fireEvent.click(card)).not.toThrow(); + }); + + it('shows cursor-pointer when onAgentClick provided', () => { + const agent: Agent = { + id: 'agent-001', + type: 'backend', + status: 'busy', + tasksCompleted: 5, + }; + const onAgentClickMock = jest.fn(); + const { container } = render( + + ); + + const card = container.firstChild as HTMLElement; + expect(card).toHaveClass('cursor-pointer'); + }); + + it('renders without onClick callback', () => { + const agent: Agent = { + id: 'agent-001', + type: 'backend', + status: 'busy', + tasksCompleted: 5, + }; + const { container } = render(); + + const card = container.firstChild as HTMLElement; + expect(card).toBeInTheDocument(); + // Should still have cursor-pointer since AgentCard is always clickable + expect(card).toHaveClass('cursor-pointer'); + }); + }); + + describe('Status Indicator', () => { + it('should show animated pulse dot for all statuses', () => { + const statuses: Array<'idle' | 'busy' | 'blocked'> = ['idle', 'busy', 'blocked']; + + statuses.forEach((status) => { + const agent: Agent = { + id: `agent-${status}`, + type: 'backend', + status, + tasksCompleted: 0, + }; + + const { container } = render(); + + const dot = container.querySelector('.animate-pulse'); + expect(dot).toBeInTheDocument(); + expect(dot).toHaveClass('w-3', 'h-3', 'rounded-full'); + }); + }); + }); + + describe('Responsive Design', () => { + it('should have proper styling classes for responsiveness', () => { + const agent: Agent = { + id: 'agent-016', + type: 'backend', + status: 'idle', + tasksCompleted: 0, + }; + + const { container } = render(); + + const card = container.firstChild as HTMLElement; + expect(card).toHaveClass('rounded-lg', 'border-2', 'p-4'); + expect(card).toHaveClass('transition-all', 'duration-200'); + expect(card).toHaveClass('hover:shadow-sm', 'cursor-pointer'); + }); + + it('should truncate long agent IDs', () => { + const agent: Agent = { + id: 'very-long-agent-identifier-that-should-be-truncated', + type: 'backend', + status: 'idle', + tasksCompleted: 0, + }; + + render(); + + const agentIdElement = screen.getByText(agent.id); + expect(agentIdElement).toHaveClass('truncate', 'max-w-[150px]'); + expect(agentIdElement).toHaveAttribute('title', agent.id); + }); + }); + + describe('Edge Cases', () => { + it('should handle zero tasks completed', () => { + const agent: Agent = { + id: 'agent-017', + type: 'backend', + status: 'idle', + tasksCompleted: 0, + }; + + render(); + + expect(screen.getByText('0')).toBeInTheDocument(); + }); + + it('should handle large tasks completed number', () => { + const agent: Agent = { + id: 'agent-018', + type: 'backend', + status: 'idle', + tasksCompleted: 9999, + }; + + render(); + + expect(screen.getByText('9999')).toBeInTheDocument(); + }); + + it('should handle undefined currentTask gracefully', () => { + const agent: Agent = { + id: 'agent-019', + type: 'backend', + status: 'busy', + tasksCompleted: 5, + // currentTask is undefined + }; + + render(); + + // Should not show current task section when undefined + expect(screen.queryByText('Current Task:')).not.toBeInTheDocument(); + }); + + it('should handle empty blockedBy array', () => { + const agent: Agent = { + id: 'agent-020', + type: 'backend', + status: 'blocked', + blockedBy: [], + tasksCompleted: 2, + }; + + render(); + + // Should not show blocked by section when array is empty + expect(screen.queryByText('Blocked By:')).not.toBeInTheDocument(); + }); }); }); diff --git a/web-ui/src/components/__tests__/DiscoveryProgress.test.tsx b/web-ui/__tests__/components/DiscoveryProgress.test.tsx similarity index 99% rename from web-ui/src/components/__tests__/DiscoveryProgress.test.tsx rename to web-ui/__tests__/components/DiscoveryProgress.test.tsx index 547f5fb4..f7bd6f89 100644 --- a/web-ui/src/components/__tests__/DiscoveryProgress.test.tsx +++ b/web-ui/__tests__/components/DiscoveryProgress.test.tsx @@ -1,10 +1,10 @@ /** * Tests for DiscoveryProgress Component (cf-17.2) - * TDD RED Phase - Write tests first + * Migrated from src/components/__tests__/DiscoveryProgress.test.tsx */ import { render, screen, waitFor, fireEvent } from '@testing-library/react'; -import DiscoveryProgress from '../DiscoveryProgress'; +import DiscoveryProgress from '@/components/DiscoveryProgress'; import { projectsApi } from '@/lib/api'; import type { DiscoveryProgressResponse } from '@/types/api'; @@ -33,7 +33,7 @@ import { authFetch } from '@/lib/api-client'; const mockAuthFetch = authFetch as jest.MockedFunction; // Mock child components -jest.mock('../ProgressBar', () => { +jest.mock('@/components/ProgressBar', () => { return function MockProgressBar({ percentage, label }: { percentage: number; label?: string }) { return (
@@ -44,7 +44,7 @@ jest.mock('../ProgressBar', () => { }; }); -jest.mock('../PhaseIndicator', () => { +jest.mock('@/components/PhaseIndicator', () => { return function MockPhaseIndicator({ phase }: { phase: string }) { return {phase}; }; diff --git a/web-ui/src/components/PRDModal.test.tsx b/web-ui/__tests__/components/PRDModal.test.tsx similarity index 98% rename from web-ui/src/components/PRDModal.test.tsx rename to web-ui/__tests__/components/PRDModal.test.tsx index ecb95b51..0c0fe43b 100644 --- a/web-ui/src/components/PRDModal.test.tsx +++ b/web-ui/__tests__/components/PRDModal.test.tsx @@ -1,11 +1,11 @@ /** * Tests for PRDModal Component - * TDD: RED phase - These tests should fail initially + * Migrated from src/components/PRDModal.test.tsx */ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import PRDModal from './PRDModal'; +import PRDModal from '@/components/PRDModal'; import type { PRDResponse } from '@/types/api'; describe('PRDModal', () => { diff --git a/web-ui/src/components/__tests__/PhaseIndicator.test.tsx b/web-ui/__tests__/components/PhaseIndicator.test.tsx similarity index 98% rename from web-ui/src/components/__tests__/PhaseIndicator.test.tsx rename to web-ui/__tests__/components/PhaseIndicator.test.tsx index dce427cd..21996df9 100644 --- a/web-ui/src/components/__tests__/PhaseIndicator.test.tsx +++ b/web-ui/__tests__/components/PhaseIndicator.test.tsx @@ -1,10 +1,10 @@ /** * Tests for PhaseIndicator Component (cf-17.2) - * TDD RED Phase - Write tests first + * Migrated from src/components/__tests__/PhaseIndicator.test.tsx */ import { render, screen } from '@testing-library/react'; -import PhaseIndicator from '../PhaseIndicator'; +import PhaseIndicator from '@/components/PhaseIndicator'; describe('PhaseIndicator Component', () => { describe('Phase Text Display', () => { diff --git a/web-ui/src/components/__tests__/ProgressBar.test.tsx b/web-ui/__tests__/components/ProgressBar.test.tsx similarity index 98% rename from web-ui/src/components/__tests__/ProgressBar.test.tsx rename to web-ui/__tests__/components/ProgressBar.test.tsx index e6a8d5fc..bac26c19 100644 --- a/web-ui/src/components/__tests__/ProgressBar.test.tsx +++ b/web-ui/__tests__/components/ProgressBar.test.tsx @@ -1,10 +1,10 @@ /** * Tests for ProgressBar Component (cf-17.2) - * TDD RED Phase - Write tests first + * Migrated from src/components/__tests__/ProgressBar.test.tsx */ import { render, screen } from '@testing-library/react'; -import ProgressBar from '../ProgressBar'; +import ProgressBar from '@/components/ProgressBar'; describe('ProgressBar Component', () => { describe('Rendering and Width', () => { diff --git a/web-ui/src/components/__tests__/ProjectCreationForm.test.tsx b/web-ui/__tests__/components/ProjectCreationForm.test.tsx similarity index 99% rename from web-ui/src/components/__tests__/ProjectCreationForm.test.tsx rename to web-ui/__tests__/components/ProjectCreationForm.test.tsx index 2e970cf6..6495d3f5 100644 --- a/web-ui/src/components/__tests__/ProjectCreationForm.test.tsx +++ b/web-ui/__tests__/components/ProjectCreationForm.test.tsx @@ -1,13 +1,13 @@ /** * Tests for ProjectCreationForm Component * Feature: 011-project-creation-flow - * Sprint: 9.5 - Critical UX Fixes + * Migrated from src/components/__tests__/ProjectCreationForm.test.tsx */ import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import ProjectCreationForm from '../ProjectCreationForm'; +import ProjectCreationForm from '@/components/ProjectCreationForm'; // Create mock functions const mockPost = jest.fn(); diff --git a/web-ui/src/components/__tests__/Spinner.test.tsx b/web-ui/__tests__/components/Spinner.test.tsx similarity index 94% rename from web-ui/src/components/__tests__/Spinner.test.tsx rename to web-ui/__tests__/components/Spinner.test.tsx index 9de2882a..ea2a2985 100644 --- a/web-ui/src/components/__tests__/Spinner.test.tsx +++ b/web-ui/__tests__/components/Spinner.test.tsx @@ -1,12 +1,12 @@ /** * Tests for Spinner Component * Feature: 011-project-creation-flow (User Story 5) - * Sprint: 9.5 - Critical UX Fixes + * Migrated from src/components/__tests__/Spinner.test.tsx */ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { Spinner } from '../Spinner'; +import { Spinner } from '@/components/Spinner'; describe('Spinner', () => { test('renders with default medium size', () => { diff --git a/web-ui/src/components/TaskTreeView.test.tsx b/web-ui/__tests__/components/TaskTreeView.test.tsx similarity index 99% rename from web-ui/src/components/TaskTreeView.test.tsx rename to web-ui/__tests__/components/TaskTreeView.test.tsx index fc09ef93..b3d340b3 100644 --- a/web-ui/src/components/TaskTreeView.test.tsx +++ b/web-ui/__tests__/components/TaskTreeView.test.tsx @@ -1,15 +1,15 @@ /** * Tests for TaskTreeView Component - * TDD: RED phase - These tests should fail initially + * Migrated from src/components/TaskTreeView.test.tsx */ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import TaskTreeView from './TaskTreeView'; +import TaskTreeView from '@/components/TaskTreeView'; import type { Issue, Task } from '@/types/api'; // Mock QualityGateStatus component to avoid async issues in tests -jest.mock('./quality-gates/QualityGateStatus', () => { +jest.mock('@/components/quality-gates/QualityGateStatus', () => { return function QualityGateStatus() { return 'Quality Gate Status Mock'; }; @@ -208,7 +208,7 @@ describe('TaskTreeView', () => { // Use getAllByText since task numbers appear multiple times const task001Elements = screen.getAllByText(/T-001/i); expect(task001Elements.length).toBeGreaterThan(0); - + const task002Elements = screen.getAllByText(/T-002/i); expect(task002Elements.length).toBeGreaterThan(0); }); @@ -678,4 +678,4 @@ describe('TaskTreeView', () => { expect(blockedBadges.length).toBe(0); }); }); -}); \ No newline at end of file +}); diff --git a/web-ui/src/lib/__tests__/api.test.ts b/web-ui/__tests__/lib/api.test.ts similarity index 99% rename from web-ui/src/lib/__tests__/api.test.ts rename to web-ui/__tests__/lib/api.test.ts index 3714e876..d1f222da 100644 --- a/web-ui/src/lib/__tests__/api.test.ts +++ b/web-ui/__tests__/lib/api.test.ts @@ -1,6 +1,7 @@ /** * Tests for API client methods * Following TDD methodology - tests written BEFORE implementation + * Migrated from src/lib/__tests__/api.test.ts */ import type { ProjectResponse, StartProjectResponse } from '@/types'; @@ -40,7 +41,7 @@ jest.mock('axios', () => { }); // Now import the API module after mocking axios -import { projectsApi, blockersApi } from '../api'; +import { projectsApi, blockersApi } from '@/lib/api'; beforeEach(() => { jest.clearAllMocks(); diff --git a/web-ui/src/app/__tests__/page.test.tsx b/web-ui/src/app/__tests__/page.test.tsx deleted file mode 100644 index 9ed2ba98..00000000 --- a/web-ui/src/app/__tests__/page.test.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Tests for HomePage Component - * Feature: 011-project-creation-flow - * - * Tests the home page which displays the project list. - */ - -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { useRouter } from 'next/navigation'; -import HomePage from '../page'; - -// Mock Next.js router -jest.mock('next/navigation', () => ({ - useRouter: jest.fn(), -})); - -// Mock ProtectedRoute to pass through children -jest.mock('@/components/auth/ProtectedRoute', () => ({ - ProtectedRoute: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), -})); - -// Mock ProjectList component -jest.mock('@/components/ProjectList', () => { - return function MockProjectList() { - return
ProjectList Component
; - }; -}); - -describe('HomePage', () => { - const mockPush = jest.fn(); - - beforeEach(() => { - mockPush.mockClear(); - (useRouter as jest.Mock).mockReturnValue({ - push: mockPush, - }); - }); - - describe('Page Structure', () => { - test('renders wrapped in ProtectedRoute', () => { - render(); - - expect(screen.getByTestId('protected-route')).toBeInTheDocument(); - }); - - test('renders page heading', () => { - render(); - - expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Your Projects'); - }); - - test('renders tagline', () => { - render(); - - expect(screen.getByText(/ai coding agents that work autonomously while you sleep/i)).toBeInTheDocument(); - }); - - test('renders ProjectList component', () => { - render(); - - expect(screen.getByTestId('project-list')).toBeInTheDocument(); - }); - }); - - describe('Responsive Layout', () => { - test('has responsive container classes', () => { - const { container } = render(); - - const mainElement = container.querySelector('main'); - expect(mainElement).toHaveClass('min-h-screen', 'bg-background'); - }); - - test('has max-width container', () => { - const { container } = render(); - - const contentDiv = container.querySelector('.max-w-7xl'); - expect(contentDiv).toBeInTheDocument(); - }); - }); -}); diff --git a/web-ui/src/components/AgentCard.test.tsx b/web-ui/src/components/AgentCard.test.tsx deleted file mode 100644 index 2b622eb9..00000000 --- a/web-ui/src/components/AgentCard.test.tsx +++ /dev/null @@ -1,406 +0,0 @@ -/** - * Tests for AgentCard component (cf-8ip: Phase 5.1) - */ - -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import { AgentCard, Agent } from './AgentCard'; - -describe('AgentCard Component', () => { - const mockOnAgentClick = jest.fn(); - - beforeEach(() => { - mockOnAgentClick.mockClear(); - }); - - describe('Display Agent Information', () => { - it('should display agent ID', () => { - const agent: Agent = { - id: 'backend-worker-001', - type: 'backend-worker', - status: 'idle', - tasksCompleted: 0, - }; - - render(); - - expect(screen.getByText('backend-worker-001')).toBeInTheDocument(); - }); - - it('should display formatted agent type', () => { - const agent: Agent = { - id: 'frontend-specialist-001', - type: 'frontend-specialist', - status: 'idle', - tasksCompleted: 5, - }; - - render(); - - expect(screen.getByText('Frontend Specialist')).toBeInTheDocument(); - }); - - it('should display tasks completed counter', () => { - const agent: Agent = { - id: 'test-engineer-001', - type: 'test-engineer', - status: 'busy', - currentTask: 15, - tasksCompleted: 7, - }; - - render(); - - expect(screen.getByText('Tasks Completed')).toBeInTheDocument(); - expect(screen.getByText('7')).toBeInTheDocument(); - }); - }); - - describe('Status Colors', () => { - it('should show green for idle status', () => { - const agent: Agent = { - id: 'agent-001', - type: 'backend', - status: 'idle', - tasksCompleted: 0, - }; - - const { container } = render(); - - const card = container.firstChild as HTMLElement; - expect(card).toHaveClass('bg-secondary', 'border-border', 'text-secondary-foreground'); - expect(screen.getByText('Idle')).toBeInTheDocument(); - }); - - it('should show yellow for busy status', () => { - const agent: Agent = { - id: 'agent-002', - type: 'backend', - status: 'busy', - currentTask: 10, - tasksCompleted: 3, - }; - - const { container } = render(); - - const card = container.firstChild as HTMLElement; - expect(card).toHaveClass('bg-primary/20', 'border-border', 'text-foreground'); - expect(screen.getByText('Working')).toBeInTheDocument(); - }); - - it('should show red for blocked status', () => { - const agent: Agent = { - id: 'agent-003', - type: 'backend', - status: 'blocked', - blockedBy: [5, 6], - tasksCompleted: 1, - }; - - const { container } = render(); - - const card = container.firstChild as HTMLElement; - expect(card).toHaveClass('bg-destructive/10', 'border-destructive', 'text-destructive-foreground'); - expect(screen.getByText('Blocked')).toBeInTheDocument(); - }); - }); - - describe('Current Task Display', () => { - it('should show current task when agent is busy', () => { - const agent: Agent = { - id: 'agent-004', - type: 'backend-worker', - status: 'busy', - currentTask: 42, - tasksCompleted: 5, - }; - - render(); - - expect(screen.getByText('Current Task:')).toBeInTheDocument(); - expect(screen.getByText('Task #42')).toBeInTheDocument(); - }); - - it('should NOT show current task when agent is idle', () => { - const agent: Agent = { - id: 'agent-005', - type: 'backend', - status: 'idle', - tasksCompleted: 2, - }; - - render(); - - expect(screen.queryByText('Current Task:')).not.toBeInTheDocument(); - expect(screen.getByText('Ready for work')).toBeInTheDocument(); - }); - - it('should NOT show current task when agent is blocked', () => { - const agent: Agent = { - id: 'agent-006', - type: 'frontend', - status: 'blocked', - blockedBy: [10], - tasksCompleted: 3, - }; - - render(); - - expect(screen.queryByText('Current Task:')).not.toBeInTheDocument(); - }); - }); - - describe('Blocked Status Display', () => { - it('should show blocked by information when blocked by single task', () => { - const agent: Agent = { - id: 'agent-007', - type: 'test', - status: 'blocked', - blockedBy: [15], - tasksCompleted: 1, - }; - - render(); - - expect(screen.getByText('Blocked By:')).toBeInTheDocument(); - expect(screen.getByText('Task #15')).toBeInTheDocument(); - }); - - it('should show count when blocked by multiple tasks', () => { - const agent: Agent = { - id: 'agent-008', - type: 'backend', - status: 'blocked', - blockedBy: [10, 11, 12], - tasksCompleted: 2, - }; - - render(); - - expect(screen.getByText('Blocked By:')).toBeInTheDocument(); - expect(screen.getByText('3 tasks')).toBeInTheDocument(); - }); - - it('should NOT show blocked by section when not blocked', () => { - const agent: Agent = { - id: 'agent-009', - type: 'backend', - status: 'idle', - tasksCompleted: 0, - }; - - render(); - - expect(screen.queryByText('Blocked By:')).not.toBeInTheDocument(); - }); - }); - - describe('Agent Type Badges', () => { - it('should show backend badge with correct icon', () => { - const agent: Agent = { - id: 'agent-010', - type: 'backend-worker', - status: 'idle', - tasksCompleted: 0, - }; - - render(); - - const badge = screen.getByText('Backend Worker').parentElement; - expect(badge).toHaveClass('bg-primary/10', 'text-primary-foreground'); - expect(screen.getByText('โš™๏ธ')).toBeInTheDocument(); - }); - - it('should show frontend badge with correct icon', () => { - const agent: Agent = { - id: 'agent-011', - type: 'frontend-specialist', - status: 'idle', - tasksCompleted: 0, - }; - - render(); - - const badge = screen.getByText('Frontend Specialist').parentElement; - expect(badge).toHaveClass('bg-secondary', 'text-secondary-foreground'); - expect(screen.getByText('๐ŸŽจ')).toBeInTheDocument(); - }); - - it('should show test badge with correct icon', () => { - const agent: Agent = { - id: 'agent-012', - type: 'test-engineer', - status: 'idle', - tasksCompleted: 0, - }; - - render(); - - const badge = screen.getByText('Test Engineer').parentElement; - expect(badge).toHaveClass('bg-secondary', 'text-secondary-foreground'); - expect(screen.getByText('๐Ÿงช')).toBeInTheDocument(); - }); - - it('should show default badge for unknown agent type', () => { - const agent: Agent = { - id: 'agent-013', - type: 'custom-agent', - status: 'idle', - tasksCompleted: 0, - }; - - render(); - - const badge = screen.getByText('Custom Agent').parentElement; - expect(badge).toHaveClass('bg-muted', 'text-foreground'); - expect(screen.getByText('๐Ÿค–')).toBeInTheDocument(); - }); - }); - - describe('Click Interaction', () => { - it('should call onAgentClick when card is clicked', () => { - const agent: Agent = { - id: 'agent-014', - type: 'backend', - status: 'idle', - tasksCompleted: 0, - }; - - const { container } = render(); - - const card = container.firstChild as HTMLElement; - fireEvent.click(card); - - expect(mockOnAgentClick).toHaveBeenCalledTimes(1); - expect(mockOnAgentClick).toHaveBeenCalledWith('agent-014'); - }); - - it('should handle optional onAgentClick callback', () => { - const agent: Agent = { - id: 'agent-015', - type: 'backend', - status: 'idle', - tasksCompleted: 0, - }; - - const { container } = render(); - - const card = container.firstChild as HTMLElement; - // Should not throw error when onAgentClick is undefined - expect(() => fireEvent.click(card)).not.toThrow(); - }); - }); - - describe('Status Indicator', () => { - it('should show animated pulse dot for all statuses', () => { - const statuses: Array<'idle' | 'busy' | 'blocked'> = ['idle', 'busy', 'blocked']; - - statuses.forEach((status) => { - const agent: Agent = { - id: `agent-${status}`, - type: 'backend', - status, - tasksCompleted: 0, - }; - - const { container } = render(); - - const dot = container.querySelector('.animate-pulse'); - expect(dot).toBeInTheDocument(); - expect(dot).toHaveClass('w-3', 'h-3', 'rounded-full'); - }); - }); - }); - - describe('Responsive Design', () => { - it('should have proper styling classes for responsiveness', () => { - const agent: Agent = { - id: 'agent-016', - type: 'backend', - status: 'idle', - tasksCompleted: 0, - }; - - const { container } = render(); - - const card = container.firstChild as HTMLElement; - expect(card).toHaveClass('rounded-lg', 'border-2', 'p-4'); - expect(card).toHaveClass('transition-all', 'duration-200'); - expect(card).toHaveClass('hover:shadow-sm', 'cursor-pointer'); - }); - - it('should truncate long agent IDs', () => { - const agent: Agent = { - id: 'very-long-agent-identifier-that-should-be-truncated', - type: 'backend', - status: 'idle', - tasksCompleted: 0, - }; - - render(); - - const agentIdElement = screen.getByText(agent.id); - expect(agentIdElement).toHaveClass('truncate', 'max-w-[150px]'); - expect(agentIdElement).toHaveAttribute('title', agent.id); - }); - }); - - describe('Edge Cases', () => { - it('should handle zero tasks completed', () => { - const agent: Agent = { - id: 'agent-017', - type: 'backend', - status: 'idle', - tasksCompleted: 0, - }; - - render(); - - expect(screen.getByText('0')).toBeInTheDocument(); - }); - - it('should handle large tasks completed number', () => { - const agent: Agent = { - id: 'agent-018', - type: 'backend', - status: 'idle', - tasksCompleted: 9999, - }; - - render(); - - expect(screen.getByText('9999')).toBeInTheDocument(); - }); - - it('should handle undefined currentTask gracefully', () => { - const agent: Agent = { - id: 'agent-019', - type: 'backend', - status: 'busy', - tasksCompleted: 5, - // currentTask is undefined - }; - - render(); - - // Should not show current task section when undefined - expect(screen.queryByText('Current Task:')).not.toBeInTheDocument(); - }); - - it('should handle empty blockedBy array', () => { - const agent: Agent = { - id: 'agent-020', - type: 'backend', - status: 'blocked', - blockedBy: [], - tasksCompleted: 2, - }; - - render(); - - // Should not show blocked by section when array is empty - expect(screen.queryByText('Blocked By:')).not.toBeInTheDocument(); - }); - }); -}); diff --git a/web-ui/src/components/__tests__/ProjectList.test.tsx b/web-ui/src/components/__tests__/ProjectList.test.tsx deleted file mode 100644 index da803381..00000000 --- a/web-ui/src/components/__tests__/ProjectList.test.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { SWRConfig } from 'swr'; -import { useRouter } from 'next/navigation'; -import ProjectList from '@/components/ProjectList'; -import { projectsApi } from '@/lib/api'; - -// Mock Next.js router -jest.mock('next/navigation', () => ({ - useRouter: jest.fn(), -})); - -// Mock projectsApi -jest.mock('@/lib/api', () => ({ - projectsApi: { - list: jest.fn(), - startProject: jest.fn(), - }, -})); - -// Mock ProjectCreationForm component -jest.mock('@/components/ProjectCreationForm', () => { - return function MockProjectCreationForm({ - onSuccess, - onSubmit, - }: { - onSuccess: (projectId: number) => void; - onSubmit?: () => void; - onError?: () => void; - }) { - return ( -
- -
- ); - }; -}); - -// Mock Spinner component -jest.mock('@/components/Spinner', () => ({ - Spinner: ({ size }: { size: string }) => ( -
Loading...
- ), -})); - -// Mock Hugeicons -jest.mock('@hugeicons/react', () => ({ - Add01Icon: ({ className }: { className?: string }) => ( - - ), -})); - -// Helper to render with SWR wrapper -const renderWithSWR = (component: React.ReactElement) => { - return render( - new Map(), dedupingInterval: 0 }}> - {component} - - ); -}; - -describe('ProjectList', () => { - const mockPush = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - (useRouter as jest.Mock).mockReturnValue({ - push: mockPush, - }); - (projectsApi.startProject as jest.Mock).mockResolvedValue({}); - }); - - test('shows loading state while fetching projects', () => { - (projectsApi.list as jest.Mock).mockImplementation( - () => new Promise(() => {}) // Never resolves - ); - - renderWithSWR(); - - expect(screen.getByText(/loading/i)).toBeInTheDocument(); - }); - - test('renders project cards with correct data (name, status, phase, date)', async () => { - const mockProjects = [ - { - id: 1, - name: 'Project A', - status: 'init', - phase: 'discovery', - created_at: '2025-01-15T10:00:00Z', - }, - { - id: 2, - name: 'Project B', - status: 'running', - phase: 'planning', - created_at: '2025-01-14T09:00:00Z', - }, - ]; - - (projectsApi.list as jest.Mock).mockResolvedValue({ - data: { projects: mockProjects }, - }); - - renderWithSWR(); - - await waitFor(() => { - expect(screen.getByText('Project A')).toBeInTheDocument(); - }); - - expect(screen.getByText('Project B')).toBeInTheDocument(); - // Check for status and phase values (text is split across elements) - const allText = screen.getByText('Project A').closest('div')!.textContent; - expect(allText).toContain('Status:'); - expect(allText).toContain('init'); - expect(allText).toContain('Phase:'); - expect(allText).toContain('discovery'); - - const projectBText = screen.getByText('Project B').closest('div')!.textContent; - expect(projectBText).toContain('running'); - expect(projectBText).toContain('planning'); - }); - - test('navigates to project page when card is clicked', async () => { - const mockProjects = [ - { - id: 1, - name: 'Project A', - status: 'init', - phase: 'discovery', - created_at: '2025-01-15T10:00:00Z', - }, - ]; - - (projectsApi.list as jest.Mock).mockResolvedValue({ - data: { projects: mockProjects }, - }); - - renderWithSWR(); - - await waitFor(() => { - expect(screen.getByText('Project A')).toBeInTheDocument(); - }); - - const projectCard = screen.getByText('Project A').closest('div'); - await userEvent.click(projectCard!); - - expect(mockPush).toHaveBeenCalledWith('/projects/1'); - }); - - test('shows empty state when no projects exist', async () => { - (projectsApi.list as jest.Mock).mockResolvedValue({ - data: { projects: [] }, - }); - - renderWithSWR(); - - await waitFor(() => { - expect(screen.getByText(/No projects yet/i)).toBeInTheDocument(); - }); - - // Also check for the CTA text - expect(screen.getByText(/Create your first project/i)).toBeInTheDocument(); - }); - - test('shows "Create New Project" button', async () => { - (projectsApi.list as jest.Mock).mockResolvedValue({ - data: { projects: [] }, - }); - - renderWithSWR(); - - await waitFor(() => { - expect(screen.getByTestId('create-project-button')).toBeInTheDocument(); - }); - }); - - test('shows ProjectCreationForm when Create button is clicked', async () => { - (projectsApi.list as jest.Mock).mockResolvedValue({ - data: { projects: [] }, - }); - - renderWithSWR(); - - await waitFor(() => { - expect(screen.getByTestId('create-project-button')).toBeInTheDocument(); - }); - - const createButton = screen.getByTestId('create-project-button'); - await userEvent.click(createButton); - - expect(screen.getByTestId('project-creation-form')).toBeInTheDocument(); - }); - - test('navigates to project dashboard after creation', async () => { - (projectsApi.list as jest.Mock).mockResolvedValue({ - data: { projects: [] }, - }); - - renderWithSWR(); - - await waitFor(() => { - expect(screen.getByTestId('create-project-button')).toBeInTheDocument(); - }); - - // Show form - const createButton = screen.getByTestId('create-project-button'); - await userEvent.click(createButton); - - expect(screen.getByTestId('project-creation-form')).toBeInTheDocument(); - - // Submit form - const submitButton = screen.getByText('Submit Form'); - await userEvent.click(submitButton); - - // Should navigate to project dashboard - await waitFor(() => { - expect(mockPush).toHaveBeenCalledWith('/projects/3'); - }); - }); - - test('starts discovery after project creation', async () => { - (projectsApi.list as jest.Mock).mockResolvedValue({ - data: { projects: [] }, - }); - - renderWithSWR(); - - await waitFor(() => { - expect(screen.getByTestId('create-project-button')).toBeInTheDocument(); - }); - - // Show form and submit - const createButton = screen.getByTestId('create-project-button'); - await userEvent.click(createButton); - - const submitButton = screen.getByText('Submit Form'); - await userEvent.click(submitButton); - - // Should call startProject - await waitFor(() => { - expect(projectsApi.startProject).toHaveBeenCalledWith(3); - }); - }); - - test('shows error state if fetch fails', async () => { - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); - - (projectsApi.list as jest.Mock).mockRejectedValue( - new Error('Failed to fetch projects') - ); - - renderWithSWR(); - - await waitFor(() => { - expect( - screen.getByText(/Failed to load projects/i) - ).toBeInTheDocument(); - }); - - consoleErrorSpy.mockRestore(); - }); - - test('formats created_at date in readable format', async () => { - const mockProjects = [ - { - id: 1, - name: 'Project A', - status: 'init', - phase: 'discovery', - created_at: '2025-01-15T10:00:00Z', - }, - ]; - - (projectsApi.list as jest.Mock).mockResolvedValue({ - data: { projects: mockProjects }, - }); - - renderWithSWR(); - - await waitFor(() => { - expect(screen.getByText(/January 15, 2025/)).toBeInTheDocument(); - }); - }); -});