/** * Unit tests for LanguageSelector component * * Tests cover: * - Rendering both dropdown and inline variants * - Current language display * - Dropdown open/close functionality * - Language selection and change * - Available languages display * - Flag display * - Click outside to close dropdown * - Accessibility attributes * - Responsive text hiding * - Custom className prop */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; import LanguageSelector from '../LanguageSelector'; // Mock i18n const mockChangeLanguage = vi.fn(); const mockCurrentLanguage = 'en'; vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, i18n: { language: mockCurrentLanguage, changeLanguage: mockChangeLanguage, }, }), })); // Mock i18n module with supported languages vi.mock('../../i18n', () => ({ supportedLanguages: [ { code: 'en', name: 'English', flag: '🇺🇸' }, { code: 'es', name: 'Español', flag: '🇪🇸' }, { code: 'fr', name: 'Français', flag: '🇫🇷' }, { code: 'de', name: 'Deutsch', flag: '🇩🇪' }, ], })); describe('LanguageSelector', () => { beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.clearAllMocks(); }); describe('Dropdown Variant (Default)', () => { describe('Rendering', () => { it('should render the language selector button', () => { render(); const button = screen.getByRole('button', { expanded: false }); expect(button).toBeInTheDocument(); }); it('should display current language name on desktop', () => { render(); const languageName = screen.getByText('English'); expect(languageName).toBeInTheDocument(); expect(languageName).toHaveClass('hidden', 'sm:inline'); }); it('should display current language flag by default', () => { render(); const flag = screen.getByText('🇺🇸'); expect(flag).toBeInTheDocument(); }); it('should display Globe icon', () => { render(); const button = screen.getByRole('button'); const svg = button.querySelector('svg'); expect(svg).toBeInTheDocument(); }); it('should display ChevronDown icon', () => { render(); const button = screen.getByRole('button'); const chevron = button.querySelector('svg.w-4.h-4.transition-transform'); expect(chevron).toBeInTheDocument(); }); it('should not display flag when showFlag is false', () => { render(); const flag = screen.queryByText('🇺🇸'); expect(flag).not.toBeInTheDocument(); }); it('should not show dropdown by default', () => { render(); const dropdown = screen.queryByRole('listbox'); expect(dropdown).not.toBeInTheDocument(); }); }); describe('Dropdown Open/Close', () => { it('should open dropdown when button clicked', () => { render(); const button = screen.getByRole('button'); fireEvent.click(button); const dropdown = screen.getByRole('listbox'); expect(dropdown).toBeInTheDocument(); expect(button).toHaveAttribute('aria-expanded', 'true'); }); it('should close dropdown when button clicked again', () => { render(); const button = screen.getByRole('button'); // Open fireEvent.click(button); expect(screen.getByRole('listbox')).toBeInTheDocument(); // Close fireEvent.click(button); expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); expect(button).toHaveAttribute('aria-expanded', 'false'); }); it('should rotate chevron icon when dropdown is open', () => { render(); const button = screen.getByRole('button'); const chevron = button.querySelector('svg.w-4.h-4.transition-transform'); // Initially not rotated expect(chevron).not.toHaveClass('rotate-180'); // Open dropdown fireEvent.click(button); expect(chevron).toHaveClass('rotate-180'); }); it('should close dropdown when clicking outside', async () => { render(
); const button = screen.getByRole('button', { expanded: false }); fireEvent.click(button); expect(screen.getByRole('listbox')).toBeInTheDocument(); // Click outside const outsideButton = screen.getByText('Outside Button'); fireEvent.mouseDown(outsideButton); await waitFor(() => { expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); }); }); it('should not close dropdown when clicking inside dropdown', () => { render(); const button = screen.getByRole('button'); fireEvent.click(button); const dropdown = screen.getByRole('listbox'); fireEvent.mouseDown(dropdown); expect(screen.getByRole('listbox')).toBeInTheDocument(); }); }); describe('Language Selection', () => { it('should display all available languages in dropdown', () => { render(); const button = screen.getByRole('button'); fireEvent.click(button); expect(screen.getAllByText('English')).toHaveLength(2); // One in button, one in dropdown expect(screen.getByText('Español')).toBeInTheDocument(); expect(screen.getByText('Français')).toBeInTheDocument(); expect(screen.getByText('Deutsch')).toBeInTheDocument(); }); it('should display flags for all languages in dropdown', () => { render(); const button = screen.getByRole('button'); fireEvent.click(button); expect(screen.getAllByText('🇺🇸')).toHaveLength(2); // One in button, one in dropdown expect(screen.getByText('🇪🇸')).toBeInTheDocument(); expect(screen.getByText('🇫🇷')).toBeInTheDocument(); expect(screen.getByText('🇩🇪')).toBeInTheDocument(); }); it('should mark current language with Check icon', () => { render(); const button = screen.getByRole('button'); fireEvent.click(button); const options = screen.getAllByRole('option'); const englishOption = options.find(opt => opt.textContent?.includes('English')); expect(englishOption).toHaveAttribute('aria-selected', 'true'); // Check icon should be present const checkIcon = englishOption?.querySelector('svg.w-4.h-4'); expect(checkIcon).toBeInTheDocument(); }); it('should change language when option clicked', async () => { render(); const button = screen.getByRole('button'); fireEvent.click(button); const spanishOption = screen.getAllByRole('option').find( opt => opt.textContent?.includes('Español') ); fireEvent.click(spanishOption!); await waitFor(() => { expect(mockChangeLanguage).toHaveBeenCalledWith('es'); }); }); it('should close dropdown after language selection', async () => { render(); const button = screen.getByRole('button'); fireEvent.click(button); const frenchOption = screen.getAllByRole('option').find( opt => opt.textContent?.includes('Français') ); fireEvent.click(frenchOption!); await waitFor(() => { expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); }); }); it('should highlight selected language with brand color', () => { render(); const button = screen.getByRole('button'); fireEvent.click(button); const options = screen.getAllByRole('option'); const englishOption = options.find(opt => opt.textContent?.includes('English')); expect(englishOption).toHaveClass('bg-brand-50', 'dark:bg-brand-900/20'); expect(englishOption).toHaveClass('text-brand-700', 'dark:text-brand-300'); }); it('should not highlight non-selected languages with brand color', () => { render(); const button = screen.getByRole('button'); fireEvent.click(button); const options = screen.getAllByRole('option'); const spanishOption = options.find(opt => opt.textContent?.includes('Español')); expect(spanishOption).toHaveClass('text-gray-700', 'dark:text-gray-300'); expect(spanishOption).not.toHaveClass('bg-brand-50'); }); }); describe('Accessibility', () => { it('should have proper ARIA attributes on button', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveAttribute('aria-expanded', 'false'); expect(button).toHaveAttribute('aria-haspopup', 'listbox'); }); it('should update aria-expanded when dropdown opens', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveAttribute('aria-expanded', 'false'); fireEvent.click(button); expect(button).toHaveAttribute('aria-expanded', 'true'); }); it('should have aria-label on listbox', () => { render(); const button = screen.getByRole('button'); fireEvent.click(button); const listbox = screen.getByRole('listbox'); expect(listbox).toHaveAttribute('aria-label', 'Select language'); }); it('should mark language options as selected correctly', () => { render(); const button = screen.getByRole('button'); fireEvent.click(button); const options = screen.getAllByRole('option'); const englishOption = options.find(opt => opt.textContent?.includes('English')); const spanishOption = options.find(opt => opt.textContent?.includes('Español')); expect(englishOption).toHaveAttribute('aria-selected', 'true'); expect(spanishOption).toHaveAttribute('aria-selected', 'false'); }); }); describe('Styling', () => { it('should apply default classes to button', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveClass('flex', 'items-center', 'gap-2'); expect(button).toHaveClass('px-3', 'py-2'); expect(button).toHaveClass('rounded-lg'); expect(button).toHaveClass('transition-colors'); }); it('should apply custom className when provided', () => { render(); const container = screen.getByRole('button').parentElement; expect(container).toHaveClass('custom-class'); }); it('should apply dropdown animation classes', () => { render(); const button = screen.getByRole('button'); fireEvent.click(button); const dropdown = screen.getByRole('listbox').parentElement; expect(dropdown).toHaveClass('animate-in', 'fade-in', 'slide-in-from-top-2'); }); it('should apply focus ring on button', () => { render(); const button = screen.getByRole('button'); expect(button).toHaveClass('focus:outline-none', 'focus:ring-2', 'focus:ring-brand-500'); }); }); }); describe('Inline Variant', () => { describe('Rendering', () => { it('should render inline variant when specified', () => { render(); // Should show buttons, not a dropdown const buttons = screen.getAllByRole('button'); expect(buttons.length).toBe(4); // One for each language }); it('should display all languages as separate buttons', () => { render(); expect(screen.getByText('English')).toBeInTheDocument(); expect(screen.getByText('Español')).toBeInTheDocument(); expect(screen.getByText('Français')).toBeInTheDocument(); expect(screen.getByText('Deutsch')).toBeInTheDocument(); }); it('should display flags in inline variant by default', () => { render(); expect(screen.getByText('🇺🇸')).toBeInTheDocument(); expect(screen.getByText('🇪🇸')).toBeInTheDocument(); expect(screen.getByText('🇫🇷')).toBeInTheDocument(); expect(screen.getByText('🇩🇪')).toBeInTheDocument(); }); it('should not display flags when showFlag is false', () => { render(); expect(screen.queryByText('🇺🇸')).not.toBeInTheDocument(); expect(screen.queryByText('🇪🇸')).not.toBeInTheDocument(); }); it('should highlight current language button', () => { render(); const englishButton = screen.getByRole('button', { name: /English/i }); expect(englishButton).toHaveClass('bg-brand-600', 'text-white'); }); it('should not highlight non-selected language buttons', () => { render(); const spanishButton = screen.getByRole('button', { name: /Español/i }); expect(spanishButton).toHaveClass('bg-gray-100', 'text-gray-700'); expect(spanishButton).not.toHaveClass('bg-brand-600'); }); }); describe('Language Selection', () => { it('should change language when button clicked', async () => { render(); const frenchButton = screen.getByRole('button', { name: /Français/i }); fireEvent.click(frenchButton); await waitFor(() => { expect(mockChangeLanguage).toHaveBeenCalledWith('fr'); }); }); it('should change language for each available language', async () => { render(); const germanButton = screen.getByRole('button', { name: /Deutsch/i }); fireEvent.click(germanButton); await waitFor(() => { expect(mockChangeLanguage).toHaveBeenCalledWith('de'); }); }); }); describe('Styling', () => { it('should apply flex layout classes', () => { const { container } = render(); const wrapper = container.firstChild; expect(wrapper).toHaveClass('flex', 'flex-wrap', 'gap-2'); }); it('should apply custom className when provided', () => { const { container } = render(); const wrapper = container.firstChild; expect(wrapper).toHaveClass('my-custom-class'); }); it('should apply button styling classes', () => { render(); const buttons = screen.getAllByRole('button'); buttons.forEach(button => { expect(button).toHaveClass('px-3', 'py-1.5', 'rounded-lg', 'text-sm', 'font-medium', 'transition-colors'); }); }); it('should apply hover classes to non-selected buttons', () => { render(); const spanishButton = screen.getByRole('button', { name: /Español/i }); expect(spanishButton).toHaveClass('hover:bg-gray-200', 'dark:hover:bg-gray-600'); }); }); }); describe('Integration', () => { it('should render correctly with all dropdown props together', () => { render( ); const button = screen.getByRole('button'); expect(button).toBeInTheDocument(); expect(screen.getByText('English')).toBeInTheDocument(); expect(screen.getByText('🇺🇸')).toBeInTheDocument(); const container = button.parentElement; expect(container).toHaveClass('custom-class'); }); it('should render correctly with all inline props together', () => { const { container } = render( ); const wrapper = container.firstChild; expect(wrapper).toHaveClass('inline-custom'); const buttons = screen.getAllByRole('button'); expect(buttons.length).toBe(4); expect(screen.getByText('🇺🇸')).toBeInTheDocument(); expect(screen.getByText('English')).toBeInTheDocument(); }); it('should maintain dropdown functionality across re-renders', () => { const { rerender } = render(); const button = screen.getByRole('button'); fireEvent.click(button); expect(screen.getByRole('listbox')).toBeInTheDocument(); rerender(); expect(screen.getByRole('listbox')).toBeInTheDocument(); }); }); describe('Edge Cases', () => { it('should handle missing language gracefully', () => { // The component should fall back to the first language if current language is not found render(); // Should still render without crashing const button = screen.getByRole('button'); expect(button).toBeInTheDocument(); }); it('should cleanup event listener on unmount', () => { const { unmount } = render(); const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener'); unmount(); expect(removeEventListenerSpy).toHaveBeenCalledWith('mousedown', expect.any(Function)); }); it('should not call changeLanguage when clicking current language', async () => { render(); const englishButton = screen.getByRole('button', { name: /English/i }); fireEvent.click(englishButton); await waitFor(() => { expect(mockChangeLanguage).toHaveBeenCalledWith('en'); }); // Even if clicking the current language, it still calls changeLanguage // This is expected behavior (idempotent) }); }); });