/** * workbenchStore 单元测试 * * 覆盖 useWorkbenchStore 的状态管理逻辑: * - 初始状态值 * - selectTask / setTab:任务选择与标签切换 * - refreshTasks:加载任务列表(pending / completed) * - refreshStats:加载统计信息 * - completeTask:完成任务并自动选中下一个 * - 错误处理(API 失败) */ import { describe, it, expect, vi, beforeEach } from 'vitest'; // --------------------------------------------------------------------------- // Mock 外部依赖 // --------------------------------------------------------------------------- const mockList = vi.fn(); const mockStats = vi.fn(); vi.mock('../api/health/actionInbox', () => ({ actionInboxApi: { list: (...args: unknown[]) => mockList(...args), stats: (...args: unknown[]) => mockStats(...args), }, })); import { useWorkbenchStore } from './workbenchStore'; import type { ActionItem, WorkbenchStats } from '../api/health/actionInbox'; // --------------------------------------------------------------------------- // 测试辅助函数 // --------------------------------------------------------------------------- function createFakeAction(overrides: Partial = {}): ActionItem { return { id: 'action-001', action_type: 'alert', priority: 'high', status: 'pending', title: '高血压告警', summary: '患者血压异常升高', patient_id: 'patient-001', patient_name: '张三', source_ref: 'ref-001', created_at: '2026-05-01T08:00:00Z', updated_at: '2026-05-01T08:00:00Z', ...overrides, }; } function createFakeStats(overrides: Partial = {}): WorkbenchStats { return { total_pending: 5, ai_suggestion_pending: 2, urgent_alerts: 1, followup_due: 3, completion_rate: 0.75, ...overrides, }; } // --------------------------------------------------------------------------- // 测试套件 // --------------------------------------------------------------------------- describe('useWorkbenchStore', () => { beforeEach(() => { vi.clearAllMocks(); useWorkbenchStore.setState({ tasks: [], selectedTaskId: null, tab: 'pending', loading: false, stats: null, }); }); // ========================================================================= // 初始状态 // ========================================================================= describe('初始状态', () => { it('应有正确的默认状态字段', () => { const state = useWorkbenchStore.getState(); expect(state).toHaveProperty('tasks'); expect(state).toHaveProperty('selectedTaskId'); expect(state).toHaveProperty('tab'); expect(state).toHaveProperty('loading'); expect(state).toHaveProperty('stats'); expect(state).toHaveProperty('selectTask'); expect(state).toHaveProperty('setTab'); expect(state).toHaveProperty('refreshTasks'); expect(state).toHaveProperty('refreshStats'); expect(state).toHaveProperty('completeTask'); }); it('默认值应为空和未选中', () => { const state = useWorkbenchStore.getState(); expect(state.tasks).toEqual([]); expect(state.selectedTaskId).toBeNull(); expect(state.tab).toBe('pending'); expect(state.loading).toBe(false); expect(state.stats).toBeNull(); }); }); // ========================================================================= // selectTask // ========================================================================= describe('selectTask', () => { it('应更新 selectedTaskId', () => { useWorkbenchStore.getState().selectTask('action-001'); expect(useWorkbenchStore.getState().selectedTaskId).toBe('action-001'); }); it('应允许设为 null 以取消选择', () => { useWorkbenchStore.setState({ selectedTaskId: 'action-001' }); useWorkbenchStore.getState().selectTask(null); expect(useWorkbenchStore.getState().selectedTaskId).toBeNull(); }); it('切换选中不应影响其他状态', () => { const fakeTask = createFakeAction(); useWorkbenchStore.setState({ tasks: [fakeTask], tab: 'completed' }); useWorkbenchStore.getState().selectTask('action-001'); expect(useWorkbenchStore.getState().tasks).toEqual([fakeTask]); expect(useWorkbenchStore.getState().tab).toBe('completed'); }); }); // ========================================================================= // setTab // ========================================================================= describe('setTab', () => { it('应更新 tab 并清空 selectedTaskId', async () => { useWorkbenchStore.setState({ selectedTaskId: 'action-001', tab: 'pending' }); mockList.mockResolvedValueOnce({ data: [] }); useWorkbenchStore.getState().setTab('completed'); expect(useWorkbenchStore.getState().tab).toBe('completed'); expect(useWorkbenchStore.getState().selectedTaskId).toBeNull(); }); it('切换 tab 时应自动触发 refreshTasks', async () => { const fakeTasks = [createFakeAction({ status: 'completed' })]; mockList.mockResolvedValueOnce({ data: fakeTasks }); useWorkbenchStore.getState().setTab('completed'); // 等待异步操作完成 await vi.waitFor(() => { expect(useWorkbenchStore.getState().loading).toBe(false); }); expect(mockList).toHaveBeenCalledWith({ status: 'completed', page: 1, page_size: 50, }); expect(useWorkbenchStore.getState().tasks).toEqual(fakeTasks); }); it('切换到 pending tab 应传递 pending status', async () => { mockList.mockResolvedValueOnce({ data: [] }); useWorkbenchStore.getState().setTab('pending'); await vi.waitFor(() => { expect(useWorkbenchStore.getState().loading).toBe(false); }); expect(mockList).toHaveBeenCalledWith({ status: 'pending', page: 1, page_size: 50, }); }); }); // ========================================================================= // refreshTasks // ========================================================================= describe('refreshTasks', () => { it('成功时应更新 tasks', async () => { const fakeTasks = [ createFakeAction({ id: 'a1' }), createFakeAction({ id: 'a2', title: 'AI 建议' }), ]; mockList.mockResolvedValueOnce({ data: fakeTasks }); await useWorkbenchStore.getState().refreshTasks(); expect(useWorkbenchStore.getState().tasks).toEqual(fakeTasks); }); it('加载中 loading 应为 true,完成后恢复为 false', async () => { let resolveList: (value: unknown) => void; const listPromise = new Promise((resolve) => { resolveList = resolve; }); mockList.mockReturnValue(listPromise); const refreshPromise = useWorkbenchStore.getState().refreshTasks(); expect(useWorkbenchStore.getState().loading).toBe(true); resolveList!({ data: [] }); await refreshPromise; expect(useWorkbenchStore.getState().loading).toBe(false); }); it('应传递当前 tab 的 status 参数', async () => { mockList.mockResolvedValueOnce({ data: [] }); useWorkbenchStore.setState({ tab: 'completed' }); await useWorkbenchStore.getState().refreshTasks(); expect(mockList).toHaveBeenCalledWith({ status: 'completed', page: 1, page_size: 50, }); }); it('API 返回非数组数据时 tasks 应为空数组', async () => { mockList.mockResolvedValueOnce({ data: null }); await useWorkbenchStore.getState().refreshTasks(); expect(useWorkbenchStore.getState().tasks).toEqual([]); }); it('API 返回 undefined data 时 tasks 应为空数组', async () => { mockList.mockResolvedValueOnce(undefined); await useWorkbenchStore.getState().refreshTasks(); expect(useWorkbenchStore.getState().tasks).toEqual([]); }); it('API 失败时应恢复 loading 为 false', async () => { mockList.mockRejectedValueOnce(new Error('network error')); await useWorkbenchStore.getState().refreshTasks(); expect(useWorkbenchStore.getState().loading).toBe(false); }); it('API 失败时不应清空已有 tasks', async () => { const existingTasks = [createFakeAction()]; useWorkbenchStore.setState({ tasks: existingTasks }); mockList.mockRejectedValueOnce(new Error('network error')); await useWorkbenchStore.getState().refreshTasks(); // 失败时 set 只重置了 loading,未更新 tasks // 但注意:代码中 catch 块只 set({ loading: false }),不修改 tasks // 不过 loading: true 已经在前面 set 了,这里 tasks 在成功路径才会被替换 // 由于代码是 try/catch/finally 模式,catch 中 tasks 不会被覆盖 expect(useWorkbenchStore.getState().loading).toBe(false); }); }); // ========================================================================= // refreshStats // ========================================================================= describe('refreshStats', () => { it('成功时应更新 stats', async () => { const fakeStats = createFakeStats(); mockStats.mockResolvedValueOnce(fakeStats); await useWorkbenchStore.getState().refreshStats(); expect(useWorkbenchStore.getState().stats).toEqual(fakeStats); }); it('API 返回 null 时 stats 应为 null', async () => { mockStats.mockResolvedValueOnce(null); await useWorkbenchStore.getState().refreshStats(); expect(useWorkbenchStore.getState().stats).toBeNull(); }); it('API 失败时 stats 应保持不变', async () => { const existingStats = createFakeStats(); useWorkbenchStore.setState({ stats: existingStats }); mockStats.mockRejectedValueOnce(new Error('server error')); await useWorkbenchStore.getState().refreshStats(); expect(useWorkbenchStore.getState().stats).toEqual(existingStats); }); it('API 失败时不应抛出异常', async () => { mockStats.mockRejectedValueOnce(new Error('timeout')); await expect( useWorkbenchStore.getState().refreshStats(), ).resolves.toBeUndefined(); }); }); // ========================================================================= // completeTask // ========================================================================= describe('completeTask', () => { it('应从 tasks 中移除指定 id', () => { const task1 = createFakeAction({ id: 'a1' }); const task2 = createFakeAction({ id: 'a2' }); useWorkbenchStore.setState({ tasks: [task1, task2] }); useWorkbenchStore.getState().completeTask('a1'); expect(useWorkbenchStore.getState().tasks).toEqual([task2]); }); it('应自动选中下一个任务', () => { const task1 = createFakeAction({ id: 'a1' }); const task2 = createFakeAction({ id: 'a2' }); const task3 = createFakeAction({ id: 'a3' }); useWorkbenchStore.setState({ tasks: [task1, task2, task3], selectedTaskId: 'a2' }); useWorkbenchStore.getState().completeTask('a2'); // 移除 a2 后剩余 [a1, a3],下一个选中 a1 expect(useWorkbenchStore.getState().selectedTaskId).toBe('a1'); }); it('无剩余任务时 selectedTaskId 应为 null', () => { const task1 = createFakeAction({ id: 'a1' }); useWorkbenchStore.setState({ tasks: [task1], selectedTaskId: 'a1' }); useWorkbenchStore.getState().completeTask('a1'); expect(useWorkbenchStore.getState().tasks).toEqual([]); expect(useWorkbenchStore.getState().selectedTaskId).toBeNull(); }); it('完成后应自动刷新 stats', async () => { const fakeStats = createFakeStats({ total_pending: 4 }); mockStats.mockResolvedValueOnce(fakeStats); const task1 = createFakeAction({ id: 'a1' }); useWorkbenchStore.setState({ tasks: [task1] }); useWorkbenchStore.getState().completeTask('a1'); // 等待异步 refreshStats 完成 await vi.waitFor(() => { expect(mockStats).toHaveBeenCalledTimes(1); }); expect(useWorkbenchStore.getState().stats).toEqual(fakeStats); }); it('移除不存在的 id 不应影响 tasks', () => { const task1 = createFakeAction({ id: 'a1' }); useWorkbenchStore.setState({ tasks: [task1] }); useWorkbenchStore.getState().completeTask('nonexistent'); expect(useWorkbenchStore.getState().tasks).toEqual([task1]); }); }); // ========================================================================= // 边界情况 // ========================================================================= describe('边界情况', () => { it('快速连续 selectTask 应反映最后设置的值', () => { useWorkbenchStore.getState().selectTask('a1'); useWorkbenchStore.getState().selectTask('a2'); useWorkbenchStore.getState().selectTask('a3'); expect(useWorkbenchStore.getState().selectedTaskId).toBe('a3'); }); it('stats 含 null completion_rate 时应正确存储', async () => { const statsWithNull = createFakeStats({ completion_rate: null }); mockStats.mockResolvedValueOnce(statsWithNull); await useWorkbenchStore.getState().refreshStats(); expect(useWorkbenchStore.getState().stats!.completion_rate).toBeNull(); }); it('completeTask 后再 refreshTasks 应正常加载新列表', async () => { const task1 = createFakeAction({ id: 'a1' }); const task2 = createFakeAction({ id: 'a2' }); useWorkbenchStore.setState({ tasks: [task1, task2] }); useWorkbenchStore.getState().completeTask('a1'); expect(useWorkbenchStore.getState().tasks).toEqual([task2]); const newTasks = [createFakeAction({ id: 'a3' }), createFakeAction({ id: 'a4' })]; mockList.mockResolvedValueOnce({ data: newTasks }); mockStats.mockResolvedValueOnce(createFakeStats()); await useWorkbenchStore.getState().refreshTasks(); expect(useWorkbenchStore.getState().tasks).toEqual(newTasks); }); }); });