重构 ThemeContext 为多主题模型(themeId + followSystem + systemIsDark), 新增设置页面(主题下拉栏 + 跟随系统开关),移除旧 ThemeToggle 按钮, 引入 antd-style 和 clsx 依赖支持 MUI/shadcn/Bootstrap/玻璃主题。
124 lines
3.9 KiB
TypeScript
124 lines
3.9 KiB
TypeScript
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');
|
|
});
|
|
});
|