test(web): health store 单元测试 — 名称缓存/批量解析/去重
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

17 个测试覆盖:
- getPatientName/getDoctorName 缓存命中/miss
- resolvePatientName/resolveDoctorName API 调用+缓存+降级
- 并发去重(同一 id 只触发一次 API 调用)
- batchResolve 批量解析 + 部分失败降级 + 输入去重
This commit is contained in:
iven
2026-05-03 10:05:27 +08:00
parent 9d07ea0be0
commit bba47b7b1c

View File

@@ -0,0 +1,175 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useHealthStore } from './health';
vi.mock('../api/health/patients', () => ({
patientApi: {
get: vi.fn(),
},
}));
vi.mock('../api/health/doctors', () => ({
doctorApi: {
get: vi.fn(),
},
}));
import { patientApi } from '../api/health/patients';
import { doctorApi } from '../api/health/doctors';
const mockedPatientGet = vi.mocked(patientApi.get);
const mockedDoctorGet = vi.mocked(doctorApi.get);
describe('useHealthStore', () => {
beforeEach(() => {
vi.clearAllMocks();
useHealthStore.setState({
patientNames: {},
doctorNames: {},
loadingIds: new Set(),
});
});
describe('initial state', () => {
it('starts with empty caches', () => {
const s = useHealthStore.getState();
expect(s.patientNames).toEqual({});
expect(s.doctorNames).toEqual({});
expect(s.loadingIds.size).toBe(0);
});
});
describe('getPatientName / getDoctorName', () => {
it('returns truncated id when name not cached', () => {
expect(useHealthStore.getState().getPatientName('abcdefgh-1234')).toBe('abcdefgh');
expect(useHealthStore.getState().getDoctorName('xyz98765-4321')).toBe('xyz98765');
});
it('returns cached name when available', () => {
useHealthStore.setState({ patientNames: { id1: 'Alice' }, doctorNames: { id2: 'Dr.Bob' } });
expect(useHealthStore.getState().getPatientName('id1')).toBe('Alice');
expect(useHealthStore.getState().getDoctorName('id2')).toBe('Dr.Bob');
});
});
describe('resolvePatientName', () => {
it('returns cached name without API call', async () => {
useHealthStore.setState({ patientNames: { pid1: '张三' } });
const name = await useHealthStore.getState().resolvePatientName('pid1');
expect(name).toBe('张三');
expect(mockedPatientGet).not.toHaveBeenCalled();
});
it('fetches and caches name on success', async () => {
mockedPatientGet.mockResolvedValueOnce({ id: 'p1', name: '李四' } as any);
const name = await useHealthStore.getState().resolvePatientName('p1');
expect(name).toBe('李四');
expect(useHealthStore.getState().patientNames['p1']).toBe('李四');
});
it('falls back to truncated id on API failure', async () => {
mockedPatientGet.mockRejectedValueOnce(new Error('not found'));
const name = await useHealthStore.getState().resolvePatientName('abcdefgh-xxx');
expect(name).toBe('abcdefgh');
expect(useHealthStore.getState().patientNames['abcdefgh-xxx']).toBe('abcdefgh');
});
it('deduplicates concurrent calls for same id', async () => {
let resolve: (v: any) => void;
const promise = new Promise((r) => { resolve = r; });
mockedPatientGet.mockReturnValueOnce(promise as any);
const r1 = useHealthStore.getState().resolvePatientName('dup');
const r2 = useHealthStore.getState().resolvePatientName('dup');
resolve!({ id: 'dup', name: '王五' } as any);
const [n1, n2] = await Promise.all([r1, r2]);
expect(n1).toBe('王五');
expect(typeof n2).toBe('string');
expect(mockedPatientGet).toHaveBeenCalledTimes(1);
});
});
describe('resolveDoctorName', () => {
it('returns cached name without API call', async () => {
useHealthStore.setState({ doctorNames: { did1: '赵医生' } });
const name = await useHealthStore.getState().resolveDoctorName('did1');
expect(name).toBe('赵医生');
expect(mockedDoctorGet).not.toHaveBeenCalled();
});
it('fetches and caches name on success', async () => {
mockedDoctorGet.mockResolvedValueOnce({ id: 'd1', name: '孙医生' } as any);
const name = await useHealthStore.getState().resolveDoctorName('d1');
expect(name).toBe('孙医生');
expect(useHealthStore.getState().doctorNames['d1']).toBe('孙医生');
});
it('falls back to truncated id on API failure', async () => {
mockedDoctorGet.mockRejectedValueOnce(new Error('not found'));
const name = await useHealthStore.getState().resolveDoctorName('doctor123-abc');
expect(name).toBe('doctor12');
});
});
describe('batchResolvePatientNames', () => {
it('skips already cached ids', async () => {
useHealthStore.setState({ patientNames: { p1: 'cached' } });
await useHealthStore.getState().batchResolvePatientNames(['p1']);
expect(mockedPatientGet).not.toHaveBeenCalled();
});
it('resolves multiple uncached ids', async () => {
mockedPatientGet
.mockResolvedValueOnce({ id: 'b1', name: 'batch1' } as any)
.mockResolvedValueOnce({ id: 'b2', name: 'batch2' } as any);
await useHealthStore.getState().batchResolvePatientNames(['b1', 'b2']);
expect(useHealthStore.getState().patientNames['b1']).toBe('batch1');
expect(useHealthStore.getState().patientNames['b2']).toBe('batch2');
});
it('handles partial failures gracefully', async () => {
mockedPatientGet
.mockResolvedValueOnce({ id: 'ok', name: '成功' } as any)
.mockRejectedValueOnce(new Error('fail'));
await useHealthStore.getState().batchResolvePatientNames(['ok', 'failid123456']);
expect(useHealthStore.getState().patientNames['ok']).toBe('成功');
expect(useHealthStore.getState().patientNames['failid123456']).toBe('failid12');
});
it('deduplicates input ids', async () => {
mockedPatientGet.mockResolvedValue({ id: 'dup', name: '去重' } as any);
await useHealthStore.getState().batchResolvePatientNames(['dup', 'dup', 'dup']);
expect(mockedPatientGet).toHaveBeenCalledTimes(1);
});
});
describe('batchResolveDoctorNames', () => {
it('skips already cached ids', async () => {
useHealthStore.setState({ doctorNames: { d1: 'cached' } });
await useHealthStore.getState().batchResolveDoctorNames(['d1']);
expect(mockedDoctorGet).not.toHaveBeenCalled();
});
it('resolves multiple uncached ids', async () => {
mockedDoctorGet
.mockResolvedValueOnce({ id: 'bd1', name: '陈医生' } as any)
.mockResolvedValueOnce({ id: 'bd2', name: '周医生' } as any);
await useHealthStore.getState().batchResolveDoctorNames(['bd1', 'bd2']);
expect(useHealthStore.getState().doctorNames['bd1']).toBe('陈医生');
expect(useHealthStore.getState().doctorNames['bd2']).toBe('周医生');
});
it('handles partial failures gracefully', async () => {
mockedDoctorGet
.mockResolvedValueOnce({ id: 'ok', name: '成功' } as any)
.mockRejectedValueOnce(new Error('fail'));
await useHealthStore.getState().batchResolveDoctorNames(['ok', 'failid123456']);
expect(useHealthStore.getState().doctorNames['ok']).toBe('成功');
expect(useHealthStore.getState().doctorNames['failid123456']).toBe('failid12');
});
});
});