forked from cardosofelipe/fast-next-template
test(frontend): add unit tests for Dashboard components
Add comprehensive test coverage for dashboard components: - Dashboard.test.tsx: Main component integration tests - WelcomeHeader.test.tsx: User greeting and time-based messages - DashboardQuickStats.test.tsx: Stats cards rendering and links - RecentProjects.test.tsx: Project cards grid and navigation - PendingApprovals.test.tsx: Approval items and actions - EmptyState.test.tsx: New user onboarding experience 46 tests covering rendering, interactions, and edge cases. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
211
frontend/tests/components/dashboard/Dashboard.test.tsx
Normal file
211
frontend/tests/components/dashboard/Dashboard.test.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Dashboard Component Tests
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { Dashboard } from '@/components/dashboard/Dashboard';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
import { useDashboard } from '@/lib/api/hooks/useDashboard';
|
||||
import { useProjectEvents } from '@/lib/hooks/useProjectEvents';
|
||||
import { useProjectEventsFromStore } from '@/lib/stores/eventStore';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@/lib/auth/AuthContext', () => ({
|
||||
useAuth: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/api/hooks/useDashboard', () => ({
|
||||
useDashboard: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/hooks/useProjectEvents', () => ({
|
||||
useProjectEvents: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@/lib/stores/eventStore', () => ({
|
||||
useProjectEventsFromStore: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock next-intl navigation
|
||||
jest.mock('@/lib/i18n/routing', () => ({
|
||||
Link: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href}>{children}</a>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock sonner
|
||||
jest.mock('sonner', () => ({
|
||||
toast: {
|
||||
success: jest.fn(),
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockUseAuth = useAuth as jest.MockedFunction<typeof useAuth>;
|
||||
const mockUseDashboard = useDashboard as jest.MockedFunction<typeof useDashboard>;
|
||||
const mockUseProjectEvents = useProjectEvents as jest.MockedFunction<typeof useProjectEvents>;
|
||||
const mockUseProjectEventsFromStore = useProjectEventsFromStore as jest.MockedFunction<
|
||||
typeof useProjectEventsFromStore
|
||||
>;
|
||||
|
||||
describe('Dashboard', () => {
|
||||
const mockUser = {
|
||||
id: '1',
|
||||
email: 'test@example.com',
|
||||
first_name: 'Test',
|
||||
is_active: true,
|
||||
is_superuser: false,
|
||||
created_at: '',
|
||||
};
|
||||
|
||||
const mockDashboardData = {
|
||||
stats: {
|
||||
activeProjects: 3,
|
||||
runningAgents: 8,
|
||||
openIssues: 24,
|
||||
pendingApprovals: 2,
|
||||
},
|
||||
recentProjects: [
|
||||
{
|
||||
id: 'proj-1',
|
||||
name: 'Test Project',
|
||||
description: 'Test description',
|
||||
status: 'active' as const,
|
||||
autonomy_level: 'milestone' as const,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
owner_id: 'user-1',
|
||||
progress: 50,
|
||||
openIssues: 5,
|
||||
activeAgents: 2,
|
||||
lastActivity: '5 min ago',
|
||||
},
|
||||
],
|
||||
pendingApprovals: [
|
||||
{
|
||||
id: 'approval-1',
|
||||
type: 'sprint_boundary' as const,
|
||||
title: 'Sprint Review',
|
||||
description: 'Review sprint',
|
||||
projectId: 'proj-1',
|
||||
projectName: 'Test Project',
|
||||
requestedBy: 'Agent',
|
||||
requestedAt: new Date().toISOString(),
|
||||
priority: 'high' as const,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: mockUser,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
clearError: jest.fn(),
|
||||
checkAuth: jest.fn(),
|
||||
});
|
||||
|
||||
mockUseDashboard.mockReturnValue({
|
||||
data: mockDashboardData,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isError: false,
|
||||
isPending: false,
|
||||
isSuccess: true,
|
||||
status: 'success',
|
||||
} as ReturnType<typeof useDashboard>);
|
||||
|
||||
mockUseProjectEvents.mockReturnValue({
|
||||
connectionState: 'connected',
|
||||
events: [],
|
||||
isConnected: true,
|
||||
error: null,
|
||||
retryCount: 0,
|
||||
reconnect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
clearEvents: jest.fn(),
|
||||
});
|
||||
|
||||
mockUseProjectEventsFromStore.mockReturnValue([]);
|
||||
});
|
||||
|
||||
it('renders welcome header', () => {
|
||||
render(<Dashboard />);
|
||||
|
||||
// User first name appears in welcome message
|
||||
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders quick stats', () => {
|
||||
render(<Dashboard />);
|
||||
|
||||
expect(screen.getByText('Active Projects')).toBeInTheDocument();
|
||||
expect(screen.getByText('Running Agents')).toBeInTheDocument();
|
||||
expect(screen.getByText('Open Issues')).toBeInTheDocument();
|
||||
// "Pending Approvals" appears in both stats and approvals section
|
||||
const pendingApprovalsTexts = screen.getAllByText('Pending Approvals');
|
||||
expect(pendingApprovalsTexts.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders recent projects section', () => {
|
||||
render(<Dashboard />);
|
||||
|
||||
expect(screen.getByText('Recent Projects')).toBeInTheDocument();
|
||||
// Use getAllByText since project name appears in multiple places
|
||||
const projectNames = screen.getAllByText('Test Project');
|
||||
expect(projectNames.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders pending approvals section when approvals exist', () => {
|
||||
render(<Dashboard />);
|
||||
|
||||
// Check for pending approvals header
|
||||
const approvalHeaders = screen.getAllByText('Pending Approvals');
|
||||
expect(approvalHeaders.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('shows empty state when no projects', () => {
|
||||
mockUseDashboard.mockReturnValue({
|
||||
data: { ...mockDashboardData, recentProjects: [] },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
isError: false,
|
||||
isPending: false,
|
||||
isSuccess: true,
|
||||
status: 'success',
|
||||
} as unknown as ReturnType<typeof useDashboard>);
|
||||
|
||||
render(<Dashboard />);
|
||||
|
||||
expect(screen.getByText(/Welcome to Syndarix/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Create Your First Project/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows loading state', () => {
|
||||
mockUseDashboard.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
isError: false,
|
||||
isPending: true,
|
||||
isSuccess: false,
|
||||
status: 'pending',
|
||||
} as ReturnType<typeof useDashboard>);
|
||||
|
||||
render(<Dashboard />);
|
||||
|
||||
// Should show skeleton loading states
|
||||
expect(screen.getByText('Recent Projects')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Dashboard className="custom-class" />);
|
||||
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* DashboardQuickStats Component Tests
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { DashboardQuickStats } from '@/components/dashboard/DashboardQuickStats';
|
||||
import type { DashboardStats } from '@/lib/api/hooks/useDashboard';
|
||||
|
||||
describe('DashboardQuickStats', () => {
|
||||
const mockStats: DashboardStats = {
|
||||
activeProjects: 5,
|
||||
runningAgents: 12,
|
||||
openIssues: 34,
|
||||
pendingApprovals: 3,
|
||||
};
|
||||
|
||||
it('renders all four stat cards', () => {
|
||||
render(<DashboardQuickStats stats={mockStats} />);
|
||||
|
||||
expect(screen.getByText('Active Projects')).toBeInTheDocument();
|
||||
expect(screen.getByText('Running Agents')).toBeInTheDocument();
|
||||
expect(screen.getByText('Open Issues')).toBeInTheDocument();
|
||||
expect(screen.getByText('Pending Approvals')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays correct stat values', () => {
|
||||
render(<DashboardQuickStats stats={mockStats} />);
|
||||
|
||||
expect(screen.getByText('5')).toBeInTheDocument();
|
||||
expect(screen.getByText('12')).toBeInTheDocument();
|
||||
expect(screen.getByText('34')).toBeInTheDocument();
|
||||
expect(screen.getByText('3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays descriptions for each stat', () => {
|
||||
render(<DashboardQuickStats stats={mockStats} />);
|
||||
|
||||
expect(screen.getByText('Currently in progress')).toBeInTheDocument();
|
||||
expect(screen.getByText('Working on tasks')).toBeInTheDocument();
|
||||
expect(screen.getByText('Across all projects')).toBeInTheDocument();
|
||||
expect(screen.getByText('Awaiting your review')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows zero values when stats are undefined', () => {
|
||||
render(<DashboardQuickStats />);
|
||||
|
||||
// Should show 0 for all stats
|
||||
const zeros = screen.getAllByText('0');
|
||||
expect(zeros).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('shows loading state when isLoading is true', () => {
|
||||
render(<DashboardQuickStats isLoading />);
|
||||
|
||||
// StatCard shows loading animation
|
||||
const statCards = screen.getAllByTestId('stat-card');
|
||||
statCards.forEach((card) => {
|
||||
expect(card).toHaveClass('animate-pulse');
|
||||
});
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<DashboardQuickStats className="custom-class" />);
|
||||
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
57
frontend/tests/components/dashboard/EmptyState.test.tsx
Normal file
57
frontend/tests/components/dashboard/EmptyState.test.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* EmptyState Component Tests
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { EmptyState } from '@/components/dashboard/EmptyState';
|
||||
|
||||
// Mock next-intl navigation
|
||||
jest.mock('@/lib/i18n/routing', () => ({
|
||||
Link: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href}>{children}</a>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('EmptyState', () => {
|
||||
it('displays welcome message with user name', () => {
|
||||
render(<EmptyState userName="John" />);
|
||||
|
||||
expect(screen.getByText(/Welcome to Syndarix, John!/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays default greeting when no userName provided', () => {
|
||||
render(<EmptyState />);
|
||||
|
||||
expect(screen.getByText(/Welcome to Syndarix, there!/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays description text', () => {
|
||||
render(<EmptyState />);
|
||||
|
||||
expect(screen.getByText(/Get started by creating your first project/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays Create Your First Project button', () => {
|
||||
render(<EmptyState />);
|
||||
|
||||
const createButton = screen.getByRole('link', { name: /Create Your First Project/i });
|
||||
expect(createButton).toBeInTheDocument();
|
||||
expect(createButton).toHaveAttribute('href', '/projects/new');
|
||||
});
|
||||
|
||||
it('displays quick action links', () => {
|
||||
render(<EmptyState />);
|
||||
|
||||
const agentsLink = screen.getByRole('link', { name: /Set up AI agent types/i });
|
||||
expect(agentsLink).toHaveAttribute('href', '/agents');
|
||||
|
||||
const settingsLink = screen.getByRole('link', { name: /Configure your account/i });
|
||||
expect(settingsLink).toHaveAttribute('href', '/settings');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<EmptyState className="custom-class" />);
|
||||
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
123
frontend/tests/components/dashboard/PendingApprovals.test.tsx
Normal file
123
frontend/tests/components/dashboard/PendingApprovals.test.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* PendingApprovals Component Tests
|
||||
*/
|
||||
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { PendingApprovals } from '@/components/dashboard/PendingApprovals';
|
||||
import type { PendingApproval } from '@/lib/api/hooks/useDashboard';
|
||||
|
||||
// Mock next-intl navigation
|
||||
jest.mock('@/lib/i18n/routing', () => ({
|
||||
Link: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href}>{children}</a>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('PendingApprovals', () => {
|
||||
const mockApprovals: PendingApproval[] = [
|
||||
{
|
||||
id: 'approval-1',
|
||||
type: 'sprint_boundary',
|
||||
title: 'Sprint 3 Completion',
|
||||
description: 'Review sprint deliverables',
|
||||
projectId: 'proj-1',
|
||||
projectName: 'E-Commerce Platform',
|
||||
requestedBy: 'Product Owner Agent',
|
||||
requestedAt: new Date().toISOString(),
|
||||
priority: 'high',
|
||||
},
|
||||
{
|
||||
id: 'approval-2',
|
||||
type: 'code_review',
|
||||
title: 'PR #123 Review',
|
||||
description: 'Authentication module changes',
|
||||
projectId: 'proj-2',
|
||||
projectName: 'Banking App',
|
||||
requestedBy: 'Developer Agent',
|
||||
requestedAt: new Date().toISOString(),
|
||||
priority: 'medium',
|
||||
},
|
||||
];
|
||||
|
||||
it('renders approval items', () => {
|
||||
render(<PendingApprovals approvals={mockApprovals} />);
|
||||
|
||||
expect(screen.getByText('Sprint 3 Completion')).toBeInTheDocument();
|
||||
expect(screen.getByText('PR #123 Review')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays section header with count', () => {
|
||||
render(<PendingApprovals approvals={mockApprovals} />);
|
||||
|
||||
expect(screen.getByText('Pending Approvals')).toBeInTheDocument();
|
||||
expect(screen.getByText('2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays approval descriptions', () => {
|
||||
render(<PendingApprovals approvals={mockApprovals} />);
|
||||
|
||||
expect(screen.getByText('Review sprint deliverables')).toBeInTheDocument();
|
||||
expect(screen.getByText('Authentication module changes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays project names with links', () => {
|
||||
render(<PendingApprovals approvals={mockApprovals} />);
|
||||
|
||||
const projectLink = screen.getByRole('link', { name: 'E-Commerce Platform' });
|
||||
expect(projectLink).toHaveAttribute('href', '/projects/proj-1');
|
||||
});
|
||||
|
||||
it('displays requestor information', () => {
|
||||
render(<PendingApprovals approvals={mockApprovals} />);
|
||||
|
||||
expect(screen.getByText(/Product Owner Agent/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Developer Agent/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays priority badges', () => {
|
||||
render(<PendingApprovals approvals={mockApprovals} />);
|
||||
|
||||
expect(screen.getByText('High')).toBeInTheDocument();
|
||||
expect(screen.getByText('Medium')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onApprove when Approve button clicked', () => {
|
||||
const onApprove = jest.fn();
|
||||
render(<PendingApprovals approvals={mockApprovals} onApprove={onApprove} />);
|
||||
|
||||
const approveButtons = screen.getAllByRole('button', { name: /Approve/i });
|
||||
fireEvent.click(approveButtons[0]);
|
||||
|
||||
expect(onApprove).toHaveBeenCalledWith(mockApprovals[0]);
|
||||
});
|
||||
|
||||
it('calls onReject when Reject button clicked', () => {
|
||||
const onReject = jest.fn();
|
||||
render(<PendingApprovals approvals={mockApprovals} onReject={onReject} />);
|
||||
|
||||
const rejectButtons = screen.getAllByRole('button', { name: /Reject/i });
|
||||
fireEvent.click(rejectButtons[0]);
|
||||
|
||||
expect(onReject).toHaveBeenCalledWith(mockApprovals[0]);
|
||||
});
|
||||
|
||||
it('does not render when no approvals and not loading', () => {
|
||||
const { container } = render(<PendingApprovals approvals={[]} />);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('shows loading skeletons when isLoading is true', () => {
|
||||
render(<PendingApprovals isLoading />);
|
||||
|
||||
expect(screen.getByText('Pending Approvals')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<PendingApprovals approvals={mockApprovals} className="custom-class" />
|
||||
);
|
||||
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
120
frontend/tests/components/dashboard/RecentProjects.test.tsx
Normal file
120
frontend/tests/components/dashboard/RecentProjects.test.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* RecentProjects Component Tests
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { RecentProjects } from '@/components/dashboard/RecentProjects';
|
||||
import type { DashboardProject } from '@/lib/api/hooks/useDashboard';
|
||||
|
||||
// Mock next-intl navigation
|
||||
jest.mock('@/lib/i18n/routing', () => ({
|
||||
Link: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href}>{children}</a>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('RecentProjects', () => {
|
||||
const mockProjects: DashboardProject[] = [
|
||||
{
|
||||
id: 'proj-1',
|
||||
name: 'Project One',
|
||||
description: 'First project description',
|
||||
status: 'active',
|
||||
autonomy_level: 'milestone',
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
owner_id: 'user-1',
|
||||
progress: 75,
|
||||
openIssues: 5,
|
||||
activeAgents: 3,
|
||||
currentSprint: 'Sprint 2',
|
||||
lastActivity: '5 minutes ago',
|
||||
},
|
||||
{
|
||||
id: 'proj-2',
|
||||
name: 'Project Two',
|
||||
description: 'Second project description',
|
||||
status: 'paused',
|
||||
autonomy_level: 'full_control',
|
||||
created_at: '2025-01-02T00:00:00Z',
|
||||
owner_id: 'user-1',
|
||||
progress: 30,
|
||||
openIssues: 8,
|
||||
activeAgents: 0,
|
||||
lastActivity: '2 days ago',
|
||||
},
|
||||
];
|
||||
|
||||
it('renders project cards', () => {
|
||||
render(<RecentProjects projects={mockProjects} />);
|
||||
|
||||
expect(screen.getByText('Project One')).toBeInTheDocument();
|
||||
expect(screen.getByText('Project Two')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays section header with View all link', () => {
|
||||
render(<RecentProjects projects={mockProjects} />);
|
||||
|
||||
expect(screen.getByText('Recent Projects')).toBeInTheDocument();
|
||||
const viewAllLink = screen.getByRole('link', { name: /View all/i });
|
||||
expect(viewAllLink).toHaveAttribute('href', '/projects');
|
||||
});
|
||||
|
||||
it('displays project descriptions', () => {
|
||||
render(<RecentProjects projects={mockProjects} />);
|
||||
|
||||
expect(screen.getByText('First project description')).toBeInTheDocument();
|
||||
expect(screen.getByText('Second project description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays project metrics', () => {
|
||||
render(<RecentProjects projects={mockProjects} />);
|
||||
|
||||
// Check agents count
|
||||
expect(screen.getByText('3 agents')).toBeInTheDocument();
|
||||
expect(screen.getByText('0 agents')).toBeInTheDocument();
|
||||
|
||||
// Check issues count
|
||||
expect(screen.getByText('5 issues')).toBeInTheDocument();
|
||||
expect(screen.getByText('8 issues')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays sprint info when available', () => {
|
||||
render(<RecentProjects projects={mockProjects} />);
|
||||
|
||||
expect(screen.getByText('Sprint 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays last activity time', () => {
|
||||
render(<RecentProjects projects={mockProjects} />);
|
||||
|
||||
expect(screen.getByText('5 minutes ago')).toBeInTheDocument();
|
||||
expect(screen.getByText('2 days ago')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows loading skeletons when isLoading is true', () => {
|
||||
render(<RecentProjects isLoading />);
|
||||
|
||||
// Should show skeleton cards
|
||||
expect(screen.getByText('Recent Projects')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty state when no projects', () => {
|
||||
render(<RecentProjects projects={[]} />);
|
||||
|
||||
expect(screen.getByText('No projects yet')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: /Create your first project/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('links project cards to project detail page', () => {
|
||||
render(<RecentProjects projects={mockProjects} />);
|
||||
|
||||
const projectLink = screen.getByRole('link', { name: /Project One/i });
|
||||
expect(projectLink).toHaveAttribute('href', '/projects/proj-1');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<RecentProjects projects={mockProjects} className="custom-class" />);
|
||||
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
131
frontend/tests/components/dashboard/WelcomeHeader.test.tsx
Normal file
131
frontend/tests/components/dashboard/WelcomeHeader.test.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* WelcomeHeader Component Tests
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { WelcomeHeader } from '@/components/dashboard/WelcomeHeader';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
|
||||
// Mock useAuth hook
|
||||
jest.mock('@/lib/auth/AuthContext', () => ({
|
||||
useAuth: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock next-intl navigation
|
||||
jest.mock('@/lib/i18n/routing', () => ({
|
||||
Link: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href}>{children}</a>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockUseAuth = useAuth as jest.MockedFunction<typeof useAuth>;
|
||||
|
||||
describe('WelcomeHeader', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('displays greeting with user first name', () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { id: '1', email: 'john@example.com', first_name: 'John', is_active: true, is_superuser: false, created_at: '' },
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
clearError: jest.fn(),
|
||||
checkAuth: jest.fn(),
|
||||
});
|
||||
|
||||
render(<WelcomeHeader />);
|
||||
|
||||
expect(screen.getByText(/John/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('falls back to email prefix when first_name is empty', () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { id: '1', email: 'jane@example.com', first_name: '', is_active: true, is_superuser: false, created_at: '' },
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
clearError: jest.fn(),
|
||||
checkAuth: jest.fn(),
|
||||
});
|
||||
|
||||
render(<WelcomeHeader />);
|
||||
|
||||
expect(screen.getByText(/jane/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays default greeting when no user', () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
clearError: jest.fn(),
|
||||
checkAuth: jest.fn(),
|
||||
});
|
||||
|
||||
render(<WelcomeHeader />);
|
||||
|
||||
expect(screen.getByText(/there/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays subtitle text', () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { id: '1', email: 'test@example.com', first_name: 'Test', is_active: true, is_superuser: false, created_at: '' },
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
clearError: jest.fn(),
|
||||
checkAuth: jest.fn(),
|
||||
});
|
||||
|
||||
render(<WelcomeHeader />);
|
||||
|
||||
expect(screen.getByText(/Here's what's happening/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays Create Project button', () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { id: '1', email: 'test@example.com', first_name: 'Test', is_active: true, is_superuser: false, created_at: '' },
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
clearError: jest.fn(),
|
||||
checkAuth: jest.fn(),
|
||||
});
|
||||
|
||||
render(<WelcomeHeader />);
|
||||
|
||||
const createButton = screen.getByRole('link', { name: /Create Project/i });
|
||||
expect(createButton).toBeInTheDocument();
|
||||
expect(createButton).toHaveAttribute('href', '/projects/new');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
clearError: jest.fn(),
|
||||
checkAuth: jest.fn(),
|
||||
});
|
||||
|
||||
const { container } = render(<WelcomeHeader className="custom-class" />);
|
||||
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user