diff --git a/apps/miniprogram/__tests__/helpers/index.ts b/apps/miniprogram/__tests__/helpers/index.ts new file mode 100644 index 0000000..58fefc0 --- /dev/null +++ b/apps/miniprogram/__tests__/helpers/index.ts @@ -0,0 +1,2 @@ +export { mockTaro, createTaroMock } from './mock-taro'; +export { mockApi, apiOk } from './mock-api'; diff --git a/apps/miniprogram/__tests__/helpers/mock-api.ts b/apps/miniprogram/__tests__/helpers/mock-api.ts new file mode 100644 index 0000000..d2c40fa --- /dev/null +++ b/apps/miniprogram/__tests__/helpers/mock-api.ts @@ -0,0 +1,19 @@ +import { vi } from 'vitest'; + +// 顶层 mock — vitest 自动提升,无警告 +vi.mock('@/services/request', () => ({ + api: { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + }, + clearRequestCache: vi.fn(), + markLoggingOut: vi.fn(), + clearLoggingOut: vi.fn(), +})); + +/** 创建一个成功的 API 响应 */ +export function apiOk(data: T) { + return Promise.resolve(data); +} diff --git a/apps/miniprogram/__tests__/helpers/mock-taro.ts b/apps/miniprogram/__tests__/helpers/mock-taro.ts new file mode 100644 index 0000000..f45f279 --- /dev/null +++ b/apps/miniprogram/__tests__/helpers/mock-taro.ts @@ -0,0 +1,50 @@ +import { vi } from 'vitest'; + +/** + * Taro API mock — 所有小程序测试共享的基础 mock。 + * storage 使用内存 Map,可在 beforeEach 中 clear。 + */ +export function createTaroMock() { + const storage = new Map(); + + return { + storage, + mock: { + default: { + getStorageSync: vi.fn((key: string) => storage.get(key) ?? ''), + setStorageSync: vi.fn((key: string, val: any) => storage.set(key, val)), + removeStorageSync: vi.fn((key: string) => storage.delete(key)), + showToast: vi.fn(), + hideToast: vi.fn(), + showLoading: vi.fn(), + hideLoading: vi.fn(), + request: vi.fn(), + reLaunch: vi.fn(), + navigateTo: vi.fn(), + redirectTo: vi.fn(), + switchTab: vi.fn(), + getCurrentPages: vi.fn(() => []), + getAccountInfoSync: vi.fn(() => ({ miniProgram: { envVersion: 'develop' } })), + getSystemInfoSync: vi.fn(() => ({ + windowHeight: 800, + windowWidth: 375, + pixelRatio: 2, + })), + }, + }, + }; +} + +/** 一键 mock @tarojs/taro — 在 vi.mock 回调中使用 */ +export function mockTaro() { + const { storage, mock } = createTaroMock(); + + vi.mock('@tarojs/taro', () => mock); + vi.mock('@/utils/secure-storage', () => ({ + secureGet: vi.fn((key: string) => storage.get(key) ?? ''), + secureSet: vi.fn((key: string, val: string) => storage.set(key, val)), + secureRemove: vi.fn((key: string) => storage.delete(key)), + })); + + return { storage }; +} diff --git a/apps/miniprogram/__tests__/services/appointment.test.ts b/apps/miniprogram/__tests__/services/appointment.test.ts new file mode 100644 index 0000000..32deeff --- /dev/null +++ b/apps/miniprogram/__tests__/services/appointment.test.ts @@ -0,0 +1,149 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import '../helpers/mock-api'; + +import { api } from '@/services/request'; +import { + listAppointments, + getAppointment, + createAppointment, + cancelAppointment, + getDoctorSchedules, + listDoctors, + calendarView, +} from '@/services/appointment'; + +describe('appointment service', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('listAppointments', () => { + it('无 patientId 时传递分页参数', async () => { + const mock = { data: [], total: 0 }; + vi.mocked(api.get).mockResolvedValueOnce(mock); + + await listAppointments(); + + expect(api.get).toHaveBeenCalledWith('/health/appointments', { + page: 1, + page_size: 20, + }); + }); + + it('有 patientId 时附加 patient_id', async () => { + vi.mocked(api.get).mockResolvedValueOnce({ data: [], total: 0 }); + + await listAppointments('p-123'); + + expect(api.get).toHaveBeenCalledWith('/health/appointments', { + page: 1, + page_size: 20, + patient_id: 'p-123', + }); + }); + }); + + describe('getAppointment', () => { + it('调用 api.get 拼接 ID', async () => { + const mock = { id: 'a-1', status: 'confirmed', version: 1 } as any; + vi.mocked(api.get).mockResolvedValueOnce(mock); + + const result = await getAppointment('a-1'); + + expect(api.get).toHaveBeenCalledWith('/health/appointments/a-1'); + expect(result).toEqual(mock); + }); + }); + + describe('createAppointment', () => { + it('调用 api.post 传递预约数据', async () => { + const input = { + patient_id: 'p-1', + doctor_id: 'd-1', + appointment_date: '2026-06-01', + start_time: '09:00', + end_time: '09:30', + }; + vi.mocked(api.post).mockResolvedValueOnce({ id: 'ap-1', ...input }); + + await createAppointment(input); + + expect(api.post).toHaveBeenCalledWith('/health/appointments', input); + }); + }); + + describe('cancelAppointment', () => { + it('调用 api.put 传递 cancelled 状态和 version', async () => { + vi.mocked(api.put).mockResolvedValueOnce({}); + + await cancelAppointment('ap-1', 2); + + expect(api.put).toHaveBeenCalledWith('/health/appointments/ap-1/status', { + status: 'cancelled', + version: 2, + }); + }); + }); + + describe('getDoctorSchedules', () => { + it('返回排班并计算 available_count', async () => { + vi.mocked(api.get).mockResolvedValueOnce({ + data: [ + { id: 's-1', doctor_id: 'd-1', date: '2026-06-01', start_time: '09:00', end_time: '12:00', max_appointments: 10, current_appointments: 3 }, + ], + total: 1, + }); + + const result = await getDoctorSchedules('d-1', '2026-06-01', '2026-06-07'); + + expect(result.data[0].available_count).toBe(7); + }); + }); + + describe('listDoctors', () => { + it('无 department 时获取全部', async () => { + vi.mocked(api.get).mockResolvedValueOnce({ data: [], total: 0 }); + + await listDoctors(); + + expect(api.get).toHaveBeenCalledWith('/health/doctors', { page_size: 100 }); + }); + + it('有 department 时过滤', async () => { + vi.mocked(api.get).mockResolvedValueOnce({ data: [], total: 0 }); + + await listDoctors('内科'); + + expect(api.get).toHaveBeenCalledWith('/health/doctors', { + page_size: 100, + department: '内科', + }); + }); + }); + + describe('calendarView', () => { + it('展平嵌套结构并计算 available_count', async () => { + vi.mocked(api.get).mockResolvedValueOnce([ + { + date: '2026-06-01', + schedules: [ + { id: 's-1', doctor_id: 'd-1', start_time: '09:00', end_time: '12:00', max_appointments: 5, current_appointments: 2 }, + ], + }, + { + date: '2026-06-02', + schedules: [ + { id: 's-2', doctor_id: 'd-1', start_time: '14:00', end_time: '17:00', max_appointments: 8, current_appointments: 1 }, + ], + }, + ] as any); + + const result = await calendarView('2026-06-01', '2026-06-07'); + + expect(result).toHaveLength(2); + expect(result[0].available_count).toBe(3); + expect(result[1].available_count).toBe(7); + expect(result[0].date).toBe('2026-06-01'); + }); + }); +}); diff --git a/apps/miniprogram/__tests__/services/patient.test.ts b/apps/miniprogram/__tests__/services/patient.test.ts new file mode 100644 index 0000000..0d69d05 --- /dev/null +++ b/apps/miniprogram/__tests__/services/patient.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +// mock-api.ts 的顶层 vi.mock 自动生效,无需显式调用 +import '../helpers/mock-api'; + +import { api } from '@/services/request'; +import { listPatients, createPatient, updatePatient } from '@/services/patient'; + +describe('patient service', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('listPatients', () => { + it('调用 api.get 并传递分页参数', async () => { + const mockData = { data: [{ id: '1', name: '张三', version: 1 }], total: 1 }; + vi.mocked(api.get).mockResolvedValueOnce(mockData); + + const result = await listPatients(); + + expect(api.get).toHaveBeenCalledWith('/health/patients', { + page: 1, + page_size: 100, + }); + expect(result).toEqual(mockData); + }); + }); + + describe('createPatient', () => { + it('调用 api.post 传递患者数据', async () => { + const input = { name: '李四', gender: 'male', birth_date: '1990-01-01' }; + const mockPatient = { id: '2', name: '李四', version: 1 }; + vi.mocked(api.post).mockResolvedValueOnce(mockPatient); + + const result = await createPatient(input); + + expect(api.post).toHaveBeenCalledWith('/health/patients', input); + expect(result).toEqual(mockPatient); + }); + }); + + describe('updatePatient', () => { + it('调用 api.put 传递更新数据和 version', async () => { + const input = { name: '王五' }; + const mockPatient = { id: '1', name: '王五', version: 2 }; + vi.mocked(api.put).mockResolvedValueOnce(mockPatient); + + const result = await updatePatient('1', input, 1); + + expect(api.put).toHaveBeenCalledWith('/health/patients/1', { + ...input, + version: 1, + }); + expect(result).toEqual(mockPatient); + }); + }); +});