727 lines
25 KiB
TypeScript
727 lines
25 KiB
TypeScript
/** @vitest-environment jsdom */
|
|
|
|
// ============================================================
|
|
// Mock MediaQueryListEvent for system theme change simulations
|
|
// Note: Other mocks (ResizeObserver, localStorage, matchMedia) are set up in vitest.setup.unit.ts
|
|
// ============================================================
|
|
|
|
class MockMediaQueryListEvent extends Event {
|
|
matches: boolean;
|
|
media: string;
|
|
|
|
constructor(type: string, eventInitDict: { matches: boolean; media: string }) {
|
|
super(type);
|
|
this.matches = eventInitDict.matches;
|
|
this.media = eventInitDict.media;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// NOW IT'S SAFE TO IMPORT
|
|
// ============================================================
|
|
|
|
import {
|
|
afterEach,
|
|
beforeEach,
|
|
describe,
|
|
expect,
|
|
it,
|
|
vi,
|
|
} from 'vitest';
|
|
import { ThemeManager } from './ThemeManager.svelte';
|
|
|
|
/**
|
|
* Test Suite for ThemeManager
|
|
*
|
|
* Tests theme management functionality including:
|
|
* - Initial state from localStorage or system preference
|
|
* - Theme setting and persistence
|
|
* - Toggle functionality
|
|
* - System preference detection and following
|
|
* - DOM manipulation for theme application
|
|
* - MediaQueryList listener management
|
|
*/
|
|
|
|
// Storage key used by ThemeManager
|
|
const STORAGE_KEY = 'glyphdiff:theme';
|
|
|
|
// Helper type for MediaQueryList event handler
|
|
type MediaQueryListCallback = (this: MediaQueryList, ev: MediaQueryListEvent) => void;
|
|
|
|
// Helper to flush Svelte effects (they run in microtasks)
|
|
async function flushEffects() {
|
|
await Promise.resolve();
|
|
}
|
|
|
|
describe('ThemeManager', () => {
|
|
let classListMock: DOMTokenList;
|
|
let darkClassAdded = false;
|
|
let mediaQueryListeners: Map<string, Set<MediaQueryListCallback>> = new Map();
|
|
let matchMediaSpy: ReturnType<typeof vi.fn>;
|
|
|
|
beforeEach(() => {
|
|
// Reset tracking variables
|
|
darkClassAdded = false;
|
|
mediaQueryListeners.clear();
|
|
// Clear localStorage before each test
|
|
localStorage.clear();
|
|
|
|
// Mock documentElement.classList
|
|
classListMock = {
|
|
contains: (className: string) => className === 'dark' ? darkClassAdded : false,
|
|
add: vi.fn((..._classNames: string[]) => {
|
|
darkClassAdded = true;
|
|
}),
|
|
remove: vi.fn((..._classNames: string[]) => {
|
|
darkClassAdded = false;
|
|
}),
|
|
toggle: vi.fn((className: string, force?: boolean) => {
|
|
if (className === 'dark') {
|
|
if (force !== undefined) {
|
|
darkClassAdded = force;
|
|
} else {
|
|
darkClassAdded = !darkClassAdded;
|
|
}
|
|
return darkClassAdded;
|
|
}
|
|
return false;
|
|
}),
|
|
supports: vi.fn(() => true),
|
|
entries: vi.fn(() => []),
|
|
forEach: vi.fn(),
|
|
keys: vi.fn(() => []),
|
|
values: vi.fn(() => []),
|
|
length: 0,
|
|
item: vi.fn(() => null),
|
|
replace: vi.fn(() => false),
|
|
} as unknown as DOMTokenList;
|
|
|
|
// Mock document.documentElement
|
|
if (typeof document !== 'undefined' && document.documentElement) {
|
|
Object.defineProperty(document.documentElement, 'classList', {
|
|
configurable: true,
|
|
get: () => classListMock,
|
|
});
|
|
}
|
|
|
|
// Mock window.matchMedia with spy to track listeners
|
|
matchMediaSpy = vi.fn((query: string) => {
|
|
// Default to light theme (matches = false)
|
|
const mediaQueryList = {
|
|
matches: false,
|
|
media: query,
|
|
onchange: null,
|
|
addListener: vi.fn(), // Deprecated
|
|
removeListener: vi.fn(), // Deprecated
|
|
addEventListener: vi.fn((_type: string, listener: MediaQueryListCallback) => {
|
|
if (!mediaQueryListeners.has(query)) {
|
|
mediaQueryListeners.set(query, new Set());
|
|
}
|
|
mediaQueryListeners.get(query)!.add(listener);
|
|
}),
|
|
removeEventListener: vi.fn((_type: string, listener: MediaQueryListCallback) => {
|
|
if (mediaQueryListeners.has(query)) {
|
|
mediaQueryListeners.get(query)!.delete(listener);
|
|
}
|
|
}),
|
|
dispatchEvent: vi.fn(),
|
|
};
|
|
return mediaQueryList;
|
|
});
|
|
|
|
Object.defineProperty(window, 'matchMedia', {
|
|
writable: true,
|
|
value: matchMediaSpy,
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
/**
|
|
* Helper to trigger a MediaQueryList change event
|
|
*/
|
|
function triggerSystemThemeChange(isDark: boolean) {
|
|
const query = '(prefers-color-scheme: dark)';
|
|
const listeners = mediaQueryListeners.get(query);
|
|
if (listeners) {
|
|
const event = new MockMediaQueryListEvent('change', {
|
|
matches: isDark,
|
|
media: query,
|
|
});
|
|
listeners.forEach(listener => listener.call({ matches: isDark, media: query } as MediaQueryList, event));
|
|
}
|
|
}
|
|
|
|
describe('Constructor - Initial State', () => {
|
|
it('should initialize with light theme when localStorage is empty and system prefers light', () => {
|
|
const manager = new ThemeManager();
|
|
|
|
expect(manager.value).toBe('light');
|
|
expect(manager.isDark).toBe(false);
|
|
expect(manager.source).toBe('system');
|
|
});
|
|
|
|
it('should initialize with system dark theme when localStorage is empty', () => {
|
|
// Mock system prefers dark theme
|
|
matchMediaSpy.mockImplementation((query: string) => ({
|
|
matches: query === '(prefers-color-scheme: dark)',
|
|
media: query,
|
|
onchange: null,
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
}));
|
|
|
|
const manager = new ThemeManager();
|
|
|
|
expect(manager.value).toBe('dark');
|
|
expect(manager.isDark).toBe(true);
|
|
expect(manager.source).toBe('system');
|
|
});
|
|
|
|
it('should initialize with stored light theme from localStorage', () => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('light'));
|
|
|
|
const manager = new ThemeManager();
|
|
|
|
expect(manager.value).toBe('light');
|
|
expect(manager.isDark).toBe(false);
|
|
expect(manager.source).toBe('user');
|
|
});
|
|
|
|
it('should initialize with stored dark theme from localStorage', () => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('dark'));
|
|
|
|
const manager = new ThemeManager();
|
|
|
|
expect(manager.value).toBe('dark');
|
|
expect(manager.isDark).toBe(true);
|
|
expect(manager.source).toBe('user');
|
|
});
|
|
|
|
it('should ignore invalid values in localStorage and use system theme', () => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('invalid'));
|
|
|
|
const manager = new ThemeManager();
|
|
|
|
expect(manager.value).toBe('light');
|
|
expect(manager.source).toBe('system');
|
|
});
|
|
|
|
it('should handle null in localStorage as system theme', () => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(null));
|
|
|
|
const manager = new ThemeManager();
|
|
|
|
expect(manager.value).toBe('light');
|
|
expect(manager.source).toBe('system');
|
|
});
|
|
|
|
it('should be in user-controlled mode when localStorage has a valid theme', () => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('dark'));
|
|
|
|
const manager = new ThemeManager();
|
|
|
|
expect(manager.isUserControlled).toBe(true);
|
|
});
|
|
|
|
it('should not be in user-controlled mode when following system', () => {
|
|
const manager = new ThemeManager();
|
|
|
|
expect(manager.isUserControlled).toBe(false);
|
|
expect(manager.source).toBe('system');
|
|
});
|
|
});
|
|
|
|
describe('init() - Initialization', () => {
|
|
it('should apply initial theme to DOM on init', () => {
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
|
|
expect(classListMock.toggle).toHaveBeenCalledWith('dark', false);
|
|
});
|
|
|
|
it('should apply dark theme to DOM when initialized with dark theme', () => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('dark'));
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
|
|
expect(classListMock.toggle).toHaveBeenCalledWith('dark', true);
|
|
});
|
|
|
|
it('should set up MediaQueryList listener on init', () => {
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
|
|
expect(matchMediaSpy).toHaveBeenCalledWith('(prefers-color-scheme: dark)');
|
|
});
|
|
|
|
it('should not fail if called multiple times', () => {
|
|
const manager = new ThemeManager();
|
|
expect(() => {
|
|
manager.init();
|
|
manager.init();
|
|
}).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('destroy() - Cleanup', () => {
|
|
it('should remove MediaQueryList listener on destroy', () => {
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
manager.destroy();
|
|
|
|
const listeners = mediaQueryListeners.get('(prefers-color-scheme: dark)');
|
|
expect(listeners?.size ?? 0).toBe(0);
|
|
});
|
|
|
|
it('should not fail if destroy is called before init', () => {
|
|
const manager = new ThemeManager();
|
|
expect(() => {
|
|
manager.destroy();
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it('should not fail if destroy is called multiple times', () => {
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
expect(() => {
|
|
manager.destroy();
|
|
manager.destroy();
|
|
}).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('setTheme() - Set Explicit Theme', () => {
|
|
it('should set theme to light and update source to user', () => {
|
|
const manager = new ThemeManager();
|
|
manager.setTheme('light');
|
|
|
|
expect(manager.value).toBe('light');
|
|
expect(manager.isDark).toBe(false);
|
|
expect(manager.source).toBe('user');
|
|
});
|
|
|
|
it('should set theme to dark and update source to user', () => {
|
|
const manager = new ThemeManager();
|
|
manager.setTheme('dark');
|
|
|
|
expect(manager.value).toBe('dark');
|
|
expect(manager.isDark).toBe(true);
|
|
expect(manager.source).toBe('user');
|
|
});
|
|
|
|
it('should save theme to localStorage when set', async () => {
|
|
const manager = new ThemeManager();
|
|
manager.setTheme('dark');
|
|
|
|
await flushEffects();
|
|
|
|
expect(localStorage.getItem(STORAGE_KEY)).toBe(JSON.stringify('dark'));
|
|
});
|
|
|
|
it('should apply theme to DOM when set', () => {
|
|
const manager = new ThemeManager();
|
|
manager.setTheme('dark');
|
|
|
|
expect(classListMock.toggle).toHaveBeenCalledWith('dark', true);
|
|
});
|
|
|
|
it('should overwrite existing localStorage value', async () => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('light'));
|
|
const manager = new ThemeManager();
|
|
manager.setTheme('dark');
|
|
|
|
await flushEffects();
|
|
|
|
expect(localStorage.getItem(STORAGE_KEY)).toBe(JSON.stringify('dark'));
|
|
});
|
|
|
|
it('should handle switching from light to dark', () => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('light'));
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
manager.setTheme('dark');
|
|
|
|
expect(manager.value).toBe('dark');
|
|
expect(manager.source).toBe('user');
|
|
});
|
|
|
|
it('should handle switching from dark to light', () => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('dark'));
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
manager.setTheme('light');
|
|
|
|
expect(manager.value).toBe('light');
|
|
expect(manager.source).toBe('user');
|
|
});
|
|
});
|
|
|
|
describe('toggle() - Toggle Between Themes', () => {
|
|
it('should toggle from light to dark', () => {
|
|
const manager = new ThemeManager();
|
|
manager.toggle();
|
|
|
|
expect(manager.value).toBe('dark');
|
|
expect(manager.isDark).toBe(true);
|
|
expect(manager.source).toBe('user');
|
|
});
|
|
|
|
it('should toggle from dark to light', () => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('dark'));
|
|
const manager = new ThemeManager();
|
|
manager.toggle();
|
|
|
|
expect(manager.value).toBe('light');
|
|
expect(manager.isDark).toBe(false);
|
|
expect(manager.source).toBe('user');
|
|
});
|
|
|
|
it('should save toggled theme to localStorage', async () => {
|
|
const manager = new ThemeManager();
|
|
manager.toggle();
|
|
|
|
await flushEffects();
|
|
|
|
expect(localStorage.getItem(STORAGE_KEY)).toBe(JSON.stringify('dark'));
|
|
});
|
|
|
|
it('should apply toggled theme to DOM', () => {
|
|
const manager = new ThemeManager();
|
|
manager.toggle();
|
|
|
|
expect(classListMock.toggle).toHaveBeenCalledWith('dark', true);
|
|
});
|
|
|
|
it('should handle multiple rapid toggles', () => {
|
|
const manager = new ThemeManager();
|
|
manager.toggle();
|
|
expect(manager.value).toBe('dark');
|
|
|
|
manager.toggle();
|
|
expect(manager.value).toBe('light');
|
|
|
|
manager.toggle();
|
|
expect(manager.value).toBe('dark');
|
|
});
|
|
});
|
|
|
|
describe('resetToSystem() - Reset to System Preference', () => {
|
|
it('should clear localStorage when resetting to system', () => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('dark'));
|
|
const manager = new ThemeManager();
|
|
manager.resetToSystem();
|
|
|
|
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
|
|
});
|
|
|
|
it('should set source to system after reset', () => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('dark'));
|
|
const manager = new ThemeManager();
|
|
manager.resetToSystem();
|
|
|
|
expect(manager.source).toBe('system');
|
|
expect(manager.isUserControlled).toBe(false);
|
|
});
|
|
|
|
it('should detect and apply light system theme', () => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('dark'));
|
|
const manager = new ThemeManager();
|
|
manager.resetToSystem();
|
|
|
|
expect(manager.value).toBe('light');
|
|
expect(classListMock.toggle).toHaveBeenCalledWith('dark', false);
|
|
});
|
|
|
|
it('should detect and apply dark system theme', () => {
|
|
// Override matchMedia to return dark preference
|
|
matchMediaSpy.mockImplementation((query: string) => ({
|
|
matches: query === '(prefers-color-scheme: dark)',
|
|
media: query,
|
|
onchange: null,
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
}));
|
|
|
|
const manager = new ThemeManager();
|
|
manager.resetToSystem();
|
|
|
|
expect(manager.value).toBe('dark');
|
|
expect(classListMock.toggle).toHaveBeenCalledWith('dark', true);
|
|
});
|
|
|
|
it('should apply system theme to DOM on reset', () => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('dark'));
|
|
const manager = new ThemeManager();
|
|
manager.resetToSystem();
|
|
|
|
expect(classListMock.toggle).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('System Theme Change Handling', () => {
|
|
it('should update theme when system changes to dark while following system', () => {
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
|
|
triggerSystemThemeChange(true);
|
|
|
|
expect(manager.value).toBe('dark');
|
|
expect(manager.isDark).toBe(true);
|
|
});
|
|
|
|
it('should update theme when system changes to light while following system', () => {
|
|
// Start with dark system theme
|
|
// Keep the listener tracking while overriding matches behavior
|
|
matchMediaSpy.mockImplementation((query: string) => ({
|
|
matches: query === '(prefers-color-scheme: dark)',
|
|
media: query,
|
|
onchange: null,
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
addEventListener: vi.fn((_type: string, listener: MediaQueryListCallback) => {
|
|
if (!mediaQueryListeners.has(query)) {
|
|
mediaQueryListeners.set(query, new Set());
|
|
}
|
|
mediaQueryListeners.get(query)!.add(listener);
|
|
}),
|
|
removeEventListener: vi.fn((_type: string, listener: MediaQueryListCallback) => {
|
|
if (mediaQueryListeners.has(query)) {
|
|
mediaQueryListeners.get(query)!.delete(listener);
|
|
}
|
|
}),
|
|
dispatchEvent: vi.fn(),
|
|
}));
|
|
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
expect(manager.value).toBe('dark');
|
|
|
|
// Now change to light
|
|
triggerSystemThemeChange(false);
|
|
|
|
expect(manager.value).toBe('light');
|
|
expect(manager.isDark).toBe(false);
|
|
});
|
|
|
|
it('should update DOM when system theme changes while following system', () => {
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
|
|
triggerSystemThemeChange(true);
|
|
|
|
expect(classListMock.toggle).toHaveBeenCalledWith('dark', true);
|
|
});
|
|
|
|
it('should NOT update theme when system changes if user has set theme', () => {
|
|
const manager = new ThemeManager();
|
|
manager.setTheme('light'); // User explicitly sets light
|
|
manager.init();
|
|
|
|
// Simulate system changing to dark
|
|
triggerSystemThemeChange(true);
|
|
|
|
// Theme should remain light because user set it
|
|
expect(manager.value).toBe('light');
|
|
expect(manager.source).toBe('user');
|
|
});
|
|
|
|
it('should respond to system changes after resetToSystem', () => {
|
|
// Start with user-controlled dark theme
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('dark'));
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
|
|
expect(manager.value).toBe('dark');
|
|
expect(manager.source).toBe('user');
|
|
|
|
// Reset to system (which is light)
|
|
manager.resetToSystem();
|
|
expect(manager.value).toBe('light');
|
|
expect(manager.source).toBe('system');
|
|
|
|
// Now system changes to dark
|
|
triggerSystemThemeChange(true);
|
|
|
|
// Should update because we're back to following system
|
|
expect(manager.value).toBe('dark');
|
|
expect(manager.source).toBe('system');
|
|
});
|
|
|
|
it('should stop responding to system changes after setTheme is called', () => {
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
|
|
// System changes to dark
|
|
triggerSystemThemeChange(true);
|
|
expect(manager.value).toBe('dark');
|
|
expect(manager.source).toBe('system');
|
|
|
|
// User explicitly sets light
|
|
manager.setTheme('light');
|
|
expect(manager.value).toBe('light');
|
|
expect(manager.source).toBe('user');
|
|
|
|
// System changes again
|
|
triggerSystemThemeChange(false);
|
|
|
|
// Should stay light because user set it
|
|
expect(manager.value).toBe('light');
|
|
});
|
|
|
|
it('should not trigger updates after destroy is called', () => {
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
manager.destroy();
|
|
|
|
// This should not cause any updates since listener was removed
|
|
expect(() => {
|
|
triggerSystemThemeChange(true);
|
|
}).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('DOM Interaction', () => {
|
|
it('should add dark class when applying dark theme', () => {
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
|
|
manager.setTheme('dark');
|
|
|
|
// Check toggle was called with force=true for dark
|
|
expect(classListMock.toggle).toHaveBeenCalledWith('dark', true);
|
|
});
|
|
|
|
it('should remove dark class when applying light theme', () => {
|
|
// Start with dark
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('dark'));
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
|
|
// Switch to light
|
|
manager.setTheme('light');
|
|
|
|
expect(classListMock.toggle).toHaveBeenCalledWith('dark', false);
|
|
});
|
|
|
|
it('should not add dark class when system prefers light', () => {
|
|
const manager = new ThemeManager();
|
|
manager.init();
|
|
|
|
expect(classListMock.toggle).toHaveBeenCalledWith('dark', false);
|
|
});
|
|
});
|
|
|
|
describe('Getter Properties', () => {
|
|
it('value getter should return current theme', () => {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify('dark'));
|
|
const manager = new ThemeManager();
|
|
|
|
expect(manager.value).toBe('dark');
|
|
});
|
|
|
|
it('source getter should return "user" when theme is user-controlled', () => {
|
|
const manager = new ThemeManager();
|
|
manager.setTheme('dark');
|
|
|
|
expect(manager.source).toBe('user');
|
|
});
|
|
|
|
it('source getter should return "system" when following system', () => {
|
|
const manager = new ThemeManager();
|
|
|
|
expect(manager.source).toBe('system');
|
|
});
|
|
|
|
it('isDark getter should return true for dark theme', () => {
|
|
const manager = new ThemeManager();
|
|
manager.setTheme('dark');
|
|
|
|
expect(manager.isDark).toBe(true);
|
|
});
|
|
|
|
it('isDark getter should return false for light theme', () => {
|
|
const manager = new ThemeManager();
|
|
|
|
expect(manager.isDark).toBe(false);
|
|
});
|
|
|
|
it('isUserControlled getter should return true when source is user', () => {
|
|
const manager = new ThemeManager();
|
|
manager.setTheme('light');
|
|
|
|
expect(manager.isUserControlled).toBe(true);
|
|
});
|
|
|
|
it('isUserControlled getter should return false when source is system', () => {
|
|
const manager = new ThemeManager();
|
|
|
|
expect(manager.isUserControlled).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle rapid setTheme calls', async () => {
|
|
const manager = new ThemeManager();
|
|
manager.setTheme('dark');
|
|
manager.setTheme('light');
|
|
manager.setTheme('dark');
|
|
manager.setTheme('light');
|
|
|
|
await flushEffects();
|
|
|
|
expect(manager.value).toBe('light');
|
|
expect(localStorage.getItem(STORAGE_KEY)).toBe(JSON.stringify('light'));
|
|
});
|
|
|
|
it('should handle toggle immediately followed by setTheme', async () => {
|
|
const manager = new ThemeManager();
|
|
manager.toggle();
|
|
manager.setTheme('light');
|
|
|
|
await flushEffects();
|
|
|
|
expect(manager.value).toBe('light');
|
|
expect(localStorage.getItem(STORAGE_KEY)).toBe(JSON.stringify('light'));
|
|
});
|
|
|
|
it('should handle setTheme immediately followed by resetToSystem', () => {
|
|
const manager = new ThemeManager();
|
|
manager.setTheme('dark');
|
|
manager.resetToSystem();
|
|
|
|
expect(manager.value).toBe('light');
|
|
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
|
|
});
|
|
|
|
it('should handle resetToSystem when already following system', () => {
|
|
const manager = new ThemeManager();
|
|
manager.resetToSystem();
|
|
|
|
expect(manager.value).toBe('light');
|
|
expect(manager.source).toBe('system');
|
|
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('Type Safety', () => {
|
|
it('should accept "light" as valid theme', () => {
|
|
const manager = new ThemeManager();
|
|
expect(() => manager.setTheme('light')).not.toThrow();
|
|
});
|
|
|
|
it('should accept "dark" as valid theme', () => {
|
|
const manager = new ThemeManager();
|
|
expect(() => manager.setTheme('dark')).not.toThrow();
|
|
});
|
|
});
|
|
});
|