diff --git a/frontend/src/__tests__/i18n.test.ts b/frontend/src/__tests__/i18n.test.ts new file mode 100644 index 00000000..cac1d1b0 --- /dev/null +++ b/frontend/src/__tests__/i18n.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import i18n from '../i18n' + +describe('i18n configuration', () => { + beforeEach(async () => { + await i18n.changeLanguage('en') + }) + + it('initializes with default language', () => { + expect(i18n.language).toBeDefined() + expect(i18n.isInitialized).toBe(true) + }) + + it('has all required language resources', () => { + const languages = ['en', 'es', 'fr', 'de', 'zh'] + languages.forEach((lang) => { + expect(i18n.hasResourceBundle(lang, 'translation')).toBe(true) + }) + }) + + it('translates common keys', () => { + expect(i18n.t('common.save')).toBe('Save') + expect(i18n.t('common.cancel')).toBe('Cancel') + expect(i18n.t('common.delete')).toBe('Delete') + }) + + it('translates navigation keys', () => { + expect(i18n.t('navigation.dashboard')).toBe('Dashboard') + expect(i18n.t('navigation.settings')).toBe('Settings') + }) + + it('changes language and translates correctly', async () => { + await i18n.changeLanguage('es') + expect(i18n.t('common.save')).toBe('Guardar') + expect(i18n.t('common.cancel')).toBe('Cancelar') + + await i18n.changeLanguage('fr') + expect(i18n.t('common.save')).toBe('Enregistrer') + expect(i18n.t('common.cancel')).toBe('Annuler') + + await i18n.changeLanguage('de') + expect(i18n.t('common.save')).toBe('Speichern') + expect(i18n.t('common.cancel')).toBe('Abbrechen') + + await i18n.changeLanguage('zh') + expect(i18n.t('common.save')).toBe('保存') + expect(i18n.t('common.cancel')).toBe('取消') + }) + + it('falls back to English for missing translations', async () => { + await i18n.changeLanguage('en') + const key = 'nonexistent.key' + expect(i18n.t(key)).toBe(key) // Should return the key itself + }) + + it('supports interpolation', () => { + expect(i18n.t('dashboard.activeHosts', { count: 5 })).toBe('5 active') + }) +}) diff --git a/frontend/src/components/__tests__/LanguageSelector.test.tsx b/frontend/src/components/__tests__/LanguageSelector.test.tsx new file mode 100644 index 00000000..26b3209c --- /dev/null +++ b/frontend/src/components/__tests__/LanguageSelector.test.tsx @@ -0,0 +1,60 @@ +import { describe, it, expect, vi } from 'vitest' +import { render, screen, fireEvent } from '@testing-library/react' +import { LanguageSelector } from '../LanguageSelector' +import { LanguageProvider } from '../../context/LanguageContext' + +// Mock i18next +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + i18n: { + changeLanguage: vi.fn(), + language: 'en', + }, + }), +})) + +describe('LanguageSelector', () => { + const renderWithProvider = () => { + return render( + + + + ) + } + + it('renders language selector with all options', () => { + renderWithProvider() + + const select = screen.getByRole('combobox') + expect(select).toBeInTheDocument() + + // Check that all language options are available + const options = screen.getAllByRole('option') + expect(options).toHaveLength(5) + expect(options[0]).toHaveTextContent('English') + expect(options[1]).toHaveTextContent('Español') + expect(options[2]).toHaveTextContent('Français') + expect(options[3]).toHaveTextContent('Deutsch') + expect(options[4]).toHaveTextContent('中文') + }) + + it('displays globe icon', () => { + const { container } = renderWithProvider() + const svgElement = container.querySelector('svg') + expect(svgElement).toBeInTheDocument() + }) + + it('changes language when option is selected', () => { + renderWithProvider() + + const select = screen.getByRole('combobox') as HTMLSelectElement + expect(select.value).toBe('en') + + fireEvent.change(select, { target: { value: 'es' } }) + expect(select.value).toBe('es') + + fireEvent.change(select, { target: { value: 'fr' } }) + expect(select.value).toBe('fr') + }) +}) diff --git a/frontend/src/hooks/__tests__/useLanguage.test.tsx b/frontend/src/hooks/__tests__/useLanguage.test.tsx new file mode 100644 index 00000000..ff776965 --- /dev/null +++ b/frontend/src/hooks/__tests__/useLanguage.test.tsx @@ -0,0 +1,89 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderHook, act } from '@testing-library/react' +import { ReactNode } from 'react' +import { useLanguage } from '../useLanguage' +import { LanguageProvider } from '../../context/LanguageContext' + +// Mock i18next +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + i18n: { + changeLanguage: vi.fn(), + language: 'en', + }, + }), +})) + +describe('useLanguage', () => { + beforeEach(() => { + localStorage.clear() + vi.clearAllMocks() + }) + + it('throws error when used outside LanguageProvider', () => { + // Suppress console.error for this test as React logs the error + const consoleSpy = vi.spyOn(console, 'error') + consoleSpy.mockImplementation(() => {}) + + expect(() => { + renderHook(() => useLanguage()) + }).toThrow('useLanguage must be used within a LanguageProvider') + + consoleSpy.mockRestore() + }) + + it('provides default language', () => { + const wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ) + + const { result } = renderHook(() => useLanguage(), { wrapper }) + + expect(result.current.language).toBe('en') + }) + + it('changes language', () => { + const wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ) + + const { result } = renderHook(() => useLanguage(), { wrapper }) + + act(() => { + result.current.setLanguage('es') + }) + + expect(result.current.language).toBe('es') + expect(localStorage.getItem('charon-language')).toBe('es') + }) + + it('persists language selection', () => { + localStorage.setItem('charon-language', 'fr') + + const wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ) + + const { result } = renderHook(() => useLanguage(), { wrapper }) + + expect(result.current.language).toBe('fr') + }) + + it('supports all configured languages', () => { + const wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ) + + const { result } = renderHook(() => useLanguage(), { wrapper }) + + const languages = ['en', 'es', 'fr', 'de', 'zh'] as const + + languages.forEach((lang) => { + act(() => { + result.current.setLanguage(lang) + }) + expect(result.current.language).toBe(lang) + }) + }) +})