import { renderHook, act } from '@testing-library/react'; import { describe, it, expect, beforeEach, vi } from 'vitest'; import { ThemeProvider, useTheme } from '@/contexts/ThemeContext'; describe('ThemeContext', () => { beforeEach(() => { localStorage.clear(); }); it('returns default theme on initial load', () => { const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider }); expect(result.current.themeId).toBe('default'); expect(result.current.followSystem).toBe(false); expect(result.current.systemIsDark).toBe(false); expect(result.current.effectiveThemeId).toBe('default'); }); it('restores themeId from localStorage', () => { localStorage.setItem('nex-theme-id', 'mui'); const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider }); expect(result.current.themeId).toBe('mui'); }); it('restores followSystem from localStorage', () => { localStorage.setItem('nex-follow-system', 'true'); const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider }); expect(result.current.followSystem).toBe(true); }); it('falls back to default for invalid themeId', () => { localStorage.setItem('nex-theme-id', 'invalid-theme'); const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider }); expect(result.current.themeId).toBe('default'); }); it('setThemeId updates themeId and persists to localStorage', () => { const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider }); act(() => { result.current.setThemeId('shadcn'); }); expect(result.current.themeId).toBe('shadcn'); expect(localStorage.getItem('nex-theme-id')).toBe('shadcn'); }); it('setFollowSystem updates followSystem and persists to localStorage', () => { const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider }); act(() => { result.current.setFollowSystem(true); }); expect(result.current.followSystem).toBe(true); expect(localStorage.getItem('nex-follow-system')).toBe('true'); }); it('computes effectiveThemeId as dark when followSystem and systemIsDark', () => { localStorage.setItem('nex-theme-id', 'mui'); localStorage.setItem('nex-follow-system', 'true'); const mediaQuerySpy = vi.spyOn(window, 'matchMedia').mockReturnValue({ matches: true, media: '(prefers-color-scheme: dark)', onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), }); const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider }); expect(result.current.effectiveThemeId).toBe('dark'); mediaQuerySpy.mockRestore(); }); it('computes effectiveThemeId as themeId when followSystem but system is light', () => { localStorage.setItem('nex-theme-id', 'mui'); localStorage.setItem('nex-follow-system', 'true'); const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider }); expect(result.current.effectiveThemeId).toBe('mui'); }); it('computes effectiveThemeId as themeId when followSystem is off', () => { localStorage.setItem('nex-theme-id', 'glass'); localStorage.setItem('nex-follow-system', 'false'); const mediaQuerySpy = vi.spyOn(window, 'matchMedia').mockReturnValue({ matches: true, media: '(prefers-color-scheme: dark)', onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), }); const { result } = renderHook(() => useTheme(), { wrapper: ThemeProvider }); expect(result.current.effectiveThemeId).toBe('glass'); mediaQuerySpy.mockRestore(); }); it('throws error when useTheme is used outside ThemeProvider', () => { expect(() => { renderHook(() => useTheme()); }).toThrow('useTheme must be used within ThemeProvider'); }); });