plugin.test.ts: fetchPlugins/refreshMenuItems/pluginMenuGroups 全覆盖 workbenchStore.test.ts: selectTask/setTab/refreshTasks/refreshStats/completeTask 全覆盖 前端 Store 测试总数: 22 → 140 (6 个文件)
407 lines
14 KiB
TypeScript
407 lines
14 KiB
TypeScript
/**
|
||
* 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> = {}): 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> = {}): 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);
|
||
});
|
||
});
|
||
});
|