Files
Aniworld/tests/unit/test_i18n.js
Lukas d74c181556 Update test files with refinements and fixes
- test_anime_endpoints.py: Minor updates
- test_download_retry.py: Refinements
- test_i18n.js: Updates
- test_tmdb_client.py: Improvements
- test_tmdb_rate_limiting.py: Test enhancements
- test_user_preferences.js: Updates
2026-02-02 07:19:36 +01:00

497 lines
18 KiB
JavaScript

/**
* Unit tests for internationalization (i18n) functionality
* Tests translation loading, language switching, and text updates
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
describe('Localization', () => {
let localization;
let originalLocalStorage;
let originalNavigator;
beforeEach(() => {
// Mock localStorage
originalLocalStorage = global.localStorage;
global.localStorage = {
data: {},
getItem(key) {
return this.data[key] || null;
},
setItem(key, value) {
this.data[key] = value;
},
removeItem(key) {
delete this.data[key];
},
clear() {
this.data = {};
}
};
// Mock navigator
originalNavigator = global.navigator;
global.navigator = {
language: 'en-US',
userLanguage: 'en-US'
};
// Mock document
global.document = {
querySelectorAll: vi.fn(() => []),
getElementById: vi.fn(() => null)
};
// Load the Localization class
const LocalizationClass = class Localization {
constructor() {
this.currentLanguage = 'en';
this.fallbackLanguage = 'en';
this.translations = {};
this.loadTranslations();
}
loadTranslations() {
this.translations.en = {
'config-title': 'Configuration',
'toggle-theme': 'Toggle theme',
'search-placeholder': 'Search for anime...',
'series-collection': 'Series Collection',
'download-queue': 'Download Queue',
'loading': 'Loading...',
'close': 'Close',
'ok': 'OK'
};
this.translations.de = {
'config-title': 'Konfiguration',
'toggle-theme': 'Design wechseln',
'search-placeholder': 'Nach Anime suchen...',
'series-collection': 'Serien-Sammlung',
'download-queue': 'Download-Warteschlange',
'loading': 'Wird geladen...',
'close': 'Schließen',
'ok': 'OK'
};
const savedLanguage = localStorage.getItem('language') || this.detectLanguage();
this.setLanguage(savedLanguage);
}
detectLanguage() {
const browserLang = navigator.language || navigator.userLanguage;
const langCode = browserLang.split('-')[0];
return this.translations[langCode] ? langCode : this.fallbackLanguage;
}
setLanguage(langCode) {
if (this.translations[langCode]) {
this.currentLanguage = langCode;
localStorage.setItem('language', langCode);
this.updatePageTexts();
}
}
getText(key, fallback = key) {
const translation = this.translations[this.currentLanguage];
if (translation && translation[key]) {
return translation[key];
}
const fallbackTranslation = this.translations[this.fallbackLanguage];
if (fallbackTranslation && fallbackTranslation[key]) {
return fallbackTranslation[key];
}
return fallback;
}
updatePageTexts() {
document.querySelectorAll('[data-text]').forEach(element => {
const key = element.getAttribute('data-text');
const text = this.getText(key);
if (element.tagName === 'INPUT' && element.type === 'text') {
element.placeholder = text;
} else {
element.textContent = text;
}
});
}
getAvailableLanguages() {
return Object.keys(this.translations).map(code => ({
code: code,
name: this.getLanguageName(code)
}));
}
getLanguageName(code) {
const names = {
'en': 'English',
'de': 'Deutsch'
};
return names[code] || code.toUpperCase();
}
formatMessage(key, ...args) {
let message = this.getText(key);
args.forEach((arg, index) => {
message = message.replace(`{${index}}`, arg);
});
return message;
}
};
localization = new LocalizationClass();
});
afterEach(() => {
global.localStorage = originalLocalStorage;
global.navigator = originalNavigator;
});
describe('Initialization', () => {
it('should initialize with default English language', () => {
expect(localization.currentLanguage).toBe('en');
expect(localization.fallbackLanguage).toBe('en');
});
it('should load English translations', () => {
expect(localization.translations.en).toBeDefined();
expect(localization.translations.en['config-title']).toBe('Configuration');
});
it('should load German translations', () => {
expect(localization.translations.de).toBeDefined();
expect(localization.translations.de['config-title']).toBe('Konfiguration');
});
it('should detect browser language', () => {
global.navigator.language = 'de-DE';
const langCode = localization.detectLanguage();
expect(langCode).toBe('de');
});
it('should fallback to English for unsupported languages', () => {
global.navigator.language = 'fr-FR';
const langCode = localization.detectLanguage();
expect(langCode).toBe('en');
});
it('should load saved language from localStorage', () => {
localStorage.setItem('language', 'de');
const LocalizationClass = localization.constructor;
const newInstance = new LocalizationClass();
expect(newInstance.currentLanguage).toBe('de');
});
});
describe('Language Switching', () => {
it('should switch to German', () => {
localization.setLanguage('de');
expect(localization.currentLanguage).toBe('de');
});
it('should switch to English', () => {
localization.setLanguage('en');
expect(localization.currentLanguage).toBe('en');
});
it('should save language preference to localStorage', () => {
localization.setLanguage('de');
expect(localStorage.getItem('language')).toBe('de');
});
it('should not switch to unsupported language', () => {
const originalLang = localization.currentLanguage;
localization.setLanguage('fr');
expect(localization.currentLanguage).toBe(originalLang);
});
it('should update page texts when switching language', () => {
const mockElements = [
{
getAttribute: () => 'config-title',
textContent: '',
tagName: 'H1'
}
];
document.querySelectorAll = vi.fn(() => mockElements);
localization.setLanguage('de');
expect(document.querySelectorAll).toHaveBeenCalledWith('[data-text]');
});
});
describe('Text Retrieval', () => {
it('should get English text for current language', () => {
localization.setLanguage('en');
const text = localization.getText('config-title');
expect(text).toBe('Configuration');
});
it('should get German text for current language', () => {
localization.setLanguage('de');
const text = localization.getText('config-title');
expect(text).toBe('Konfiguration');
});
it('should return fallback for missing key in current language', () => {
localization.setLanguage('de');
const text = localization.getText('nonexistent-key', 'Fallback');
expect(text).toBe('Fallback');
});
it('should return key as fallback when no translation exists', () => {
const text = localization.getText('missing-key');
expect(text).toBe('missing-key');
});
it('should fallback to English for missing German translations', () => {
localization.setLanguage('de');
delete localization.translations.de['loading'];
const text = localization.getText('loading');
expect(text).toBe('Loading...');
});
});
describe('Page Text Updates', () => {
it('should update element text content', () => {
const mockElement = {
getAttribute: () => 'config-title',
textContent: '',
tagName: 'H1'
};
document.querySelectorAll = vi.fn(() => [mockElement]);
localization.setLanguage('en');
localization.updatePageTexts();
expect(mockElement.textContent).toBe('Configuration');
});
it('should update input placeholder', () => {
const mockInput = {
getAttribute: () => 'search-placeholder',
placeholder: '',
tagName: 'INPUT',
type: 'text'
};
document.querySelectorAll = vi.fn(() => [mockInput]);
localization.setLanguage('en');
localization.updatePageTexts();
expect(mockInput.placeholder).toBe('Search for anime...');
});
it('should update multiple elements', () => {
const mockElements = [
{ getAttribute: () => 'config-title', textContent: '', tagName: 'H1' },
{ getAttribute: () => 'loading', textContent: '', tagName: 'SPAN' },
{ getAttribute: () => 'close', textContent: '', tagName: 'BUTTON' }
];
document.querySelectorAll = vi.fn(() => mockElements);
localization.updatePageTexts();
expect(mockElements[0].textContent).toBe('Configuration');
expect(mockElements[1].textContent).toBe('Loading...');
expect(mockElements[2].textContent).toBe('Close');
});
it('should handle missing elements gracefully', () => {
document.querySelectorAll = vi.fn(() => []);
expect(() => localization.updatePageTexts()).not.toThrow();
});
});
describe('Available Languages', () => {
it('should return list of available languages', () => {
const languages = localization.getAvailableLanguages();
expect(languages).toHaveLength(2);
expect(languages).toContainEqual({ code: 'en', name: 'English' });
expect(languages).toContainEqual({ code: 'de', name: 'Deutsch' });
});
it('should get English language name', () => {
const name = localization.getLanguageName('en');
expect(name).toBe('English');
});
it('should get German language name', () => {
const name = localization.getLanguageName('de');
expect(name).toBe('Deutsch');
});
it('should return uppercase code for unknown language', () => {
const name = localization.getLanguageName('fr');
expect(name).toBe('FR');
});
});
describe('Message Formatting', () => {
it('should format message with single argument', () => {
localization.translations.en['welcome-message'] = 'Welcome, {0}!';
const formatted = localization.formatMessage('welcome-message', 'John');
expect(formatted).toBe('Welcome, John!');
});
it('should format message with multiple arguments', () => {
localization.translations.en['greeting'] = 'Hello {0}, you have {1} new messages';
const formatted = localization.formatMessage('greeting', 'Alice', '5');
expect(formatted).toBe('Hello Alice, you have 5 new messages');
});
it('should handle missing placeholders', () => {
localization.translations.en['simple'] = 'No placeholders here';
const formatted = localization.formatMessage('simple', 'unused');
expect(formatted).toBe('No placeholders here');
});
it('should format message in German', () => {
localization.setLanguage('de');
localization.translations.de['welcome-message'] = 'Willkommen, {0}!';
const formatted = localization.formatMessage('welcome-message', 'Johann');
expect(formatted).toBe('Willkommen, Johann!');
});
});
describe('Translation Completeness', () => {
it('should have same keys in English and German', () => {
const enKeys = Object.keys(localization.translations.en);
const deKeys = Object.keys(localization.translations.de);
expect(enKeys.sort()).toEqual(deKeys.sort());
});
it('should not have empty translations', () => {
const checkTranslations = (lang) => {
const translations = localization.translations[lang];
Object.entries(translations).forEach(([key, value]) => {
expect(value).toBeTruthy();
expect(value.trim()).not.toBe('');
});
};
checkTranslations('en');
checkTranslations('de');
});
it('should have non-identical translations for different languages', () => {
const enKeys = Object.keys(localization.translations.en);
let identicalCount = 0;
enKeys.forEach(key => {
if (localization.translations.en[key] === localization.translations.de[key]) {
identicalCount++;
}
});
// Some translations might be identical (e.g., "OK"), but not all
expect(identicalCount).toBeLessThan(enKeys.length);
});
});
describe('Edge Cases', () => {
it('should handle null key', () => {
const text = localization.getText(null);
expect(text).toBe(null);
});
it('should handle undefined key', () => {
const text = localization.getText(undefined);
expect(text).toBe(undefined);
});
it('should handle empty string key', () => {
const text = localization.getText('');
expect(text).toBe('');
});
it('should handle rapid language switching', () => {
localization.setLanguage('de');
localization.setLanguage('en');
localization.setLanguage('de');
localization.setLanguage('en');
expect(localization.currentLanguage).toBe('en');
});
it('should handle localStorage errors gracefully', () => {
const originalSetItem = localStorage.setItem;
localStorage.setItem = () => {
throw new Error('localStorage full');
};
expect(() => localization.setLanguage('de')).not.toThrow();
localStorage.setItem = originalSetItem;
});
it('should handle missing navigator.language', () => {
delete global.navigator.language;
global.navigator.userLanguage = 'de-DE';
const langCode = localization.detectLanguage();
expect(langCode).toBe('de');
});
it('should handle missing both language properties', () => {
delete global.navigator.language;
delete global.navigator.userLanguage;
const langCode = localization.detectLanguage();
expect(langCode).toBe('en');
});
});
describe('Integration with Document', () => {
it('should query for elements with data-text attribute', () => {
const spy = vi.spyOn(document, 'querySelectorAll');
localization.updatePageTexts();
expect(spy).toHaveBeenCalledWith('[data-text]');
});
it('should handle elements without getAttribute method', () => {
const mockElements = [
{ getAttribute: undefined, textContent: '', tagName: 'DIV' }
];
document.querySelectorAll = vi.fn(() => mockElements);
expect(() => localization.updatePageTexts()).not.toThrow();
});
it('should handle elements with null attributes', () => {
const mockElements = [
{ getAttribute: () => null, textContent: '', tagName: 'DIV' }
];
document.querySelectorAll = vi.fn(() => mockElements);
expect(() => localization.updatePageTexts()).not.toThrow();
});
});
describe('Persistence', () => {
it('should persist language across page reloads', () => {
localization.setLanguage('de');
const savedLang = localStorage.getItem('language');
expect(savedLang).toBe('de');
// Simulate page reload
const LocalizationClass = localization.constructor;
const newInstance = new LocalizationClass();
expect(newInstance.currentLanguage).toBe('de');
});
it('should clear previous language when switching', () => {
localization.setLanguage('de');
expect(localStorage.getItem('language')).toBe('de');
localization.setLanguage('en');
expect(localStorage.getItem('language')).toBe('en');
});
});
});