|
|
|
@ -0,0 +1,285 @@ |
|
|
|
/** |
|
|
|
* Theme Utility Tests |
|
|
|
* Tests for dark/light theme switching and color management |
|
|
|
*/ |
|
|
|
|
|
|
|
import Theme from '../../src/assets/scripts/utils/theme.js'; |
|
|
|
|
|
|
|
describe('Theme Utility', () => { |
|
|
|
beforeEach(() => { |
|
|
|
// Clear localStorage
|
|
|
|
localStorage.clear(); |
|
|
|
// Reset document theme
|
|
|
|
document.documentElement.removeAttribute('data-theme'); |
|
|
|
// Clear any theme-related CSS variables
|
|
|
|
document.documentElement.style.cssText = ''; |
|
|
|
}); |
|
|
|
|
|
|
|
afterEach(() => { |
|
|
|
localStorage.clear(); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('Theme Application', () => { |
|
|
|
it('should apply light theme', () => { |
|
|
|
Theme.apply('light'); |
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('light'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should apply dark theme', () => { |
|
|
|
Theme.apply('dark'); |
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should save theme to localStorage', () => { |
|
|
|
Theme.apply('dark'); |
|
|
|
expect(localStorage.getItem('adminator-theme')).toBe('dark'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should dispatch theme changed event', () => { |
|
|
|
const handler = jest.fn(); |
|
|
|
window.addEventListener('adminator:themeChanged', handler); |
|
|
|
|
|
|
|
Theme.apply('dark'); |
|
|
|
|
|
|
|
expect(handler).toHaveBeenCalled(); |
|
|
|
expect(handler.mock.calls[0][0].detail.theme).toBe('dark'); |
|
|
|
|
|
|
|
window.removeEventListener('adminator:themeChanged', handler); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should handle localStorage errors gracefully', () => { |
|
|
|
// Mock localStorage.setItem to throw
|
|
|
|
const originalSetItem = Storage.prototype.setItem; |
|
|
|
Storage.prototype.setItem = jest.fn(() => { |
|
|
|
throw new Error('QuotaExceededError'); |
|
|
|
}); |
|
|
|
|
|
|
|
expect(() => Theme.apply('dark')).not.toThrow(); |
|
|
|
|
|
|
|
Storage.prototype.setItem = originalSetItem; |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('Theme Toggle', () => { |
|
|
|
it('should toggle from light to dark', () => { |
|
|
|
Theme.apply('light'); |
|
|
|
Theme.toggle(); |
|
|
|
expect(Theme.current()).toBe('dark'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should toggle from dark to light', () => { |
|
|
|
Theme.apply('dark'); |
|
|
|
Theme.toggle(); |
|
|
|
expect(Theme.current()).toBe('light'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should toggle multiple times', () => { |
|
|
|
Theme.apply('light'); |
|
|
|
Theme.toggle(); // dark
|
|
|
|
expect(Theme.current()).toBe('dark'); |
|
|
|
Theme.toggle(); // light
|
|
|
|
expect(Theme.current()).toBe('light'); |
|
|
|
Theme.toggle(); // dark
|
|
|
|
expect(Theme.current()).toBe('dark'); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('Current Theme', () => { |
|
|
|
it('should return light as default', () => { |
|
|
|
expect(Theme.current()).toBe('light'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should return saved theme', () => { |
|
|
|
localStorage.setItem('adminator-theme', 'dark'); |
|
|
|
expect(Theme.current()).toBe('dark'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should handle localStorage errors', () => { |
|
|
|
const originalGetItem = Storage.prototype.getItem; |
|
|
|
Storage.prototype.getItem = jest.fn(() => { |
|
|
|
throw new Error('SecurityError'); |
|
|
|
}); |
|
|
|
|
|
|
|
expect(Theme.current()).toBe('light'); |
|
|
|
|
|
|
|
Storage.prototype.getItem = originalGetItem; |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('Theme Initialization', () => { |
|
|
|
it('should initialize with saved theme', () => { |
|
|
|
localStorage.setItem('adminator-theme', 'dark'); |
|
|
|
Theme.init(); |
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should detect OS dark mode preference', () => { |
|
|
|
// Mock matchMedia to prefer dark
|
|
|
|
window.matchMedia = jest.fn().mockImplementation(query => ({ |
|
|
|
matches: query === '(prefers-color-scheme: dark)', |
|
|
|
media: query, |
|
|
|
addEventListener: jest.fn(), |
|
|
|
removeEventListener: jest.fn(), |
|
|
|
})); |
|
|
|
|
|
|
|
Theme.init(); |
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('dark'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should detect OS light mode preference', () => { |
|
|
|
// Mock matchMedia to prefer light
|
|
|
|
window.matchMedia = jest.fn().mockImplementation(query => ({ |
|
|
|
matches: false, |
|
|
|
media: query, |
|
|
|
addEventListener: jest.fn(), |
|
|
|
removeEventListener: jest.fn(), |
|
|
|
})); |
|
|
|
|
|
|
|
Theme.init(); |
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('light'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should not override existing theme', () => { |
|
|
|
localStorage.setItem('adminator-theme', 'light'); |
|
|
|
|
|
|
|
// Mock matchMedia to prefer dark
|
|
|
|
window.matchMedia = jest.fn().mockImplementation(query => ({ |
|
|
|
matches: query === '(prefers-color-scheme: dark)', |
|
|
|
media: query, |
|
|
|
addEventListener: jest.fn(), |
|
|
|
removeEventListener: jest.fn(), |
|
|
|
})); |
|
|
|
|
|
|
|
Theme.init(); |
|
|
|
// Should use saved 'light', not OS preference 'dark'
|
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('light'); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('CSS Variable Access', () => { |
|
|
|
it('should get CSS variable value', () => { |
|
|
|
document.documentElement.style.setProperty('--test-color', '#FF0000'); |
|
|
|
const value = Theme.getCSSVar('--test-color'); |
|
|
|
expect(value).toBe('#FF0000'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should trim whitespace from CSS variable', () => { |
|
|
|
document.documentElement.style.setProperty('--test-color', ' #FF0000 '); |
|
|
|
const value = Theme.getCSSVar('--test-color'); |
|
|
|
expect(value).toBe('#FF0000'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should return empty string for non-existent variable', () => { |
|
|
|
const value = Theme.getCSSVar('--non-existent'); |
|
|
|
expect(value).toBe(''); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('Color Getters', () => { |
|
|
|
beforeEach(() => { |
|
|
|
// Set up CSS variables
|
|
|
|
document.documentElement.style.setProperty('--vmap-bg-color', '#FFFFFF'); |
|
|
|
document.documentElement.style.setProperty('--vmap-border-color', '#CCCCCC'); |
|
|
|
document.documentElement.style.setProperty('--sparkline-success', '#28A745'); |
|
|
|
document.documentElement.style.setProperty('--sparkline-purple', '#6F42C1'); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('getVectorMapColors', () => { |
|
|
|
it('should return vector map colors object', () => { |
|
|
|
const colors = Theme.getVectorMapColors(); |
|
|
|
expect(colors).toHaveProperty('backgroundColor'); |
|
|
|
expect(colors).toHaveProperty('borderColor'); |
|
|
|
expect(colors).toHaveProperty('regionColor'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should return correct color values', () => { |
|
|
|
const colors = Theme.getVectorMapColors(); |
|
|
|
expect(colors.backgroundColor).toBe('#FFFFFF'); |
|
|
|
expect(colors.borderColor).toBe('#CCCCCC'); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('getSparklineColors', () => { |
|
|
|
it('should return sparkline colors object', () => { |
|
|
|
const colors = Theme.getSparklineColors(); |
|
|
|
expect(colors).toHaveProperty('success'); |
|
|
|
expect(colors).toHaveProperty('purple'); |
|
|
|
expect(colors).toHaveProperty('info'); |
|
|
|
expect(colors).toHaveProperty('danger'); |
|
|
|
expect(colors).toHaveProperty('light'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should return correct color values', () => { |
|
|
|
const colors = Theme.getSparklineColors(); |
|
|
|
expect(colors.success).toBe('#28A745'); |
|
|
|
expect(colors.purple).toBe('#6F42C1'); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('getChartColors', () => { |
|
|
|
it('should return light theme chart colors', () => { |
|
|
|
Theme.apply('light'); |
|
|
|
const colors = Theme.getChartColors(); |
|
|
|
expect(colors.textColor).toBe('#212529'); |
|
|
|
expect(colors.mutedColor).toBe('#6C757D'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should return dark theme chart colors', () => { |
|
|
|
Theme.apply('dark'); |
|
|
|
const colors = Theme.getChartColors(); |
|
|
|
expect(colors.textColor).toBe('#FFFFFF'); |
|
|
|
expect(colors.mutedColor).toBe('#D1D5DB'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should have all required color properties', () => { |
|
|
|
const colors = Theme.getChartColors(); |
|
|
|
expect(colors).toHaveProperty('textColor'); |
|
|
|
expect(colors).toHaveProperty('mutedColor'); |
|
|
|
expect(colors).toHaveProperty('borderColor'); |
|
|
|
expect(colors).toHaveProperty('gridColor'); |
|
|
|
expect(colors).toHaveProperty('tooltipBg'); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('Theme Persistence', () => { |
|
|
|
it('should persist theme across page reloads', () => { |
|
|
|
Theme.apply('dark'); |
|
|
|
expect(localStorage.getItem('adminator-theme')).toBe('dark'); |
|
|
|
|
|
|
|
// Simulate page reload
|
|
|
|
const savedTheme = localStorage.getItem('adminator-theme'); |
|
|
|
expect(savedTheme).toBe('dark'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should maintain theme after toggle', () => { |
|
|
|
Theme.apply('light'); |
|
|
|
Theme.toggle(); |
|
|
|
expect(localStorage.getItem('adminator-theme')).toBe('dark'); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
describe('Edge Cases', () => { |
|
|
|
it('should handle invalid theme values', () => { |
|
|
|
Theme.apply('invalid-theme'); |
|
|
|
expect(document.documentElement.getAttribute('data-theme')).toBe('invalid-theme'); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should handle null theme', () => { |
|
|
|
expect(() => Theme.apply(null)).not.toThrow(); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should handle undefined theme', () => { |
|
|
|
expect(() => Theme.apply(undefined)).not.toThrow(); |
|
|
|
}); |
|
|
|
|
|
|
|
it('should handle missing matchMedia', () => { |
|
|
|
const originalMatchMedia = window.matchMedia; |
|
|
|
delete window.matchMedia; |
|
|
|
|
|
|
|
expect(() => Theme.init()).not.toThrow(); |
|
|
|
|
|
|
|
window.matchMedia = originalMatchMedia; |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |