/** * Unit tests for user preferences functionality * Tests preference storage, loading, and application */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; describe('UserPreferences', () => { let originalLocalStorage; let originalDocument; let consoleErrorSpy; const STORAGE_KEY = 'aniworld_preferences'; // User Preferences module implementation const createUserPreferences = () => { const UserPrefs = { loadPreferences() { try { const stored = localStorage.getItem(STORAGE_KEY); if (stored) { const preferences = JSON.parse(stored); this.applyPreferences(preferences); return preferences; } return {}; } catch (error) { console.error('[User Preferences] Error loading:', error); return {}; } }, savePreferences(preferences) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences)); return true; } catch (error) { console.error('[User Preferences] Error saving:', error); return false; } }, applyPreferences(preferences) { if (preferences.theme) { document.documentElement.setAttribute('data-theme', preferences.theme); } if (preferences.language) { // Language preference would be applied here document.documentElement.setAttribute('lang', preferences.language); } }, getPreferences() { try { const stored = localStorage.getItem(STORAGE_KEY); return stored ? JSON.parse(stored) : {}; } catch (error) { console.error('[User Preferences] Error getting preferences:', error); return {}; } }, updatePreference(key, value) { const preferences = this.getPreferences(); preferences[key] = value; this.savePreferences(preferences); this.applyPreferences(preferences); }, resetPreferences() { try { localStorage.removeItem(STORAGE_KEY); return true; } catch (error) { console.error('[User Preferences] Error resetting:', error); return false; } } }; return UserPrefs; }; 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 document originalDocument = global.document; global.document = { documentElement: { attributes: {}, setAttribute(key, value) { this.attributes[key] = value; }, getAttribute(key) { return this.attributes[key] || null; }, removeAttribute(key) { delete this.attributes[key]; } }, readyState: 'complete', addEventListener: vi.fn() }; // Spy on console.error consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); }); afterEach(() => { global.localStorage = originalLocalStorage; global.document = originalDocument; consoleErrorSpy.mockRestore(); }); describe('Loading Preferences', () => { it('should load preferences from localStorage', () => { const UserPrefs = createUserPreferences(); const testPrefs = { theme: 'dark', language: 'de' }; localStorage.setItem(STORAGE_KEY, JSON.stringify(testPrefs)); const loaded = UserPrefs.loadPreferences(); expect(loaded).toEqual(testPrefs); }); it('should return empty object when no preferences exist', () => { const UserPrefs = createUserPreferences(); const loaded = UserPrefs.loadPreferences(); expect(loaded).toEqual({}); }); it('should handle invalid JSON gracefully', () => { const UserPrefs = createUserPreferences(); localStorage.setItem(STORAGE_KEY, 'invalid-json{'); const loaded = UserPrefs.loadPreferences(); expect(loaded).toEqual({}); expect(consoleErrorSpy).toHaveBeenCalled(); }); it('should handle localStorage errors', () => { const UserPrefs = createUserPreferences(); const originalGetItem = localStorage.getItem; localStorage.getItem = () => { throw new Error('localStorage error'); }; const loaded = UserPrefs.loadPreferences(); expect(loaded).toEqual({}); expect(consoleErrorSpy).toHaveBeenCalled(); localStorage.getItem = originalGetItem; }); it('should apply preferences after loading', () => { const UserPrefs = createUserPreferences(); const testPrefs = { theme: 'dark' }; localStorage.setItem(STORAGE_KEY, JSON.stringify(testPrefs)); UserPrefs.loadPreferences(); expect(document.documentElement.getAttribute('data-theme')).toBe('dark'); }); }); describe('Saving Preferences', () => { it('should save preferences to localStorage', () => { const UserPrefs = createUserPreferences(); const testPrefs = { theme: 'light', language: 'en' }; const result = UserPrefs.savePreferences(testPrefs); expect(result).toBe(true); expect(localStorage.getItem(STORAGE_KEY)).toBe(JSON.stringify(testPrefs)); }); it('should overwrite existing preferences', () => { const UserPrefs = createUserPreferences(); localStorage.setItem(STORAGE_KEY, JSON.stringify({ theme: 'dark' })); UserPrefs.savePreferences({ theme: 'light' }); const stored = JSON.parse(localStorage.getItem(STORAGE_KEY)); expect(stored.theme).toBe('light'); }); it('should handle localStorage errors', () => { const UserPrefs = createUserPreferences(); const originalSetItem = localStorage.setItem; localStorage.setItem = () => { throw new Error('localStorage full'); }; const result = UserPrefs.savePreferences({ theme: 'dark' }); expect(result).toBe(false); expect(consoleErrorSpy).toHaveBeenCalled(); localStorage.setItem = originalSetItem; }); it('should handle null preferences', () => { const UserPrefs = createUserPreferences(); expect(() => UserPrefs.savePreferences(null)).not.toThrow(); }); it('should handle undefined preferences', () => { const UserPrefs = createUserPreferences(); expect(() => UserPrefs.savePreferences(undefined)).not.toThrow(); }); }); describe('Getting Preferences', () => { it('should get stored preferences', () => { const UserPrefs = createUserPreferences(); const testPrefs = { theme: 'dark', language: 'de' }; localStorage.setItem(STORAGE_KEY, JSON.stringify(testPrefs)); const prefs = UserPrefs.getPreferences(); expect(prefs).toEqual(testPrefs); }); it('should return empty object when none exist', () => { const UserPrefs = createUserPreferences(); const prefs = UserPrefs.getPreferences(); expect(prefs).toEqual({}); }); it('should handle parse errors gracefully', () => { const UserPrefs = createUserPreferences(); localStorage.setItem(STORAGE_KEY, '{invalid}'); const prefs = UserPrefs.getPreferences(); expect(prefs).toEqual({}); expect(consoleErrorSpy).toHaveBeenCalled(); }); it('should not modify original stored data', () => { const UserPrefs = createUserPreferences(); const testPrefs = { theme: 'dark' }; localStorage.setItem(STORAGE_KEY, JSON.stringify(testPrefs)); const prefs = UserPrefs.getPreferences(); prefs.theme = 'light'; const storedPrefs = UserPrefs.getPreferences(); expect(storedPrefs.theme).toBe('dark'); }); }); describe('Applying Preferences', () => { it('should apply theme preference to document', () => { const UserPrefs = createUserPreferences(); const preferences = { theme: 'dark' }; UserPrefs.applyPreferences(preferences); expect(document.documentElement.getAttribute('data-theme')).toBe('dark'); }); it('should apply language preference to document', () => { const UserPrefs = createUserPreferences(); const preferences = { language: 'de' }; UserPrefs.applyPreferences(preferences); expect(document.documentElement.getAttribute('lang')).toBe('de'); }); it('should apply multiple preferences', () => { const UserPrefs = createUserPreferences(); const preferences = { theme: 'dark', language: 'de' }; UserPrefs.applyPreferences(preferences); expect(document.documentElement.getAttribute('data-theme')).toBe('dark'); expect(document.documentElement.getAttribute('lang')).toBe('de'); }); it('should handle empty preferences object', () => { const UserPrefs = createUserPreferences(); expect(() => UserPrefs.applyPreferences({})).not.toThrow(); }); it('should handle undefined preferences', () => { const UserPrefs = createUserPreferences(); expect(() => UserPrefs.applyPreferences(undefined)).not.toThrow(); }); it('should only apply defined preferences', () => { const UserPrefs = createUserPreferences(); const preferences = { theme: 'dark' }; UserPrefs.applyPreferences(preferences); expect(document.documentElement.getAttribute('data-theme')).toBe('dark'); expect(document.documentElement.getAttribute('lang')).toBeNull(); }); }); describe('Updating Specific Preference', () => { it('should update single preference', () => { const UserPrefs = createUserPreferences(); UserPrefs.updatePreference('theme', 'dark'); const prefs = UserPrefs.getPreferences(); expect(prefs.theme).toBe('dark'); }); it('should update existing preference', () => { const UserPrefs = createUserPreferences(); UserPrefs.savePreferences({ theme: 'light', language: 'en' }); UserPrefs.updatePreference('theme', 'dark'); const prefs = UserPrefs.getPreferences(); expect(prefs.theme).toBe('dark'); expect(prefs.language).toBe('en'); }); it('should add new preference to existing ones', () => { const UserPrefs = createUserPreferences(); UserPrefs.savePreferences({ theme: 'light' }); UserPrefs.updatePreference('language', 'de'); const prefs = UserPrefs.getPreferences(); expect(prefs.theme).toBe('light'); expect(prefs.language).toBe('de'); }); it('should apply preference after updating', () => { const UserPrefs = createUserPreferences(); UserPrefs.updatePreference('theme', 'dark'); expect(document.documentElement.getAttribute('data-theme')).toBe('dark'); }); it('should persist updated preference', () => { const UserPrefs = createUserPreferences(); UserPrefs.updatePreference('theme', 'dark'); const stored = JSON.parse(localStorage.getItem(STORAGE_KEY)); expect(stored.theme).toBe('dark'); }); }); describe('Resetting Preferences', () => { it('should remove preferences from localStorage', () => { const UserPrefs = createUserPreferences(); UserPrefs.savePreferences({ theme: 'dark' }); const result = UserPrefs.resetPreferences(); expect(result).toBe(true); expect(localStorage.getItem(STORAGE_KEY)).toBeNull(); }); it('should handle missing preferences gracefully', () => { const UserPrefs = createUserPreferences(); expect(() => UserPrefs.resetPreferences()).not.toThrow(); }); it('should handle localStorage errors', () => { const UserPrefs = createUserPreferences(); const originalRemoveItem = localStorage.removeItem; localStorage.removeItem = () => { throw new Error('localStorage error'); }; const result = UserPrefs.resetPreferences(); expect(result).toBe(false); expect(consoleErrorSpy).toHaveBeenCalled(); localStorage.removeItem = originalRemoveItem; }); }); describe('Persistence Across Sessions', () => { it('should persist theme across sessions', () => { const UserPrefs1 = createUserPreferences(); UserPrefs1.updatePreference('theme', 'dark'); // Simulate new session const UserPrefs2 = createUserPreferences(); const prefs = UserPrefs2.getPreferences(); expect(prefs.theme).toBe('dark'); }); it('should persist language across sessions', () => { const UserPrefs1 = createUserPreferences(); UserPrefs1.updatePreference('language', 'de'); // Simulate new session const UserPrefs2 = createUserPreferences(); const prefs = UserPrefs2.getPreferences(); expect(prefs.language).toBe('de'); }); it('should persist multiple preferences', () => { const UserPrefs1 = createUserPreferences(); UserPrefs1.savePreferences({ theme: 'dark', language: 'de', autoUpdate: true }); // Simulate new session const UserPrefs2 = createUserPreferences(); const prefs = UserPrefs2.getPreferences(); expect(prefs.theme).toBe('dark'); expect(prefs.language).toBe('de'); expect(prefs.autoUpdate).toBe(true); }); }); describe('Edge Cases', () => { it('should handle very large preferences object', () => { const UserPrefs = createUserPreferences(); const largePrefs = {}; for (let i = 0; i < 100; i++) { largePrefs[`key${i}`] = `value${i}`; } const result = UserPrefs.savePreferences(largePrefs); expect(result).toBe(true); const loaded = UserPrefs.getPreferences(); expect(loaded).toEqual(largePrefs); }); it('should handle special characters in values', () => { const UserPrefs = createUserPreferences(); const prefs = { theme: 'dark-', language: '日本語' }; UserPrefs.savePreferences(prefs); const loaded = UserPrefs.getPreferences(); expect(loaded).toEqual(prefs); }); it('should handle numeric values', () => { const UserPrefs = createUserPreferences(); const prefs = { fontSize: 16, volume: 0.5 }; UserPrefs.savePreferences(prefs); const loaded = UserPrefs.getPreferences(); expect(loaded.fontSize).toBe(16); expect(loaded.volume).toBe(0.5); }); it('should handle boolean values', () => { const UserPrefs = createUserPreferences(); const prefs = { autoUpdate: true, notifications: false }; UserPrefs.savePreferences(prefs); const loaded = UserPrefs.getPreferences(); expect(loaded.autoUpdate).toBe(true); expect(loaded.notifications).toBe(false); }); it('should handle nested objects', () => { const UserPrefs = createUserPreferences(); const prefs = { display: { theme: 'dark', fontSize: 16 } }; UserPrefs.savePreferences(prefs); const loaded = UserPrefs.getPreferences(); expect(loaded.display.theme).toBe('dark'); expect(loaded.display.fontSize).toBe(16); }); it('should handle arrays in preferences', () => { const UserPrefs = createUserPreferences(); const prefs = { recentSearches: ['naruto', 'one piece', 'bleach'] }; UserPrefs.savePreferences(prefs); const loaded = UserPrefs.getPreferences(); expect(loaded.recentSearches).toEqual(['naruto', 'one piece', 'bleach']); }); it('should handle rapid updates', () => { const UserPrefs = createUserPreferences(); UserPrefs.updatePreference('theme', 'dark'); UserPrefs.updatePreference('theme', 'light'); UserPrefs.updatePreference('theme', 'dark'); const prefs = UserPrefs.getPreferences(); expect(prefs.theme).toBe('dark'); }); it('should handle empty string values', () => { const UserPrefs = createUserPreferences(); const prefs = { theme: '' }; UserPrefs.savePreferences(prefs); const loaded = UserPrefs.getPreferences(); expect(loaded.theme).toBe(''); }); }); describe('Default Preferences', () => { it('should return empty object as default', () => { const UserPrefs = createUserPreferences(); const prefs = UserPrefs.getPreferences(); expect(prefs).toEqual({}); }); it('should not apply anything when no preferences exist', () => { const UserPrefs = createUserPreferences(); UserPrefs.loadPreferences(); expect(document.documentElement.getAttribute('data-theme')).toBeNull(); expect(document.documentElement.getAttribute('lang')).toBeNull(); }); }); describe('Storage Key', () => { it('should use correct storage key', () => { const UserPrefs = createUserPreferences(); UserPrefs.savePreferences({ theme: 'dark' }); const stored = localStorage.getItem(STORAGE_KEY); expect(stored).toBeTruthy(); }); it('should not interfere with other localStorage keys', () => { const UserPrefs = createUserPreferences(); localStorage.setItem('other_key', 'other_value'); UserPrefs.savePreferences({ theme: 'dark' }); expect(localStorage.getItem('other_key')).toBe('other_value'); }); }); });