Files
hms/apps/web/src/stores/workbenchStore.test.ts
iven 7a73a90238
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
test(web): Store 单元测试 — plugin(25) + workbench(27) = 52 新测试
plugin.test.ts: fetchPlugins/refreshMenuItems/pluginMenuGroups 全覆盖
workbenchStore.test.ts: selectTask/setTab/refreshTasks/refreshStats/completeTask 全覆盖

前端 Store 测试总数: 22 → 140 (6 个文件)
2026-05-03 19:49:08 +08:00

407 lines
14 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.
/**
* 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);
});
});
});