import { describe, it, expect, beforeEach, vi } from 'vitest'; import { screen, fireEvent, waitFor } from '@testing-library/react'; import { renderWithProviders } from '../../test/utils'; vi.mock('../../lib/api', () => ({ api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() }, ApiError: class extends Error {}, })); import { api } from '../../lib/api'; import { useAuthStore } from '../../stores/authStore'; import NewAppointmentModal from './NewAppointmentModal'; const get = api.get as ReturnType; const post = api.post as ReturnType; const START = 1_900_000_000; const slot = { start: 0, end: 0, start_time: '', end_time: '', doctor_uuid: 'doc1', doctor_name: 'مینا یوسفی', }; const resource = { uuid: 'r-2', name: 'لیزر CO2' }; const services = [ { uuid: 's-1', name: 'کرایوتراپی', duration_minutes: 25, price_rials: 1_000_000, service_section: { uuid: 'sec-1', name: 'خدمات درمانگاه' } }, { uuid: 's-2', name: 'RF فرکشنال', duration_minutes: 50, price_rials: 2_000_000, service_section: { uuid: 'sec-1', name: 'خدمات درمانگاه' } }, ]; /** بیمارِ یافت‌شده تا فرم معتبر شود؛ زمان‌های خالی از تقویم منبع. */ function mockApi() { get.mockImplementation((url: string) => { if (url.includes('patient-lookup')) { return Promise.resolve({ success: true, data: { found: true, name: 'رضا رحیمی', mobile: '09120001307', national_code: '0012345678' } }); } if (url === '/api/v1/staff') { return Promise.resolve({ success: true, data: [{ uuid: 'st-1', full_name: 'پرسنل یک' }] }); } if (url.includes('/treatment-protocol')) { return Promise.resolve({ success: true, data: { staff: [{ uuid: 'st-1', name: 'پرسنل یک' }] } }); } if (url.includes('/service-slots')) { return Promise.resolve({ success: true, data: { total_duration_minutes: 25, start_times: [{ start: START, end: START + 1500, start_time: '12:00', end_time: '12:25' }], } }); } return Promise.resolve({ success: true, data: {} }); }); } /** انتخاب بخش → سرویس → زمان، همان مسیرِ مودالِ طرح. */ async function pickServiceAndTime() { // مودال حالا دو combobox دارد — بخش و اپراتور. سراغ بخش با لیبل خودش می‌رویم. fireEvent.keyDown(screen.getByLabelText('بخش'), { key: 'ArrowDown' }); fireEvent.click(await screen.findByText('خدمات درمانگاه')); // ردیف سرویس یک checkbox است نه دکمه: انتخابش حالت دارد و باید برای screen reader // «انتخاب‌شده/نشده» اعلام شود. fireEvent.click(await screen.findByRole('checkbox', { name: /کرایوتراپی/ })); fireEvent.click(await screen.findByRole('button', { name: '12:00' })); } beforeEach(() => { get.mockReset(); post.mockReset(); useAuthStore.setState({ primaryRole: 'clinic' } as any); post.mockResolvedValue({ success: true, data: { uuid: 'new1' } }); mockApi(); }); describe('مودال ثبت نوبتِ منبع', () => { it('نام منبع و پزشک ناظر بالای فرم می‌آید و زمان‌ها از تقویم منبع خوانده می‌شوند', async () => { renderWithProviders( {}} onSuccess={() => {}} />, ); expect(screen.getByText('لیزر CO2')).toBeInTheDocument(); expect(screen.getByText('مینا یوسفی')).toBeInTheDocument(); await pickServiceAndTime(); await waitFor(() => expect( get.mock.calls.some((c: any[]) => typeof c[0] === 'string' && c[0].includes('/api/v1/resource/r-2/service-slots')), ).toBe(true)); }); it('ثبت، نوبت را با resource_uuid و مدتِ سرویس‌ها می‌فرستد', async () => { renderWithProviders( {}} onSuccess={() => {}} />, ); await pickServiceAndTime(); fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } }); fireEvent.click(screen.getByRole('button', { name: 'جستجو' })); await screen.findByText('بیمار یافت شد'); fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' })); await waitFor(() => expect(post).toHaveBeenCalled()); const [url, body] = post.mock.calls[0]; expect(url).toBe('/api/v1/my/appointment'); expect(body).toMatchObject({ resource_uuid: 'r-2', duration_from_services: true, service_item_uuids: ['s-1'], service_durations: { 's-1': 25 }, slot_start: START, patient_national_code: '0012345678', }); }); it('بدون انتخاب سرویس و زمان، ثبت غیرفعال می‌ماند', async () => { renderWithProviders( {}} onSuccess={() => {}} />, ); fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } }); fireEvent.click(screen.getByRole('button', { name: 'جستجو' })); await screen.findByText('بیمار یافت شد'); expect(screen.getByRole('button', { name: 'ثبت نوبت' })).toBeDisabled(); }); it('دلیلِ غیرفعال بودنِ ثبت را می‌گوید و با پیشرفتِ فرم عوض می‌شود', async () => { renderWithProviders( {}} onSuccess={() => {}} />, ); expect(await screen.findByText(/یک سرویس انتخاب کنید/)).toBeInTheDocument(); // مودال حالا دو combobox دارد — بخش و اپراتور. سراغ بخش با لیبل خودش می‌رویم. fireEvent.keyDown(screen.getByLabelText('بخش'), { key: 'ArrowDown' }); fireEvent.click(await screen.findByText('خدمات درمانگاه')); fireEvent.click(await screen.findByRole('checkbox', { name: /کرایوتراپی/ })); // سرویس هست، زمان نه — پیام باید مرحلهٔ بعد را نشان دهد نه همان قبلی. expect(await screen.findByText(/ساعت شروع را انتخاب کنید/)).toBeInTheDocument(); fireEvent.click(await screen.findByRole('button', { name: '12:00' })); expect(await screen.findByText(/ابتدا بیمار را جستجو کنید/)).toBeInTheDocument(); }); it('تاریخ نوبت به‌صورت جلالی بالای فرم می‌آید', () => { renderWithProviders( {}} onSuccess={() => {}} />, ); // بدون این، کاربر روزِ در حال رزرو را هیچ‌جای مودال نمی‌دید. expect(screen.getByText('تاریخ:')).toBeInTheDocument(); expect(screen.getByText('۱۴۰۵/۰۵/۱۳')).toBeInTheDocument(); }); it('معیار جستجو یک seg با حالتِ اعلام‌شده است', () => { renderWithProviders( {}} onSuccess={() => {}} />, ); const national = screen.getByRole('button', { name: 'کد ملی' }); const mobile = screen.getByRole('button', { name: 'شماره موبایل' }); expect(national).toHaveAttribute('aria-pressed', 'true'); expect(mobile).toHaveAttribute('aria-pressed', 'false'); fireEvent.click(mobile); expect(screen.getByRole('button', { name: 'شماره موبایل' })).toHaveAttribute('aria-pressed', 'true'); expect(screen.getByPlaceholderText('مثال: 09123456789')).toBeInTheDocument(); }); /** آنچه در «طول درمان» سرویس تعریف شده نباید دوباره از منشی پرسیده شود. */ it('پرسنلِ پروتکل سرویس را پیش‌فرض می‌فرستد', async () => { renderWithProviders( {}} onSuccess={() => {}} />, ); await pickServiceAndTime(); fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } }); fireEvent.click(screen.getByRole('button', { name: 'جستجو' })); await screen.findByText('بیمار یافت شد'); await waitFor(() => expect(screen.getByLabelText(/پرسنل/)).toBeInTheDocument()); fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' })); await waitFor(() => expect(post).toHaveBeenCalled()); expect(post.mock.calls[0][1]).toMatchObject({ staff_uuid: 'st-1' }); }); /** پروتکل بدون پرسنل نباید فرم را قفل کند. */ it('پروتکل بدون پرسنل، فیلد را خالی می‌گذارد و ثبت همچنان ممکن است', async () => { get.mockImplementation((url: string) => { if (url.includes('/treatment-protocol')) return Promise.resolve({ success: true, data: null }); if (url.includes('patient-lookup')) { return Promise.resolve({ success: true, data: { found: true, name: 'رضا رحیمی', mobile: '09120001307', national_code: '0012345678' } }); } if (url.includes('/service-slots')) { return Promise.resolve({ success: true, data: { total_duration_minutes: 25, start_times: [{ start: START, end: START + 1500, start_time: '12:00', end_time: '12:25' }], } }); } return Promise.resolve({ success: true, data: [] }); }); renderWithProviders( {}} onSuccess={() => {}} />, ); await pickServiceAndTime(); fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } }); fireEvent.click(screen.getByRole('button', { name: 'جستجو' })); await screen.findByText('بیمار یافت شد'); fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' })); await waitFor(() => expect(post).toHaveBeenCalled()); expect(post.mock.calls[0][1]).not.toHaveProperty('staff_uuid'); }); it('منبعِ بدون سرویس، پیام راهنما می‌دهد نه فهرست خالی', () => { renderWithProviders( {}} onSuccess={() => {}} />, ); expect(screen.getByText(/برای این منبع سرویسی تعریف نشده است/)).toBeInTheDocument(); }); });