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 '../components/appointments/NewAppointmentModal'; const get = api.get as ReturnType; const post = api.post as ReturnType; const slot = { start: 1_800_000_000, end: 1_800_001_800, start_time: '15:00', end_time: '15:30', doctor_uuid: 'doc1', doctor_name: 'دکتر تست' }; /** * فرم ویزارد شد: بیمار و «تأیید و ثبت» دو مرحلهٔ جدا هستند، پس ثبت یک کلیک جلوتر است. * (نوبت اسلاتی مرحلهٔ «خدمت و زمان» ندارد؛ زمانش از خودِ اسلات می‌آید.) */ const goToConfirm = () => fireEvent.click(screen.getByRole('button', { name: 'مرحلهٔ بعد' })); beforeEach(() => { get.mockReset(); post.mockReset(); useAuthStore.setState({ primaryRole: 'doctor' } as any); post.mockResolvedValue({ success: true, data: { uuid: 'new1' } }); }); describe('NewAppointmentModal — جستجوی موبایل‌محور', () => { it('submit stays disabled until a mobile search is done', () => { renderWithProviders( {}} onSuccess={() => {}} />); fireEvent.click(screen.getByRole('button', { name: 'شماره موبایل' })); fireEvent.change(screen.getByPlaceholderText('مثال: 09123456789'), { target: { value: '09121234567' } }); // بدون جستجو، رفتن به مرحلهٔ بعد ممکن نیست expect(screen.getByRole('button', { name: 'مرحلهٔ بعد' })).toBeDisabled(); expect(screen.queryByRole('button', { name: 'ثبت نوبت' })).not.toBeInTheDocument(); }); it('converts Persian digits to English and searches by the normalized mobile', async () => { get.mockResolvedValue({ success: true, data: { found: false } }); renderWithProviders( {}} onSuccess={() => {}} />); fireEvent.click(screen.getByRole('button', { name: 'شماره موبایل' })); const input = screen.getByPlaceholderText('مثال: 09123456789') as HTMLInputElement; fireEvent.change(input, { target: { value: '۰۹۱۲۳۴۵۶۷۸۹' } }); expect(input.value).toBe('09123456789'); fireEvent.click(screen.getByRole('button', { name: 'جستجو' })); await waitFor(() => expect(get).toHaveBeenCalledWith('/api/v1/my/appointment/patient-lookup?mobile=09123456789')); }); it('searches on Enter key in the mobile field', async () => { get.mockResolvedValue({ success: true, data: { found: false } }); renderWithProviders( {}} onSuccess={() => {}} />); fireEvent.click(screen.getByRole('button', { name: 'شماره موبایل' })); const input = screen.getByPlaceholderText('مثال: 09123456789'); fireEvent.change(input, { target: { value: '09121234567' } }); fireEvent.keyDown(input, { key: 'Enter' }); await waitFor(() => expect(get).toHaveBeenCalledWith('/api/v1/my/appointment/patient-lookup?mobile=09121234567')); }); it('found patient with national code books directly without extra fields', async () => { get.mockResolvedValue({ success: true, data: { found: true, name: 'علی محمدی', mobile: '09121234567', national_code: '0012345678' } }); renderWithProviders( {}} onSuccess={() => {}} />); fireEvent.click(screen.getByRole('button', { name: 'شماره موبایل' })); fireEvent.change(screen.getByPlaceholderText('مثال: 09123456789'), { target: { value: '09121234567' } }); fireEvent.click(screen.getByRole('button', { name: 'جستجو' })); await screen.findByText('بیمار یافت شد'); expect(screen.getByText('علی محمدی')).toBeInTheDocument(); // فیلد کد ملی برای بیمارِ یافت‌شده نمایش داده نمی‌شود expect(screen.queryByPlaceholderText('کد ملی ۱۰ رقمی')).toBeNull(); goToConfirm(); fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' })); await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/my/appointment', expect.objectContaining({ doctor_uuid: 'doc1', patient_mobile: '09121234567', patient_name: 'علی محمدی', patient_national_code: '0012345678', }))); }); it('unknown mobile reveals national code + name fields and books the new patient', async () => { get.mockResolvedValue({ success: true, data: { found: false } }); renderWithProviders( {}} onSuccess={() => {}} />); fireEvent.click(screen.getByRole('button', { name: 'شماره موبایل' })); fireEvent.change(screen.getByPlaceholderText('مثال: 09123456789'), { target: { value: '09990001122' } }); fireEvent.click(screen.getByRole('button', { name: 'جستجو' })); const nc = await screen.findByPlaceholderText('کد ملی ۱۰ رقمی'); fireEvent.change(screen.getByPlaceholderText('مثال: علی محمدی'), { target: { value: 'مریم خلیلی' } }); fireEvent.change(nc, { target: { value: '1234567891' } }); goToConfirm(); fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' })); await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/my/appointment', expect.objectContaining({ patient_mobile: '09990001122', patient_name: 'مریم خلیلی', patient_national_code: '1234567891', }))); }); }); describe('NewAppointmentModal — جستجو با کد ملی', () => { it('searches by national_code and books the found patient with its mobile', async () => { get.mockResolvedValue({ success: true, data: { found: true, name: 'رضا کریمی', mobile: '09121112233', national_code: '0012345678' } }); renderWithProviders( {}} onSuccess={() => {}} />); // تغییر معیار جستجو به کد ملی fireEvent.click(screen.getByRole('button', { name: 'کد ملی' })); fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } }); fireEvent.click(screen.getByRole('button', { name: 'جستجو' })); await waitFor(() => expect(get).toHaveBeenCalledWith('/api/v1/my/appointment/patient-lookup?national_code=0012345678')); await screen.findByText('بیمار یافت شد'); goToConfirm(); fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' })); await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/my/appointment', expect.objectContaining({ patient_mobile: '09121112233', patient_name: 'رضا کریمی', patient_national_code: '0012345678', }))); }); it('unknown national code reveals mobile + name fields and books the new patient', async () => { get.mockResolvedValue({ success: true, data: { found: false } }); renderWithProviders( {}} onSuccess={() => {}} />); fireEvent.click(screen.getByRole('button', { name: 'کد ملی' })); fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '1234567891' } }); fireEvent.click(screen.getByRole('button', { name: 'جستجو' })); // موبایل خواسته می‌شود؛ کد ملی فقط همان ورودیِ جستجو است (بدون فیلد تکراری) const mob = await screen.findByPlaceholderText('مثال: 09123456789'); expect(screen.getAllByPlaceholderText('کد ملی ۱۰ رقمی')).toHaveLength(1); fireEvent.change(screen.getByPlaceholderText('مثال: علی محمدی'), { target: { value: 'مریم خلیلی' } }); fireEvent.change(mob, { target: { value: '09990001122' } }); goToConfirm(); fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' })); await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/my/appointment', expect.objectContaining({ patient_mobile: '09990001122', patient_name: 'مریم خلیلی', patient_national_code: '1234567891', }))); }); }); describe('NewAppointmentModal — هزینه ویزیت', () => { /** بیمارِ یافت‌شده را رد می‌کند تا به مرحلهٔ «تأیید و ثبت» برسیم. */ async function bookableWithoutPatient() { fireEvent.click(screen.getByRole('button', { name: 'شماره موبایل' })); fireEvent.change(screen.getByPlaceholderText('مثال: 09123456789'), { target: { value: '09121234567' } }); fireEvent.click(screen.getByRole('button', { name: 'جستجو' })); await screen.findByPlaceholderText('مثال: علی محمدی'); fireEvent.change(screen.getByPlaceholderText('مثال: علی محمدی'), { target: { value: 'علی محمدی' } }); fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } }); goToConfirm(); } /** تعرفه را برای همان پزشکِ اسلات برمی‌گرداند؛ بقیهٔ GETها بیمارِ ناشناس. */ function mockPricing(rials: number, requireVisit = false) { get.mockImplementation((url: string) => url.startsWith('/api/v1/insurance-pricing') ? Promise.resolve({ success: true, data: { free_visit_price_rials: rials, require_visit_price: requireVisit } }) : Promise.resolve({ success: true, data: { found: false } })); } it('تعرفه را با doctor_uuid همان اسلات می‌خواند و در فیلد می‌گذارد', async () => { mockPricing(2_120_000); renderWithProviders( {}} onSuccess={() => {}} />); await waitFor(() => expect(get).toHaveBeenCalledWith('/api/v1/insurance-pricing?doctor_uuid=doc1')); // هزینه در مرحلهٔ تأیید است؛ بدون بیمار هم می‌شود جلو رفت چون دکمه فقط با // نبودِ بیمار قفل است — پس اول بیمار را رد می‌کنیم. await bookableWithoutPatient(); // اختیاری → کلپس بسته است؛ باز کردن آن تا فیلد دیده شود fireEvent.click(screen.getByRole('button', { name: /هزینه ویزیت/ })); // ۲٬۱۲۰٬۰۰۰ ریال = ۲۱۲٬۰۰۰ تومان await waitFor(() => expect(screen.getByPlaceholderText('0')).toHaveValue('۲۱۲٬۰۰۰')); }); it('هزینه ویزیت اختیاری در کلپسِ بسته است و با کلیک باز می‌شود', async () => { mockPricing(0); renderWithProviders( {}} onSuccess={() => {}} />); await bookableWithoutPatient(); // برچسب اختیاری همیشه در سرِ کلپس هست، ولی فیلد در ابتدا پنهان است expect(screen.getByText('(اختیاری)')).toBeInTheDocument(); expect(screen.queryByPlaceholderText('0')).toBeNull(); fireEvent.click(screen.getByRole('button', { name: /هزینه ویزیت/ })); await screen.findByText(/تعرفه‌ای ثبت نشده/); expect(screen.getByPlaceholderText('0')).toHaveValue(''); }); it('با الزامی بودن هزینه ویزیت، کلپس باز است و مبلغ صفر ثبت را غیرفعال می‌کند', async () => { mockPricing(0, true); renderWithProviders( {}} onSuccess={() => {}} />); await bookableWithoutPatient(); // الزامی → بدون کلیک، فیلد و خطا نمایش داده می‌شوند await screen.findByText('هزینه ویزیت الزامی است'); expect(screen.getByPlaceholderText('0')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'ثبت نوبت' })).toBeDisabled(); }); });