diff --git a/apps/web/src/stores/health.test.ts b/apps/web/src/stores/health.test.ts new file mode 100644 index 0000000..08cd421 --- /dev/null +++ b/apps/web/src/stores/health.test.ts @@ -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'); + }); + }); +});