diff --git a/web/src/lib/stores/theme.svelte.test.ts b/web/src/lib/stores/theme.svelte.test.ts new file mode 100644 index 00000000..42f3aea3 --- /dev/null +++ b/web/src/lib/stores/theme.svelte.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, test, beforeEach, vi } from 'vitest'; + +function setupLocalStorage(initial: Record = {}) { + const store: Record = { ...initial }; + vi.stubGlobal('localStorage', { + getItem: (k: string) => (k in store ? store[k] : null), + setItem: (k: string, v: string) => { store[k] = v; }, + removeItem: (k: string) => { delete store[k]; } + }); + return store; +} + +function setupMatchMedia(prefersLight: boolean) { + const listeners: ((e: { matches: boolean }) => void)[] = []; + const mql = { + matches: prefersLight, + addEventListener: (_: string, cb: (e: { matches: boolean }) => void) => listeners.push(cb), + removeEventListener: () => {} + }; + vi.stubGlobal('window', { + matchMedia: () => mql + }); + return { mql, listeners, fire: (matches: boolean) => listeners.forEach((cb) => cb({ matches })) }; +} + +beforeEach(() => { + vi.resetModules(); + vi.unstubAllGlobals(); + if (typeof document !== 'undefined') { + document.documentElement.removeAttribute('data-theme'); + } +}); + +describe('theme store', () => { + test('default preference is system; resolves via matchMedia', async () => { + setupLocalStorage(); + setupMatchMedia(false); // prefers-dark + const mod = await import('./theme.svelte'); + expect(mod.theme.value).toBe('system'); + expect(mod.resolvedTheme.value).toBe('dark'); + }); + + test('explicit preference from localStorage is honored', async () => { + setupLocalStorage({ 'minstrel:theme': 'light' }); + setupMatchMedia(false); + const mod = await import('./theme.svelte'); + expect(mod.theme.value).toBe('light'); + expect(mod.resolvedTheme.value).toBe('light'); + }); + + test('setTheme persists, updates resolved, sets data-theme attribute', async () => { + const store = setupLocalStorage(); + setupMatchMedia(true); + const mod = await import('./theme.svelte'); + mod.setTheme('dark'); + expect(store['minstrel:theme']).toBe('dark'); + expect(mod.theme.value).toBe('dark'); + expect(mod.resolvedTheme.value).toBe('dark'); + expect(document.documentElement.getAttribute('data-theme')).toBe('dark'); + }); + + test('matchMedia change updates resolved while in system mode', async () => { + setupLocalStorage(); + const mm = setupMatchMedia(false); // prefers-dark + const mod = await import('./theme.svelte'); + expect(mod.resolvedTheme.value).toBe('dark'); + mm.fire(true); // OS flipped to light + expect(mod.resolvedTheme.value).toBe('light'); + }); + + test('matchMedia change does NOT update resolved when explicit preference set', async () => { + setupLocalStorage({ 'minstrel:theme': 'dark' }); + const mm = setupMatchMedia(false); + const mod = await import('./theme.svelte'); + expect(mod.resolvedTheme.value).toBe('dark'); + mm.fire(true); // OS prefers light, but user explicitly chose dark + expect(mod.resolvedTheme.value).toBe('dark'); + }); +});