/** * Unit tests for TopBar component * * Tests the top navigation bar that appears at the top of the application. * Covers: * - Rendering of all UI elements (search, theme toggle, notifications, etc.) * - Menu button for mobile view * - Theme toggle functionality * - User profile dropdown integration * - Language selector integration * - Notification dropdown integration * - Sandbox toggle integration * - Search input */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; import TopBar from '../TopBar'; import { User } from '../../types'; // Mock child components vi.mock('../UserProfileDropdown', () => ({ default: ({ user }: { user: User }) => (
User: {user.email}
), })); vi.mock('../LanguageSelector', () => ({ default: () =>
Language Selector
, })); vi.mock('../NotificationDropdown', () => ({ default: ({ onTicketClick }: { onTicketClick?: (id: string) => void }) => (
Notifications
), })); vi.mock('../SandboxToggle', () => ({ default: ({ isSandbox, sandboxEnabled, onToggle, isToggling }: any) => (
Sandbox: {isSandbox ? 'On' : 'Off'}
), })); // Mock SandboxContext const mockUseSandbox = vi.fn(); vi.mock('../../contexts/SandboxContext', () => ({ useSandbox: () => mockUseSandbox(), })); // Mock react-i18next vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => { const translations: Record = { 'common.search': 'Search...', }; return translations[key] || key; }, }), })); // Test data factory for User objects const createMockUser = (overrides?: Partial): User => ({ id: '1', email: 'test@example.com', firstName: 'Test', lastName: 'User', role: 'owner', phone: '+1234567890', preferences: { email: true, sms: false, in_app: true, }, twoFactorEnabled: false, profilePictureUrl: undefined, ...overrides, }); // Wrapper component that provides router context const renderWithRouter = (ui: React.ReactElement) => { return render({ui}); }; describe('TopBar', () => { const mockToggleTheme = vi.fn(); const mockOnMenuClick = vi.fn(); const mockOnTicketClick = vi.fn(); beforeEach(() => { vi.clearAllMocks(); mockUseSandbox.mockReturnValue({ isSandbox: false, sandboxEnabled: true, toggleSandbox: vi.fn(), isToggling: false, }); }); describe('Rendering', () => { it('should render the top bar with all main elements', () => { const user = createMockUser(); renderWithRouter( ); expect(screen.getByTestId('user-profile-dropdown')).toBeInTheDocument(); expect(screen.getByTestId('language-selector')).toBeInTheDocument(); expect(screen.getByTestId('notification-dropdown')).toBeInTheDocument(); expect(screen.getByTestId('sandbox-toggle')).toBeInTheDocument(); }); it('should render search input on desktop', () => { const user = createMockUser(); renderWithRouter( ); const searchInput = screen.getByPlaceholderText('Search...'); expect(searchInput).toBeInTheDocument(); expect(searchInput).toHaveClass('w-full'); }); it('should render mobile menu button', () => { const user = createMockUser(); renderWithRouter( ); const menuButton = screen.getByLabelText('Open sidebar'); expect(menuButton).toBeInTheDocument(); }); it('should pass user to UserProfileDropdown', () => { const user = createMockUser({ email: 'john@example.com' }); renderWithRouter( ); expect(screen.getByText('User: john@example.com')).toBeInTheDocument(); }); it('should render with dark mode styles when isDarkMode is true', () => { const user = createMockUser(); const { container } = renderWithRouter( ); const header = container.querySelector('header'); expect(header).toHaveClass('dark:bg-gray-800', 'dark:border-gray-700'); }); }); describe('Theme Toggle', () => { it('should render moon icon when in light mode', () => { const user = createMockUser(); renderWithRouter( ); // The button should exist const buttons = screen.getAllByRole('button'); const themeButton = buttons.find(btn => btn.className.includes('text-gray-400') ); expect(themeButton).toBeInTheDocument(); }); it('should render sun icon when in dark mode', () => { const user = createMockUser(); renderWithRouter( ); // The button should exist const buttons = screen.getAllByRole('button'); const themeButton = buttons.find(btn => btn.className.includes('text-gray-400') ); expect(themeButton).toBeInTheDocument(); }); it('should call toggleTheme when theme button is clicked', () => { const user = createMockUser(); renderWithRouter( ); // Find the theme toggle button by finding buttons, then clicking the one with the theme classes const buttons = screen.getAllByRole('button'); // The theme button is the one with the hover styles and not the menu button const themeButton = buttons.find(btn => btn.className.includes('text-gray-400') && btn.className.includes('hover:text-gray-600') && !btn.getAttribute('aria-label') ); expect(themeButton).toBeTruthy(); if (themeButton) { fireEvent.click(themeButton); expect(mockToggleTheme).toHaveBeenCalledTimes(1); } }); }); describe('Mobile Menu Button', () => { it('should render menu button with correct aria-label', () => { const user = createMockUser(); renderWithRouter( ); const menuButton = screen.getByLabelText('Open sidebar'); expect(menuButton).toHaveAttribute('aria-label', 'Open sidebar'); }); it('should call onMenuClick when menu button is clicked', () => { const user = createMockUser(); renderWithRouter( ); const menuButton = screen.getByLabelText('Open sidebar'); fireEvent.click(menuButton); expect(mockOnMenuClick).toHaveBeenCalledTimes(1); }); it('should have mobile-only classes on menu button', () => { const user = createMockUser(); const { container } = renderWithRouter( ); const menuButton = screen.getByLabelText('Open sidebar'); expect(menuButton).toHaveClass('md:hidden'); }); }); describe('Search Input', () => { it('should render search input with correct placeholder', () => { const user = createMockUser(); renderWithRouter( ); const searchInput = screen.getByPlaceholderText('Search...'); expect(searchInput).toHaveAttribute('type', 'text'); }); it('should have search icon', () => { const user = createMockUser(); renderWithRouter( ); // Search icon should be present const searchInput = screen.getByPlaceholderText('Search...'); expect(searchInput.parentElement?.querySelector('span')).toBeInTheDocument(); }); it('should allow typing in search input', () => { const user = createMockUser(); renderWithRouter( ); const searchInput = screen.getByPlaceholderText('Search...') as HTMLInputElement; fireEvent.change(searchInput, { target: { value: 'test query' } }); expect(searchInput.value).toBe('test query'); }); it('should have focus styles on search input', () => { const user = createMockUser(); renderWithRouter( ); const searchInput = screen.getByPlaceholderText('Search...'); expect(searchInput).toHaveClass('focus:outline-none', 'focus:border-brand-500'); }); }); describe('Sandbox Integration', () => { it('should render SandboxToggle component', () => { const user = createMockUser(); renderWithRouter( ); expect(screen.getByTestId('sandbox-toggle')).toBeInTheDocument(); }); it('should pass sandbox state to SandboxToggle', () => { const user = createMockUser(); mockUseSandbox.mockReturnValue({ isSandbox: true, sandboxEnabled: true, toggleSandbox: vi.fn(), isToggling: false, }); renderWithRouter( ); expect(screen.getByText(/Sandbox: On/i)).toBeInTheDocument(); }); it('should handle sandbox toggle being disabled', () => { const user = createMockUser(); mockUseSandbox.mockReturnValue({ isSandbox: false, sandboxEnabled: false, toggleSandbox: vi.fn(), isToggling: false, }); renderWithRouter( ); expect(screen.getByTestId('sandbox-toggle')).toBeInTheDocument(); }); }); describe('Notification Integration', () => { it('should render NotificationDropdown', () => { const user = createMockUser(); renderWithRouter( ); expect(screen.getByTestId('notification-dropdown')).toBeInTheDocument(); }); it('should pass onTicketClick to NotificationDropdown when provided', () => { const user = createMockUser(); renderWithRouter( ); expect(screen.getByTestId('notification-dropdown')).toBeInTheDocument(); }); it('should work without onTicketClick prop', () => { const user = createMockUser(); renderWithRouter( ); expect(screen.getByTestId('notification-dropdown')).toBeInTheDocument(); }); }); describe('Language Selector Integration', () => { it('should render LanguageSelector', () => { const user = createMockUser(); renderWithRouter( ); expect(screen.getByTestId('language-selector')).toBeInTheDocument(); }); }); describe('Different User Roles', () => { it('should render for owner role', () => { const user = createMockUser({ role: 'owner' }); renderWithRouter( ); expect(screen.getByTestId('user-profile-dropdown')).toBeInTheDocument(); }); it('should render for manager role', () => { const user = createMockUser({ role: 'manager' }); renderWithRouter( ); expect(screen.getByTestId('user-profile-dropdown')).toBeInTheDocument(); }); it('should render for staff role', () => { const user = createMockUser({ role: 'staff' }); renderWithRouter( ); expect(screen.getByTestId('user-profile-dropdown')).toBeInTheDocument(); }); it('should render for platform roles', () => { const user = createMockUser({ role: 'platform_manager' }); renderWithRouter( ); expect(screen.getByTestId('user-profile-dropdown')).toBeInTheDocument(); }); }); describe('Layout and Styling', () => { it('should have fixed height', () => { const user = createMockUser(); const { container } = renderWithRouter( ); const header = container.querySelector('header'); expect(header).toHaveClass('h-16'); }); it('should have border at bottom', () => { const user = createMockUser(); const { container } = renderWithRouter( ); const header = container.querySelector('header'); expect(header).toHaveClass('border-b'); }); it('should use flexbox layout', () => { const user = createMockUser(); const { container } = renderWithRouter( ); const header = container.querySelector('header'); expect(header).toHaveClass('flex', 'items-center', 'justify-between'); }); it('should have responsive padding', () => { const user = createMockUser(); const { container } = renderWithRouter( ); const header = container.querySelector('header'); expect(header).toHaveClass('px-4', 'sm:px-8'); }); }); describe('Accessibility', () => { it('should have semantic header element', () => { const user = createMockUser(); const { container } = renderWithRouter( ); expect(container.querySelector('header')).toBeInTheDocument(); }); it('should have proper button roles', () => { const user = createMockUser(); renderWithRouter( ); const buttons = screen.getAllByRole('button'); expect(buttons.length).toBeGreaterThan(0); }); it('should have focus styles on interactive elements', () => { const user = createMockUser(); renderWithRouter( ); const menuButton = screen.getByLabelText('Open sidebar'); expect(menuButton).toHaveClass('focus:outline-none', 'focus:ring-2'); }); }); describe('Responsive Behavior', () => { it('should hide search on mobile', () => { const user = createMockUser(); const { container } = renderWithRouter( ); // Search container is a relative div with hidden md:block classes const searchContainer = container.querySelector('.hidden.md\\:block'); expect(searchContainer).toBeInTheDocument(); }); it('should show menu button only on mobile', () => { const user = createMockUser(); renderWithRouter( ); const menuButton = screen.getByLabelText('Open sidebar'); expect(menuButton).toHaveClass('md:hidden'); }); }); });