forked from cardosofelipe/fast-next-template
- Replaced `next/navigation` with `@/lib/i18n/routing` across components, pages, and tests. - Removed redundant `locale` props from `ProjectWizard` and related pages. - Updated navigation to exclude explicit `locale` in paths. - Refactored tests to use mocks from `next-intl/navigation`.
453 lines
16 KiB
TypeScript
453 lines
16 KiB
TypeScript
/**
|
|
* ProjectDashboard Component Tests
|
|
*
|
|
* Comprehensive tests for the main project dashboard component.
|
|
*/
|
|
|
|
import { render, screen } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { ProjectDashboard } from '@/components/projects/ProjectDashboard';
|
|
import { EventType, type ProjectEvent, type ConnectionState } from '@/lib/types/events';
|
|
|
|
// Mock child components to isolate ProjectDashboard testing
|
|
jest.mock('@/components/projects/ProjectHeader', () => ({
|
|
ProjectHeader: jest.fn(({ project, onSettings }) => (
|
|
<div data-testid="mock-project-header">
|
|
<span>{project.name}</span>
|
|
<button onClick={onSettings}>Settings</button>
|
|
</div>
|
|
)),
|
|
}));
|
|
|
|
jest.mock('@/components/projects/AgentPanel', () => ({
|
|
AgentPanel: jest.fn(({ agents, onManageAgents, onAgentAction }) => (
|
|
<div data-testid="mock-agent-panel">
|
|
<span>Agents: {agents.length}</span>
|
|
<button onClick={onManageAgents}>Manage Agents</button>
|
|
<button onClick={() => onAgentAction('agent-001', 'view')}>View Agent</button>
|
|
</div>
|
|
)),
|
|
}));
|
|
|
|
jest.mock('@/components/projects/SprintProgress', () => ({
|
|
SprintProgress: jest.fn(({ sprint }) => (
|
|
<div data-testid="mock-sprint-progress">
|
|
<span>{sprint.name}</span>
|
|
</div>
|
|
)),
|
|
}));
|
|
|
|
jest.mock('@/components/projects/IssueSummary', () => ({
|
|
IssueSummary: jest.fn(({ summary, onViewAllIssues }) => (
|
|
<div data-testid="mock-issue-summary">
|
|
<span>Total: {summary.total}</span>
|
|
<button onClick={onViewAllIssues}>View Issues</button>
|
|
</div>
|
|
)),
|
|
}));
|
|
|
|
jest.mock('@/components/projects/RecentActivity', () => ({
|
|
RecentActivity: jest.fn(({ activities, onViewAll, onActionClick }) => (
|
|
<div data-testid="mock-recent-activity">
|
|
<span>Activities: {activities.length}</span>
|
|
<button onClick={onViewAll}>View All</button>
|
|
<button onClick={() => onActionClick('act-001')}>Action Click</button>
|
|
</div>
|
|
)),
|
|
}));
|
|
|
|
jest.mock('@/components/events/ConnectionStatus', () => ({
|
|
ConnectionStatus: jest.fn(({ state, onReconnect }) => (
|
|
<div data-testid="mock-connection-status">
|
|
<span>State: {state}</span>
|
|
<button onClick={onReconnect}>Reconnect</button>
|
|
</div>
|
|
)),
|
|
}));
|
|
|
|
// Mock useProjectEvents hook
|
|
const mockReconnect = jest.fn();
|
|
const mockDisconnect = jest.fn();
|
|
const mockClearEvents = jest.fn();
|
|
|
|
const mockUseProjectEventsDefault = {
|
|
events: [] as ProjectEvent[],
|
|
isConnected: true,
|
|
connectionState: 'connected' as ConnectionState,
|
|
error: null as {
|
|
message: string;
|
|
timestamp: string;
|
|
code?: string;
|
|
retryAttempt?: number;
|
|
} | null,
|
|
retryCount: 0,
|
|
reconnect: mockReconnect,
|
|
disconnect: mockDisconnect,
|
|
clearEvents: mockClearEvents,
|
|
};
|
|
|
|
let mockUseProjectEventsResult = { ...mockUseProjectEventsDefault };
|
|
|
|
jest.mock('@/lib/hooks/useProjectEvents', () => ({
|
|
useProjectEvents: jest.fn(() => mockUseProjectEventsResult),
|
|
}));
|
|
|
|
// Import mock from next-intl/navigation mock (used by @/lib/i18n/routing)
|
|
import { mockPush } from 'next-intl/navigation';
|
|
|
|
describe('ProjectDashboard', () => {
|
|
const projectId = 'test-project-123';
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockUseProjectEventsResult = { ...mockUseProjectEventsDefault };
|
|
});
|
|
|
|
describe('Rendering', () => {
|
|
it('renders the dashboard with test id', () => {
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
expect(screen.getByTestId('project-dashboard')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders ProjectHeader component', () => {
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
expect(screen.getByTestId('mock-project-header')).toBeInTheDocument();
|
|
expect(screen.getByText('E-Commerce Platform Redesign')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders AgentPanel component', () => {
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
expect(screen.getByTestId('mock-agent-panel')).toBeInTheDocument();
|
|
expect(screen.getByText('Agents: 5')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders SprintProgress component', () => {
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
expect(screen.getByTestId('mock-sprint-progress')).toBeInTheDocument();
|
|
expect(screen.getByText('Sprint 3')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders IssueSummary component', () => {
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
expect(screen.getByTestId('mock-issue-summary')).toBeInTheDocument();
|
|
expect(screen.getByText('Total: 70')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders RecentActivity component', () => {
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
});
|
|
|
|
it('applies custom className', () => {
|
|
render(<ProjectDashboard projectId={projectId} className="custom-class" />);
|
|
expect(screen.getByTestId('project-dashboard')).toHaveClass('custom-class');
|
|
});
|
|
});
|
|
|
|
describe('Connection Status', () => {
|
|
it('does not show ConnectionStatus when connected', () => {
|
|
mockUseProjectEventsResult.connectionState = 'connected';
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
expect(screen.queryByTestId('mock-connection-status')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('shows ConnectionStatus when disconnected', () => {
|
|
mockUseProjectEventsResult.connectionState = 'disconnected';
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
expect(screen.getByTestId('mock-connection-status')).toBeInTheDocument();
|
|
expect(screen.getByText('State: disconnected')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows ConnectionStatus when connecting', () => {
|
|
mockUseProjectEventsResult.connectionState = 'connecting';
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
expect(screen.getByTestId('mock-connection-status')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows ConnectionStatus when error', () => {
|
|
mockUseProjectEventsResult.connectionState = 'error';
|
|
mockUseProjectEventsResult.error = {
|
|
message: 'Connection failed',
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
expect(screen.getByTestId('mock-connection-status')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Navigation callbacks', () => {
|
|
it('navigates to settings when Settings is clicked', async () => {
|
|
const user = userEvent.setup();
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
|
|
await user.click(screen.getByText('Settings'));
|
|
expect(mockPush).toHaveBeenCalledWith(`/projects/${projectId}/settings`);
|
|
});
|
|
|
|
it('navigates to agents page when Manage Agents is clicked', async () => {
|
|
const user = userEvent.setup();
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
|
|
await user.click(screen.getByText('Manage Agents'));
|
|
expect(mockPush).toHaveBeenCalledWith(`/projects/${projectId}/agents`);
|
|
});
|
|
|
|
it('navigates to issues page when View Issues is clicked', async () => {
|
|
const user = userEvent.setup();
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
|
|
await user.click(screen.getByText('View Issues'));
|
|
expect(mockPush).toHaveBeenCalledWith(`/projects/${projectId}/issues`);
|
|
});
|
|
|
|
it('navigates to activity page when View All activity is clicked', async () => {
|
|
const user = userEvent.setup();
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
|
|
await user.click(screen.getByText('View All'));
|
|
expect(mockPush).toHaveBeenCalledWith(`/projects/${projectId}/activity`);
|
|
});
|
|
});
|
|
|
|
describe('Action callbacks', () => {
|
|
it('handles agent action', async () => {
|
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
const user = userEvent.setup();
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
|
|
await user.click(screen.getByText('View Agent'));
|
|
expect(consoleSpy).toHaveBeenCalledWith('Agent action: view on agent-001');
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('handles activity action click', async () => {
|
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
const user = userEvent.setup();
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
|
|
await user.click(screen.getByText('Action Click'));
|
|
expect(consoleSpy).toHaveBeenCalledWith('Action clicked for activity: act-001');
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('SSE Events Integration', () => {
|
|
it('merges SSE events with mock activities', () => {
|
|
const sseEvent: ProjectEvent = {
|
|
id: 'sse-event-1',
|
|
type: EventType.AGENT_MESSAGE,
|
|
timestamp: new Date().toISOString(),
|
|
project_id: projectId,
|
|
actor_id: 'agent-001',
|
|
actor_type: 'agent',
|
|
payload: {
|
|
message: 'Test message',
|
|
agent_name: 'Test Agent',
|
|
},
|
|
};
|
|
|
|
mockUseProjectEventsResult.events = [sseEvent];
|
|
render(<ProjectDashboard projectId={projectId} />);
|
|
|
|
// RecentActivity should receive merged activities
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
// Test eventToActivity helper function behavior through component integration
|
|
describe('Event to Activity Conversion', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockUseProjectEventsResult = { ...mockUseProjectEventsDefault };
|
|
});
|
|
|
|
const createMockEvent = (
|
|
type: EventType,
|
|
payload: Record<string, unknown>,
|
|
actorType: 'agent' | 'system' | 'user' = 'agent'
|
|
): ProjectEvent => ({
|
|
id: `event-${Date.now()}`,
|
|
type,
|
|
timestamp: new Date().toISOString(),
|
|
project_id: 'test-project',
|
|
actor_id: actorType === 'system' ? null : 'actor-001',
|
|
actor_type: actorType,
|
|
payload,
|
|
});
|
|
|
|
it('handles AGENT_SPAWNED events', () => {
|
|
const event = createMockEvent(EventType.AGENT_SPAWNED, {
|
|
agent_name: 'Product Owner',
|
|
role: 'product_owner',
|
|
});
|
|
mockUseProjectEventsResult.events = [event];
|
|
render(<ProjectDashboard projectId="test" />);
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
});
|
|
|
|
it('handles AGENT_MESSAGE events', () => {
|
|
const event = createMockEvent(EventType.AGENT_MESSAGE, {
|
|
message: 'Task completed',
|
|
agent_name: 'Backend Engineer',
|
|
});
|
|
mockUseProjectEventsResult.events = [event];
|
|
render(<ProjectDashboard projectId="test" />);
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
});
|
|
|
|
it('handles AGENT_STATUS_CHANGED events', () => {
|
|
const event = createMockEvent(EventType.AGENT_STATUS_CHANGED, {
|
|
new_status: 'working',
|
|
agent_name: 'QA Engineer',
|
|
});
|
|
mockUseProjectEventsResult.events = [event];
|
|
render(<ProjectDashboard projectId="test" />);
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
});
|
|
|
|
it('handles ISSUE_CREATED events', () => {
|
|
const event = createMockEvent(EventType.ISSUE_CREATED, {
|
|
title: 'New Feature Request',
|
|
issue_id: 'issue-001',
|
|
});
|
|
mockUseProjectEventsResult.events = [event];
|
|
render(<ProjectDashboard projectId="test" />);
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
});
|
|
|
|
it('handles ISSUE_UPDATED events', () => {
|
|
const event = createMockEvent(EventType.ISSUE_UPDATED, {
|
|
issue_id: 'issue-001',
|
|
});
|
|
mockUseProjectEventsResult.events = [event];
|
|
render(<ProjectDashboard projectId="test" />);
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
});
|
|
|
|
it('handles APPROVAL_REQUESTED events', () => {
|
|
const event = createMockEvent(EventType.APPROVAL_REQUESTED, {
|
|
description: 'Please approve the design',
|
|
approval_id: 'approval-001',
|
|
});
|
|
mockUseProjectEventsResult.events = [event];
|
|
render(<ProjectDashboard projectId="test" />);
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
});
|
|
|
|
it('handles SPRINT_STARTED events', () => {
|
|
const event = createMockEvent(
|
|
EventType.SPRINT_STARTED,
|
|
{
|
|
sprint_name: 'Sprint 4',
|
|
},
|
|
'system'
|
|
);
|
|
mockUseProjectEventsResult.events = [event];
|
|
render(<ProjectDashboard projectId="test" />);
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
});
|
|
|
|
it('handles unknown event types gracefully', () => {
|
|
// Create an event with an unknown type prefix
|
|
const event: ProjectEvent = {
|
|
id: 'unknown-event',
|
|
type: 'custom.event' as EventType,
|
|
timestamp: new Date().toISOString(),
|
|
project_id: 'test-project',
|
|
actor_id: null,
|
|
actor_type: 'system',
|
|
payload: {},
|
|
};
|
|
mockUseProjectEventsResult.events = [event];
|
|
render(<ProjectDashboard projectId="test" />);
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
});
|
|
|
|
it('handles agent events with agent_name in payload', () => {
|
|
const event = createMockEvent(EventType.AGENT_MESSAGE, {
|
|
message: 'Hello',
|
|
agent_name: 'Test Agent',
|
|
});
|
|
mockUseProjectEventsResult.events = [event];
|
|
render(<ProjectDashboard projectId="test" />);
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
});
|
|
|
|
it('defaults agent name when not in payload', () => {
|
|
const event = createMockEvent(EventType.AGENT_MESSAGE, {
|
|
message: 'Hello',
|
|
});
|
|
mockUseProjectEventsResult.events = [event];
|
|
render(<ProjectDashboard projectId="test" />);
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
});
|
|
|
|
it('handles system actor type', () => {
|
|
const event = createMockEvent(EventType.SPRINT_STARTED, { sprint_name: 'Sprint 5' }, 'system');
|
|
mockUseProjectEventsResult.events = [event];
|
|
render(<ProjectDashboard projectId="test" />);
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
});
|
|
|
|
it('handles user actor type', () => {
|
|
const event = createMockEvent(EventType.APPROVAL_REQUESTED, { description: 'Approve' }, 'user');
|
|
mockUseProjectEventsResult.events = [event];
|
|
render(<ProjectDashboard projectId="test" />);
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
// Test sorting and limiting of activities
|
|
describe('Activity Management', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockUseProjectEventsResult = { ...mockUseProjectEventsDefault };
|
|
});
|
|
|
|
it('limits activities to 10 items', () => {
|
|
// Create more than 10 SSE events
|
|
const manyEvents: ProjectEvent[] = Array.from({ length: 15 }, (_, i) => ({
|
|
id: `event-${i}`,
|
|
type: EventType.AGENT_MESSAGE,
|
|
timestamp: new Date(Date.now() - i * 60000).toISOString(),
|
|
project_id: 'test-project',
|
|
actor_id: 'agent-001',
|
|
actor_type: 'agent' as const,
|
|
payload: { message: `Message ${i}` },
|
|
}));
|
|
|
|
mockUseProjectEventsResult.events = manyEvents;
|
|
render(<ProjectDashboard projectId="test" />);
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
// The mock shows activities count - with SSE + mock = max 10
|
|
expect(screen.getByText(/Activities:/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('sorts activities by timestamp (newest first)', () => {
|
|
const oldEvent: ProjectEvent = {
|
|
id: 'old-event',
|
|
type: EventType.AGENT_MESSAGE,
|
|
timestamp: new Date(Date.now() - 100000).toISOString(),
|
|
project_id: 'test-project',
|
|
actor_id: 'agent-001',
|
|
actor_type: 'agent',
|
|
payload: { message: 'Old message' },
|
|
};
|
|
|
|
const newEvent: ProjectEvent = {
|
|
id: 'new-event',
|
|
type: EventType.AGENT_MESSAGE,
|
|
timestamp: new Date().toISOString(),
|
|
project_id: 'test-project',
|
|
actor_id: 'agent-001',
|
|
actor_type: 'agent',
|
|
payload: { message: 'New message' },
|
|
};
|
|
|
|
mockUseProjectEventsResult.events = [oldEvent, newEvent];
|
|
render(<ProjectDashboard projectId="test" />);
|
|
expect(screen.getByTestId('mock-recent-activity')).toBeInTheDocument();
|
|
});
|
|
});
|