/** * Tests for UserMenu Component * Verifies user menu rendering, navigation, and logout functionality */ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { UserMenu } from '@/components/layout/UserMenu'; import { useAuth } from '@/lib/auth/AuthContext'; import { useLogout } from '@/lib/api/hooks/useAuth'; import type { User } from '@/lib/stores/authStore'; // Mock dependencies jest.mock('@/lib/auth/AuthContext', () => ({ useAuth: jest.fn(), AuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}, })); jest.mock('@/lib/api/hooks/useAuth', () => ({ useLogout: jest.fn(), })); // Helper to create mock user function createMockUser(overrides: Partial = {}): User { return { id: 'user-123', email: 'test@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('UserMenu', () => { const mockLogout = jest.fn(); beforeEach(() => { jest.clearAllMocks(); (useLogout as jest.Mock).mockReturnValue({ mutate: mockLogout, isPending: false, }); (useAuth as unknown as jest.Mock).mockReturnValue({ user: createMockUser(), }); }); describe('Rendering', () => { it('renders user menu trigger', () => { render(); expect(screen.getByTestId('user-menu-trigger')).toBeInTheDocument(); }); it('displays user initials in avatar', () => { (useAuth as unknown as jest.Mock).mockReturnValue({ user: createMockUser({ first_name: 'John', last_name: 'Doe', }), }); render(); expect(screen.getByText('JD')).toBeInTheDocument(); }); it('displays single initial when no last name', () => { (useAuth as unknown as jest.Mock).mockReturnValue({ user: createMockUser({ first_name: 'John', last_name: null, }), }); render(); expect(screen.getByText('J')).toBeInTheDocument(); }); it('displays default initial when no first name', () => { (useAuth as unknown as jest.Mock).mockReturnValue({ user: createMockUser({ first_name: '', }), }); render(); expect(screen.getByText('U')).toBeInTheDocument(); }); it('returns null when no user', () => { (useAuth as unknown as jest.Mock).mockReturnValue({ user: null, }); const { container } = render(); expect(container).toBeEmptyDOMElement(); }); }); describe('Dropdown Menu', () => { it('opens menu when trigger is clicked', async () => { const user = userEvent.setup(); render(); const trigger = screen.getByTestId('user-menu-trigger'); await user.click(trigger); await waitFor(() => { expect(screen.getByTestId('user-menu-content')).toBeInTheDocument(); }); }); it('displays user info in menu header', 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(); const trigger = screen.getByTestId('user-menu-trigger'); await user.click(trigger); await waitFor(() => { expect(screen.getByText('John Doe')).toBeInTheDocument(); expect(screen.getByText('john@example.com')).toBeInTheDocument(); }); }); }); describe('Navigation Links', () => { it('includes profile link', async () => { const user = userEvent.setup(); render(); const trigger = screen.getByTestId('user-menu-trigger'); await user.click(trigger); const profileLink = await screen.findByTestId('user-menu-profile'); expect(profileLink).toHaveAttribute('href', '/settings/profile'); }); it('includes password/settings link', async () => { const user = userEvent.setup(); render(); const trigger = screen.getByTestId('user-menu-trigger'); await user.click(trigger); const passwordLink = await screen.findByTestId('user-menu-password'); expect(passwordLink).toHaveAttribute('href', '/settings/password'); }); it('includes sessions link', async () => { const user = userEvent.setup(); render(); const trigger = screen.getByTestId('user-menu-trigger'); await user.click(trigger); const sessionsLink = await screen.findByTestId('user-menu-sessions'); expect(sessionsLink).toHaveAttribute('href', '/settings/sessions'); }); it('includes preferences link', async () => { const user = userEvent.setup(); render(); const trigger = screen.getByTestId('user-menu-trigger'); await user.click(trigger); const preferencesLink = await screen.findByTestId('user-menu-preferences'); expect(preferencesLink).toHaveAttribute('href', '/settings/preferences'); }); }); describe('Admin Link', () => { it('shows admin link for superusers', async () => { const user = userEvent.setup(); (useAuth as unknown as jest.Mock).mockReturnValue({ user: createMockUser({ is_superuser: true }), }); render(); const trigger = screen.getByTestId('user-menu-trigger'); await user.click(trigger); const adminLink = await screen.findByTestId('user-menu-admin'); expect(adminLink).toHaveAttribute('href', '/admin'); }); it('does not show admin link for regular users', async () => { const user = userEvent.setup(); (useAuth as unknown as jest.Mock).mockReturnValue({ user: createMockUser({ is_superuser: false }), }); render(); const trigger = screen.getByTestId('user-menu-trigger'); await user.click(trigger); await waitFor(() => { expect(screen.queryByTestId('user-menu-admin')).not.toBeInTheDocument(); }); }); }); describe('Logout Functionality', () => { it('calls logout when logout button is clicked', async () => { const user = userEvent.setup(); render(); const trigger = screen.getByTestId('user-menu-trigger'); await user.click(trigger); const logoutButton = await screen.findByTestId('user-menu-logout'); await user.click(logoutButton); expect(mockLogout).toHaveBeenCalledTimes(1); }); it('shows loading state when logging out', async () => { const user = userEvent.setup(); (useLogout as jest.Mock).mockReturnValue({ mutate: mockLogout, isPending: true, }); render(); const trigger = screen.getByTestId('user-menu-trigger'); await user.click(trigger); await waitFor(() => { expect(screen.getByText('loggingOut')).toBeInTheDocument(); }); }); it('disables logout button when logging out', async () => { const user = userEvent.setup(); (useLogout as jest.Mock).mockReturnValue({ mutate: mockLogout, isPending: true, }); render(); const trigger = screen.getByTestId('user-menu-trigger'); await user.click(trigger); const logoutButton = await screen.findByTestId('user-menu-logout'); expect(logoutButton).toHaveAttribute('data-disabled'); }); }); describe('Accessibility', () => { it('has accessible label on trigger', () => { (useAuth as unknown as jest.Mock).mockReturnValue({ user: createMockUser({ first_name: 'John', }), }); render(); const trigger = screen.getByTestId('user-menu-trigger'); expect(trigger).toHaveAttribute('aria-label', 'User menu for John'); }); it('uses email when no first name', () => { (useAuth as unknown as jest.Mock).mockReturnValue({ user: createMockUser({ first_name: '', email: 'test@example.com', }), }); render(); const trigger = screen.getByTestId('user-menu-trigger'); expect(trigger).toHaveAttribute('aria-label', 'User menu for test@example.com'); }); it('logout button has destructive styling', async () => { const user = userEvent.setup(); render(); const trigger = screen.getByTestId('user-menu-trigger'); await user.click(trigger); const logoutButton = await screen.findByTestId('user-menu-logout'); expect(logoutButton).toHaveClass('text-destructive'); }); }); });