- 修复 verbatimModuleSyntax 要求的 import type 声明 - 修复未使用导入(Badge/EditOutlined/Space/Input/Switch 等) - 修复 mock.calls 类型注解([string,unknown] → any[]) - 修复 vitest 全局超时和 poolTimeout 配置 - 修复 PageContainer 缺少 onBack prop、MenuInfo children 可选 - 修复 CopilotAlert Badge status info→processing、useCopilotRisk 二次解包 - 修复 articles/doctors 测试 delete 调用缺少 version 参数 - 添加排班管理/预约管理面包屑标题 fallback
262 lines
8.6 KiB
TypeScript
262 lines
8.6 KiB
TypeScript
/**
|
||
* App Store 单元测试
|
||
*
|
||
* 覆盖 useAppStore 的状态管理逻辑:
|
||
* - 初始状态值
|
||
* - 主题切换与持久化
|
||
* - 侧边栏折叠状态
|
||
* - 远程主题配置加载
|
||
*/
|
||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||
|
||
// --- Mock localStorage ---
|
||
const localStorageStore: Record<string, string> = {}
|
||
|
||
const mockLocalStorage = {
|
||
getItem: vi.fn((key: string) => localStorageStore[key] ?? null),
|
||
setItem: vi.fn((key: string, value: string) => {
|
||
localStorageStore[key] = value
|
||
}),
|
||
removeItem: vi.fn((key: string) => {
|
||
delete localStorageStore[key]
|
||
}),
|
||
clear: vi.fn(() => {
|
||
Object.keys(localStorageStore).forEach((k) => delete localStorageStore[k])
|
||
}),
|
||
}
|
||
|
||
Object.defineProperty(globalThis, 'localStorage', {
|
||
value: mockLocalStorage,
|
||
writable: true,
|
||
})
|
||
|
||
// --- Mock getTheme API ---
|
||
const mockGetTheme = vi.fn()
|
||
vi.mock('../api/themes', () => ({
|
||
getTheme: (...args: unknown[]) => mockGetTheme(...args),
|
||
}))
|
||
|
||
// --- Mock zustand 内部不依赖真实存储 ---
|
||
// 在 mock 生效后导入被测模块
|
||
import { useAppStore, THEME_OPTIONS } from './app'
|
||
import type { ThemeName } from './app'
|
||
import type { ThemeConfig } from '../api/themes'
|
||
|
||
beforeEach(() => {
|
||
vi.clearAllMocks()
|
||
mockLocalStorage.clear()
|
||
// zustand store 每次需要重新创建状态,我们通过 setState 重置
|
||
useAppStore.setState({
|
||
theme: 'blue',
|
||
sidebarCollapsed: false,
|
||
themeConfig: null,
|
||
})
|
||
})
|
||
|
||
// ============================================================
|
||
// THEME_OPTIONS 常量
|
||
// ============================================================
|
||
describe('THEME_OPTIONS', () => {
|
||
it('应包含 4 个主题选项', () => {
|
||
expect(THEME_OPTIONS).toHaveLength(4)
|
||
})
|
||
|
||
it('所有选项包含 key / label / desc / preview', () => {
|
||
for (const opt of THEME_OPTIONS) {
|
||
expect(opt).toHaveProperty('key')
|
||
expect(opt).toHaveProperty('label')
|
||
expect(opt).toHaveProperty('desc')
|
||
expect(opt).toHaveProperty('preview')
|
||
expect(opt.preview).toHaveProperty('primary')
|
||
expect(opt.preview).toHaveProperty('bg')
|
||
expect(opt.preview).toHaveProperty('surface')
|
||
}
|
||
})
|
||
|
||
it('key 值互不重复', () => {
|
||
const keys = THEME_OPTIONS.map((t) => t.key)
|
||
expect(new Set(keys).size).toBe(keys.length)
|
||
})
|
||
})
|
||
|
||
// ============================================================
|
||
// 初始状态
|
||
// ============================================================
|
||
describe('初始状态', () => {
|
||
it('sidebarCollapsed 默认为 false', () => {
|
||
expect(useAppStore.getState().sidebarCollapsed).toBe(false)
|
||
})
|
||
|
||
it('themeConfig 默认为 null', () => {
|
||
expect(useAppStore.getState().themeConfig).toBeNull()
|
||
})
|
||
|
||
it('localStorage 无记录时 theme 默认为 blue', () => {
|
||
expect(useAppStore.getState().theme).toBe('blue')
|
||
})
|
||
|
||
it('localStorage 有有效主题时恢复该主题', () => {
|
||
localStorageStore['hms-theme'] = 'dark'
|
||
// 需要重新触发 loadTheme —— 由于 zustand create 只执行一次,
|
||
// 这里通过手动 setTheme 模拟初始化行为
|
||
// 实际验证 loadTheme 函数逻辑:
|
||
// loadTheme 读取 localStorage 并验证值是否在 THEME_OPTIONS 中
|
||
const saved = localStorageStore['hms-theme']
|
||
const isValid = THEME_OPTIONS.some((t) => t.key === saved)
|
||
expect(isValid).toBe(true)
|
||
expect(saved).toBe('dark')
|
||
})
|
||
})
|
||
|
||
// ============================================================
|
||
// setTheme — 主题切换与持久化
|
||
// ============================================================
|
||
describe('setTheme', () => {
|
||
it('应更新 theme 状态', () => {
|
||
useAppStore.getState().setTheme('warm')
|
||
expect(useAppStore.getState().theme).toBe('warm')
|
||
})
|
||
|
||
it('应将主题写入 localStorage', () => {
|
||
useAppStore.getState().setTheme('emerald')
|
||
expect(mockLocalStorage.setItem).toHaveBeenCalledWith('hms-theme', 'emerald')
|
||
})
|
||
|
||
it('连续切换应反映最终值', () => {
|
||
const { setTheme } = useAppStore.getState()
|
||
setTheme('dark')
|
||
setTheme('warm')
|
||
expect(useAppStore.getState().theme).toBe('warm')
|
||
})
|
||
|
||
it('切换到每个有效主题名都应成功', () => {
|
||
const validThemes: ThemeName[] = ['blue', 'warm', 'dark', 'emerald']
|
||
for (const t of validThemes) {
|
||
useAppStore.getState().setTheme(t)
|
||
expect(useAppStore.getState().theme).toBe(t)
|
||
}
|
||
})
|
||
|
||
it('localStorage 写入失败时不应抛出异常', () => {
|
||
mockLocalStorage.setItem.mockImplementationOnce(() => {
|
||
throw new Error('QuotaExceededError')
|
||
})
|
||
expect(() => useAppStore.getState().setTheme('dark')).not.toThrow()
|
||
// 状态仍然更新(内存中生效)
|
||
expect(useAppStore.getState().theme).toBe('dark')
|
||
})
|
||
})
|
||
|
||
// ============================================================
|
||
// toggleSidebar — 侧边栏折叠切换
|
||
// ============================================================
|
||
describe('toggleSidebar', () => {
|
||
it('应从 false 切换到 true', () => {
|
||
expect(useAppStore.getState().sidebarCollapsed).toBe(false)
|
||
useAppStore.getState().toggleSidebar()
|
||
expect(useAppStore.getState().sidebarCollapsed).toBe(true)
|
||
})
|
||
|
||
it('应从 true 切换回 false', () => {
|
||
useAppStore.setState({ sidebarCollapsed: true })
|
||
useAppStore.getState().toggleSidebar()
|
||
expect(useAppStore.getState().sidebarCollapsed).toBe(false)
|
||
})
|
||
|
||
it('连续切换应交替变化', () => {
|
||
const { toggleSidebar } = useAppStore.getState()
|
||
expect(useAppStore.getState().sidebarCollapsed).toBe(false)
|
||
|
||
toggleSidebar()
|
||
expect(useAppStore.getState().sidebarCollapsed).toBe(true)
|
||
|
||
toggleSidebar()
|
||
expect(useAppStore.getState().sidebarCollapsed).toBe(false)
|
||
|
||
toggleSidebar()
|
||
expect(useAppStore.getState().sidebarCollapsed).toBe(true)
|
||
})
|
||
|
||
it('不应影响其他状态字段', () => {
|
||
useAppStore.getState().setTheme('warm')
|
||
const themeBefore = useAppStore.getState().theme
|
||
const configBefore = useAppStore.getState().themeConfig
|
||
|
||
useAppStore.getState().toggleSidebar()
|
||
|
||
expect(useAppStore.getState().theme).toBe(themeBefore)
|
||
expect(useAppStore.getState().themeConfig).toBe(configBefore)
|
||
})
|
||
})
|
||
|
||
// ============================================================
|
||
// loadThemeConfig — 远程主题配置加载
|
||
// ============================================================
|
||
describe('loadThemeConfig', () => {
|
||
const fakeConfig: ThemeConfig = {
|
||
primary_color: '#C4623A',
|
||
logo_url: '/logo.png',
|
||
sidebar_style: 'dark',
|
||
brand_name: '测试机构',
|
||
brand_slogan: '测试标语',
|
||
}
|
||
|
||
it('成功时应设置 themeConfig', async () => {
|
||
mockGetTheme.mockResolvedValueOnce(fakeConfig)
|
||
await useAppStore.getState().loadThemeConfig()
|
||
expect(useAppStore.getState().themeConfig).toEqual(fakeConfig)
|
||
})
|
||
|
||
it('调用 getTheme API 一次', async () => {
|
||
mockGetTheme.mockResolvedValueOnce(fakeConfig)
|
||
await useAppStore.getState().loadThemeConfig()
|
||
expect(mockGetTheme).toHaveBeenCalledTimes(1)
|
||
})
|
||
|
||
it('API 失败时不应修改 themeConfig(保持 null)', async () => {
|
||
mockGetTheme.mockRejectedValueOnce(new Error('Network error'))
|
||
await useAppStore.getState().loadThemeConfig()
|
||
expect(useAppStore.getState().themeConfig).toBeNull()
|
||
})
|
||
|
||
it('API 失败时不应抛出异常', async () => {
|
||
mockGetTheme.mockRejectedValueOnce(new Error('Server error'))
|
||
await expect(useAppStore.getState().loadThemeConfig()).resolves.toBeUndefined()
|
||
})
|
||
|
||
it('不应影响 theme 和 sidebarCollapsed', async () => {
|
||
useAppStore.getState().setTheme('emerald')
|
||
useAppStore.getState().toggleSidebar()
|
||
|
||
mockGetTheme.mockResolvedValueOnce(fakeConfig)
|
||
await useAppStore.getState().loadThemeConfig()
|
||
|
||
expect(useAppStore.getState().theme).toBe('emerald')
|
||
expect(useAppStore.getState().sidebarCollapsed).toBe(true)
|
||
})
|
||
})
|
||
|
||
// ============================================================
|
||
// 状态隔离 — 多次操作互不干扰
|
||
// ============================================================
|
||
describe('状态隔离', () => {
|
||
it('主题切换不应影响侧边栏', () => {
|
||
useAppStore.setState({ sidebarCollapsed: true })
|
||
useAppStore.getState().setTheme('dark')
|
||
expect(useAppStore.getState().sidebarCollapsed).toBe(true)
|
||
})
|
||
|
||
it('侧边栏切换不应影响主题', () => {
|
||
useAppStore.getState().setTheme('warm')
|
||
useAppStore.getState().toggleSidebar()
|
||
expect(useAppStore.getState().theme).toBe('warm')
|
||
})
|
||
|
||
it('加载配置不应重置已有主题', async () => {
|
||
useAppStore.getState().setTheme('emerald')
|
||
mockGetTheme.mockResolvedValueOnce({ primary_color: '#000' })
|
||
await useAppStore.getState().loadThemeConfig()
|
||
expect(useAppStore.getState().theme).toBe('emerald')
|
||
})
|
||
})
|