import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import AvatarDropdown from '../AvatarDropdown'; import { User, UserRole } from '../../../types'; import { AccountStatus } from '../../../services/auth/auth.constants'; const mockRegularUser: User = { _id: '1', _rev: '1-abc123', username: 'John Doe', email: 'john@example.com', role: UserRole.USER, status: AccountStatus.ACTIVE, createdAt: new Date(), }; const mockAdminUser: User = { _id: '2', _rev: '1-def456', username: 'Admin User', email: 'admin@example.com', role: UserRole.ADMIN, status: AccountStatus.ACTIVE, createdAt: new Date(), }; const mockUserWithAvatar: User = { ...mockRegularUser, avatar: 'https://example.com/avatar.jpg', }; const mockUserWithPassword: User = { ...mockRegularUser, password: '$2b$12$examplehashforpassword', }; type DropdownProps = Partial>; const renderDropdown = (props: DropdownProps = {}) => { const defaultProps: React.ComponentProps = { user: mockRegularUser, onLogout: jest.fn(), }; const merged = { ...defaultProps, ...props }; return render(React.createElement(AvatarDropdown, merged)); }; describe('AvatarDropdown', () => { const mockOnLogout = jest.fn(); const mockOnAdmin = jest.fn(); const mockOnChangePassword = jest.fn(); beforeEach(() => { jest.clearAllMocks(); }); describe('rendering', () => { test('should render avatar button with user initials', () => { renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout }); const button = screen.getByRole('button', { name: /user menu/i }); expect(button).toBeInTheDocument(); expect(button).toHaveTextContent('J'); }); test('should render avatar image when user has avatar', () => { renderDropdown({ user: mockUserWithAvatar, onLogout: mockOnLogout }); const avatar = screen.getByAltText('User avatar'); expect(avatar).toBeInTheDocument(); expect(avatar).toHaveAttribute('src', 'https://example.com/avatar.jpg'); }); test('should render fallback character for empty username', () => { const userWithEmptyName = { ...mockRegularUser, username: '' }; renderDropdown({ user: userWithEmptyName, onLogout: mockOnLogout }); const button = screen.getByRole('button', { name: /user menu/i }); expect(button).toHaveTextContent('?'); }); test('should not render dropdown menu initially', () => { renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout }); expect(screen.queryByText('Signed in as')).not.toBeInTheDocument(); }); }); describe('dropdown functionality', () => { test('should open dropdown when avatar button is clicked', () => { renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout }); const button = screen.getByRole('button', { name: /user menu/i }); fireEvent.click(button); expect(screen.getByText('Signed in as')).toBeInTheDocument(); expect(screen.getByText('John Doe')).toBeInTheDocument(); }); test('should close dropdown when avatar button is clicked again', () => { renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout }); const button = screen.getByRole('button', { name: /user menu/i }); fireEvent.click(button); expect(screen.getByText('Signed in as')).toBeInTheDocument(); fireEvent.click(button); expect(screen.queryByText('Signed in as')).not.toBeInTheDocument(); }); test('should close dropdown when clicking outside', async () => { render( React.createElement( 'div', null, React.createElement(AvatarDropdown, { user: mockRegularUser, onLogout: mockOnLogout, }), React.createElement( 'div', { 'data-testid': 'outside' }, 'Outside element' ) ) ); const button = screen.getByRole('button', { name: /user menu/i }); const outside = screen.getByTestId('outside'); fireEvent.click(button); expect(screen.getByText('Signed in as')).toBeInTheDocument(); fireEvent.mouseDown(outside); await waitFor(() => { expect(screen.queryByText('Signed in as')).not.toBeInTheDocument(); }); }); }); describe('user information display', () => { test('should display username in dropdown', () => { renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout }); fireEvent.click(screen.getByRole('button', { name: /user menu/i })); expect(screen.getByText('John Doe')).toBeInTheDocument(); }); test('should display administrator badge for admin users', () => { renderDropdown({ user: mockAdminUser, onLogout: mockOnLogout }); fireEvent.click(screen.getByRole('button', { name: /user menu/i })); expect(screen.getByText('Administrator')).toBeInTheDocument(); }); test('should not display administrator badge for regular users', () => { renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout }); fireEvent.click(screen.getByRole('button', { name: /user menu/i })); expect(screen.queryByText('Administrator')).not.toBeInTheDocument(); }); test('should truncate long usernames', () => { const userWithLongName = { ...mockRegularUser, username: 'Very Long Username That Should Be Truncated', }; renderDropdown({ user: userWithLongName, onLogout: mockOnLogout }); fireEvent.click(screen.getByRole('button', { name: /user menu/i })); const usernameElement = screen.getByText( 'Very Long Username That Should Be Truncated' ); expect(usernameElement).toHaveClass('truncate'); }); }); describe('logout functionality', () => { test('should call onLogout when logout button is clicked', () => { renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout }); fireEvent.click(screen.getByRole('button', { name: /user menu/i })); fireEvent.click(screen.getByText('Logout')); expect(mockOnLogout).toHaveBeenCalledTimes(1); }); test('should close dropdown after logout is clicked', () => { renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout }); fireEvent.click(screen.getByRole('button', { name: /user menu/i })); fireEvent.click(screen.getByText('Logout')); expect(screen.queryByText('Signed in as')).not.toBeInTheDocument(); }); test('should always display logout button', () => { renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout }); fireEvent.click(screen.getByRole('button', { name: /user menu/i })); expect(screen.getByText('Logout')).toBeInTheDocument(); }); }); describe('admin functionality', () => { test('should render Admin Interface button for admin users', () => { renderDropdown({ user: mockAdminUser, onLogout: mockOnLogout, onAdmin: mockOnAdmin, }); fireEvent.click(screen.getByRole('button', { name: /user menu/i })); const adminButton = screen.getByText('Admin Interface'); expect(adminButton).toBeInTheDocument(); fireEvent.click(adminButton); expect(mockOnAdmin).toHaveBeenCalledTimes(1); }); test('should not render Admin Interface button for regular users', () => { renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout }); fireEvent.click(screen.getByRole('button', { name: /user menu/i })); expect(screen.queryByText('Admin Interface')).not.toBeInTheDocument(); }); }); describe('change password visibility', () => { test('should show change password option when user has password', () => { renderDropdown({ user: mockUserWithPassword, onLogout: mockOnLogout, onChangePassword: mockOnChangePassword, }); fireEvent.click(screen.getByRole('button', { name: /user menu/i })); expect(screen.getByText('Change Password')).toBeInTheDocument(); }); test('should hide change password option when user has no password', () => { renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout, onChangePassword: mockOnChangePassword, }); fireEvent.click(screen.getByRole('button', { name: /user menu/i })); expect(screen.queryByText('Change Password')).not.toBeInTheDocument(); }); test('should call onChangePassword when change password button clicked', () => { renderDropdown({ user: mockUserWithPassword, onLogout: mockOnLogout, onChangePassword: mockOnChangePassword, }); fireEvent.click(screen.getByRole('button', { name: /user menu/i })); fireEvent.click(screen.getByText('Change Password')); expect(mockOnChangePassword).toHaveBeenCalledTimes(1); }); }); describe('keyboard accessibility', () => { test('should toggle dropdown with Enter key', () => { renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout }); const button = screen.getByRole('button', { name: /user menu/i }); fireEvent.keyDown(button, { key: 'Enter', code: 'Enter' }); fireEvent.keyUp(button, { key: 'Enter', code: 'Enter' }); expect(screen.getByText('Signed in as')).toBeInTheDocument(); }); test('should not toggle dropdown with unrelated key', () => { renderDropdown({ user: mockRegularUser, onLogout: mockOnLogout }); const button = screen.getByRole('button', { name: /user menu/i }); fireEvent.keyDown(button, { key: 'Space', code: 'Space' }); expect(screen.queryByText('Signed in as')).not.toBeInTheDocument(); }); }); describe('user initials generation', () => { test('should handle lowercase usernames', () => { const userWithLowercase = { ...mockRegularUser, username: 'john' }; renderDropdown({ user: userWithLowercase, onLogout: mockOnLogout }); const button = screen.getByRole('button', { name: /user menu/i }); expect(button).toHaveTextContent('J'); }); test('should handle empty username gracefully', () => { const userWithEmptyName = { ...mockRegularUser, username: '' }; renderDropdown({ user: userWithEmptyName, onLogout: mockOnLogout }); const button = screen.getByRole('button', { name: /user menu/i }); expect(button).toHaveTextContent('?'); }); test('should handle single character username', () => { const userWithSingleChar = { ...mockRegularUser, username: 'a' }; renderDropdown({ user: userWithSingleChar, onLogout: mockOnLogout }); const button = screen.getByRole('button', { name: /user menu/i }); expect(button).toHaveTextContent('A'); }); test('should handle usernames with special characters', () => { const userWithSpecialChar = { ...mockRegularUser, username: '!john' }; renderDropdown({ user: userWithSpecialChar, onLogout: mockOnLogout }); const button = screen.getByRole('button', { name: /user menu/i }); expect(button).toHaveTextContent('!'); }); }); });