test(web): health store 单元测试 — 名称缓存/批量解析/去重
17 个测试覆盖: - getPatientName/getDoctorName 缓存命中/miss - resolvePatientName/resolveDoctorName API 调用+缓存+降级 - 并发去重(同一 id 只触发一次 API 调用) - batchResolve 批量解析 + 部分失败降级 + 输入去重
This commit is contained in:
175
apps/web/src/stores/health.test.ts
Normal file
175
apps/web/src/stores/health.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user