/** * Tests for Sidebar Component * Verifies navigation, collapsible behavior, project context, and accessibility */ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Sidebar } from '@/components/layout/Sidebar'; import { useAuth } from '@/lib/auth/AuthContext'; import { mockUsePathname } from 'next-intl/navigation'; import type { User } from '@/lib/stores/authStore'; // Mock dependencies jest.mock('@/lib/auth/AuthContext', () => ({ useAuth: jest.fn(), AuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}, })); // Helper to create mock user function createMockUser(overrides: Partial = {}): User { return { id: 'user-123', email: 'user@example.com', first_name: 'Test', last_name: 'User', phone_number: null, is_active: true, is_superuser: false, created_at: new Date().toISOString(), updated_at: null, ...overrides, }; } describe('Sidebar', () => { beforeEach(() => { jest.clearAllMocks(); mockUsePathname.mockReturnValue('/projects'); (useAuth as unknown as jest.Mock).mockReturnValue({ user: createMockUser(), }); }); describe('Rendering', () => { it('renders sidebar with navigation header', () => { render(); expect(screen.getByText('Navigation')).toBeInTheDocument(); }); it('renders sidebar with correct test id', () => { render(); expect(screen.getByTestId('sidebar')).toBeInTheDocument(); }); it('renders main navigation items', () => { render(); expect(screen.getByTestId('nav-projects')).toBeInTheDocument(); }); it('renders collapse toggle button', () => { render(); const toggleButton = screen.getByTestId('sidebar-toggle'); expect(toggleButton).toBeInTheDocument(); expect(toggleButton).toHaveAttribute('aria-label', 'Collapse sidebar'); }); }); describe('Project Navigation', () => { it('renders project-specific navigation when projectSlug is provided', () => { render(); expect(screen.getByTestId('nav-dashboard')).toBeInTheDocument(); expect(screen.getByTestId('nav-issues')).toBeInTheDocument(); expect(screen.getByTestId('nav-sprints')).toBeInTheDocument(); expect(screen.getByTestId('nav-agents')).toBeInTheDocument(); expect(screen.getByTestId('nav-settings')).toBeInTheDocument(); }); it('does not render project navigation without projectSlug', () => { render(); expect(screen.queryByTestId('nav-dashboard')).not.toBeInTheDocument(); expect(screen.queryByTestId('nav-issues')).not.toBeInTheDocument(); }); it('generates correct hrefs for project navigation', () => { render(); expect(screen.getByTestId('nav-dashboard')).toHaveAttribute( 'href', '/projects/test-project/dashboard' ); expect(screen.getByTestId('nav-issues')).toHaveAttribute( 'href', '/projects/test-project/issues' ); }); }); describe('Admin Navigation', () => { it('renders admin navigation for superusers', () => { (useAuth as unknown as jest.Mock).mockReturnValue({ user: createMockUser({ is_superuser: true }), }); render(); expect(screen.getByTestId('nav-agent-types')).toBeInTheDocument(); expect(screen.getByTestId('nav-admin-panel')).toBeInTheDocument(); }); it('does not render admin navigation for regular users', () => { (useAuth as unknown as jest.Mock).mockReturnValue({ user: createMockUser({ is_superuser: false }), }); render(); expect(screen.queryByTestId('nav-agent-types')).not.toBeInTheDocument(); expect(screen.queryByTestId('nav-admin-panel')).not.toBeInTheDocument(); }); }); describe('Active State Highlighting', () => { it('highlights projects link when on /projects', () => { mockUsePathname.mockReturnValue('/projects'); render(); const projectsLink = screen.getByTestId('nav-projects'); expect(projectsLink).toHaveClass('bg-accent'); }); it('highlights issues link when on project issues page', () => { mockUsePathname.mockReturnValue('/projects/my-project/issues'); render(); const issuesLink = screen.getByTestId('nav-issues'); expect(issuesLink).toHaveClass('bg-accent'); }); it('sets aria-current on active link', () => { mockUsePathname.mockReturnValue('/projects'); render(); const projectsLink = screen.getByTestId('nav-projects'); expect(projectsLink).toHaveAttribute('aria-current', 'page'); }); }); describe('Collapsible Behavior', () => { it('starts in expanded state', () => { render(); expect(screen.getByText('Navigation')).toBeInTheDocument(); expect(screen.getByText('Projects')).toBeInTheDocument(); }); it('collapses when toggle button is clicked', async () => { const user = userEvent.setup(); render(); const toggleButton = screen.getByTestId('sidebar-toggle'); await user.click(toggleButton); await waitFor(() => { expect(screen.queryByText('Navigation')).not.toBeInTheDocument(); }); expect(toggleButton).toHaveAttribute('aria-label', 'Expand sidebar'); }); it('expands when toggle button is clicked twice', async () => { const user = userEvent.setup(); render(); const toggleButton = screen.getByTestId('sidebar-toggle'); // Collapse await user.click(toggleButton); await waitFor(() => { expect(screen.queryByText('Navigation')).not.toBeInTheDocument(); }); // Expand await user.click(toggleButton); await waitFor(() => { expect(screen.getByText('Navigation')).toBeInTheDocument(); }); }); it('adds title attribute to links when collapsed', async () => { const user = userEvent.setup(); render(); const projectsLink = screen.getByTestId('nav-projects'); // No title in expanded state expect(projectsLink).not.toHaveAttribute('title'); // Click to collapse const toggleButton = screen.getByTestId('sidebar-toggle'); await user.click(toggleButton); // Title should be present in collapsed state await waitFor(() => { expect(projectsLink).toHaveAttribute('title', 'Projects'); }); }); }); describe('User Info Display', () => { it('displays user info when expanded', () => { (useAuth as unknown as jest.Mock).mockReturnValue({ user: createMockUser({ first_name: 'John', last_name: 'Doe', email: 'john@example.com', }), }); render(); expect(screen.getByText('John Doe')).toBeInTheDocument(); expect(screen.getByText('john@example.com')).toBeInTheDocument(); }); it('displays user initial from first name', () => { (useAuth as unknown as jest.Mock).mockReturnValue({ user: createMockUser({ first_name: 'Alice', email: 'alice@example.com', }), }); render(); expect(screen.getByText('A')).toBeInTheDocument(); }); it('displays email initial when no first name', () => { (useAuth as unknown as jest.Mock).mockReturnValue({ user: createMockUser({ first_name: '', email: 'test@example.com', }), }); render(); expect(screen.getByText('T')).toBeInTheDocument(); }); it('hides user info when collapsed', async () => { const user = userEvent.setup(); (useAuth as unknown as jest.Mock).mockReturnValue({ user: createMockUser({ first_name: 'John', last_name: 'Doe', email: 'john@example.com', }), }); render(); expect(screen.getByText('John Doe')).toBeInTheDocument(); const toggleButton = screen.getByTestId('sidebar-toggle'); await user.click(toggleButton); await waitFor(() => { expect(screen.queryByText('John Doe')).not.toBeInTheDocument(); expect(screen.queryByText('john@example.com')).not.toBeInTheDocument(); }); }); }); describe('Mobile Navigation', () => { it('renders mobile menu trigger button', () => { render(); expect(screen.getByTestId('mobile-menu-trigger')).toBeInTheDocument(); }); it('has accessible label on mobile trigger', () => { render(); const trigger = screen.getByTestId('mobile-menu-trigger'); expect(trigger).toHaveAttribute('aria-label', 'Open navigation menu'); }); }); describe('Accessibility', () => { it('has proper aria-label on sidebar', () => { render(); const sidebar = screen.getByTestId('sidebar'); expect(sidebar).toHaveAttribute('aria-label', 'Main navigation'); }); it('navigation links are keyboard accessible', () => { render(); const projectsLink = screen.getByTestId('nav-projects'); expect(projectsLink.tagName).toBe('A'); }); it('has proper focus styling classes on links', () => { render(); const projectsLink = screen.getByTestId('nav-projects'); expect(projectsLink).toHaveClass('focus-visible:ring-2'); }); }); });