forked from cardosofelipe/fast-next-template
Implement the main dashboard / projects list page for Syndarix as the landing page after login. The implementation includes: Dashboard Components: - QuickStats: Overview cards showing active projects, agents, issues, approvals - ProjectsSection: Grid/list view with filtering and sorting controls - ProjectCardGrid: Rich project cards for grid view - ProjectRowList: Compact rows for list view - ActivityFeed: Real-time activity sidebar with connection status - PerformanceCard: Performance metrics display - EmptyState: Call-to-action for new users - ProjectStatusBadge: Status indicator with icons - ComplexityIndicator: Visual complexity dots - ProgressBar: Accessible progress bar component Features: - Projects grid/list view with view mode toggle - Filter by status (all, active, paused, completed, archived) - Sort by recent, name, progress, or issues - Quick stats overview with counts - Real-time activity feed sidebar with live/reconnecting status - Performance metrics card - Create project button linking to wizard - Responsive layout for mobile/desktop - Loading skeleton states - Empty state for new users API Integration: - useProjects hook for fetching projects (mock data until backend ready) - useDashboardStats hook for statistics - TanStack Query for caching and data fetching Testing: - 37 unit tests covering all dashboard components - E2E test suite for dashboard functionality - Accessibility tests (keyboard nav, aria attributes, heading hierarchy) Technical: - TypeScript strict mode compliance - ESLint passing - WCAG AA accessibility compliance - Mobile-first responsive design - Dark mode support via semantic tokens - Follows design system guidelines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
148 lines
4.8 KiB
TypeScript
148 lines
4.8 KiB
TypeScript
import { render, screen } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { RecentActivity } from '@/components/projects/RecentActivity';
|
|
import type { ActivityItem } from '@/components/projects/types';
|
|
|
|
const mockActivities: ActivityItem[] = [
|
|
{
|
|
id: 'act-001',
|
|
type: 'agent_message',
|
|
agent: 'Product Owner',
|
|
message: 'Approved user story #42',
|
|
timestamp: new Date().toISOString(),
|
|
},
|
|
{
|
|
id: 'act-002',
|
|
type: 'issue_update',
|
|
agent: 'Backend Engineer',
|
|
message: 'Moved issue #38 to review',
|
|
timestamp: new Date().toISOString(),
|
|
},
|
|
{
|
|
id: 'act-003',
|
|
type: 'approval_request',
|
|
agent: 'Architect',
|
|
message: 'Requesting API design approval',
|
|
timestamp: new Date().toISOString(),
|
|
requires_action: true,
|
|
},
|
|
];
|
|
|
|
describe('RecentActivity', () => {
|
|
it('renders recent activity with title', () => {
|
|
render(<RecentActivity activities={mockActivities} />);
|
|
expect(screen.getByText('Recent Activity')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays all activities', () => {
|
|
render(<RecentActivity activities={mockActivities} />);
|
|
expect(screen.getByText('Product Owner')).toBeInTheDocument();
|
|
expect(screen.getByText('Approved user story #42')).toBeInTheDocument();
|
|
|
|
expect(screen.getByText('Backend Engineer')).toBeInTheDocument();
|
|
expect(screen.getByText('Moved issue #38 to review')).toBeInTheDocument();
|
|
|
|
expect(screen.getByText('Architect')).toBeInTheDocument();
|
|
expect(screen.getByText('Requesting API design approval')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders empty state when no activities', () => {
|
|
render(<RecentActivity activities={[]} />);
|
|
expect(screen.getByText('No recent activity')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows loading skeleton when isLoading is true', () => {
|
|
const { container } = render(<RecentActivity activities={[]} isLoading />);
|
|
expect(container.querySelectorAll('.animate-pulse').length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('respects maxItems prop', () => {
|
|
render(<RecentActivity activities={mockActivities} maxItems={2} />);
|
|
expect(screen.getByText('Product Owner')).toBeInTheDocument();
|
|
expect(screen.getByText('Backend Engineer')).toBeInTheDocument();
|
|
expect(screen.queryByText('Architect')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('shows View All button when there are more activities than maxItems', () => {
|
|
const onViewAll = jest.fn();
|
|
render(
|
|
<RecentActivity
|
|
activities={mockActivities}
|
|
maxItems={2}
|
|
onViewAll={onViewAll}
|
|
/>
|
|
);
|
|
expect(screen.getByRole('button', { name: /view all/i })).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not show View All button when all activities are shown', () => {
|
|
const onViewAll = jest.fn();
|
|
render(
|
|
<RecentActivity
|
|
activities={mockActivities}
|
|
maxItems={5}
|
|
onViewAll={onViewAll}
|
|
/>
|
|
);
|
|
expect(screen.queryByRole('button', { name: /view all/i })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('calls onViewAll when View All button is clicked', async () => {
|
|
const user = userEvent.setup();
|
|
const onViewAll = jest.fn();
|
|
render(
|
|
<RecentActivity
|
|
activities={mockActivities}
|
|
maxItems={2}
|
|
onViewAll={onViewAll}
|
|
/>
|
|
);
|
|
|
|
await user.click(screen.getByRole('button', { name: /view all/i }));
|
|
expect(onViewAll).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('shows Review Request button for items requiring action', () => {
|
|
const onActionClick = jest.fn();
|
|
render(
|
|
<RecentActivity
|
|
activities={mockActivities}
|
|
onActionClick={onActionClick}
|
|
/>
|
|
);
|
|
expect(screen.getByRole('button', { name: /review request/i })).toBeInTheDocument();
|
|
});
|
|
|
|
it('calls onActionClick when Review Request button is clicked', async () => {
|
|
const user = userEvent.setup();
|
|
const onActionClick = jest.fn();
|
|
render(
|
|
<RecentActivity
|
|
activities={mockActivities}
|
|
onActionClick={onActionClick}
|
|
/>
|
|
);
|
|
|
|
await user.click(screen.getByRole('button', { name: /review request/i }));
|
|
expect(onActionClick).toHaveBeenCalledWith('act-003');
|
|
});
|
|
|
|
it('highlights activities requiring action', () => {
|
|
render(<RecentActivity activities={mockActivities} />);
|
|
|
|
const activityItem = screen.getByTestId('activity-item-act-003');
|
|
const iconContainer = activityItem.querySelector('.bg-yellow-100');
|
|
expect(iconContainer).toBeInTheDocument();
|
|
});
|
|
|
|
it('applies custom className', () => {
|
|
render(<RecentActivity activities={mockActivities} className="custom-class" />);
|
|
expect(screen.getByTestId('recent-activity')).toHaveClass('custom-class');
|
|
});
|
|
|
|
it('has accessible list role', () => {
|
|
render(<RecentActivity activities={mockActivities} />);
|
|
expect(screen.getByRole('list', { name: /recent project activity/i })).toBeInTheDocument();
|
|
});
|
|
});
|