Files
hms/apps/web/src/stores/app.test.ts
iven ced1c0ad0c fix(web): 清零前端 TS 构建错误 — 31 文件类型修复 + 面包屑 + 超时配置
- 修复 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
2026-05-15 23:03:08 +08:00

262 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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')
})
})