import { describe, it, expect, beforeEach, vi } from 'vitest'; import { screen, fireEvent, waitFor } from '@testing-library/react'; import { Routes, Route } from 'react-router'; 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 AppointmentEditPage from './AppointmentEditPage'; const get = api.get as ReturnType; const patch = api.patch as ReturnType; const slotStart = Math.floor(new Date('2026-08-01T15:00').getTime() / 1000); const slotEnd = Math.floor(new Date('2026-08-01T16:00').getTime() / 1000); beforeEach(() => { get.mockReset(); patch.mockReset(); get.mockImplementation((url: string) => { if (url === '/api/v1/appointment/ap1') return Promise.resolve({ success: true, data: { data: { uuid: 'ap1', slot_start: slotStart, slot_end: slotEnd, status: 'confirmed', version: 4, note: 'یادداشت', deposit_required: true, deposit_amount_rials: 5000000, service_section: { uuid: 'sec1', name: 'زیبایی' }, service_item: { uuid: 'it1', name: 'لیزر' }, staff: { uuid: 'st1', full_name: 'سحر ایمانی' }, } } }); if (url === '/api/v1/service-sections') return Promise.resolve({ success: true, data: [{ uuid: 'sec1', name: 'زیبایی' }] }); if (url.startsWith('/api/v1/service-items/')) return Promise.resolve({ success: true, data: [{ uuid: 'it1', name: 'لیزر' }] }); if (url === '/api/v1/staff') return Promise.resolve({ success: true, data: [{ uuid: 'st1', full_name: 'سحر ایمانی' }] }); return Promise.resolve({ success: true, data: [] }); }); patch.mockResolvedValue({ success: true, data: {} }); }); function renderEdit() { return renderWithProviders( } />, { route: '/admin/appointments/ap1/edit' }, ); } describe('AppointmentEditPage (ویرایش نوبت)', () => { it('hydrates the form from the appointment', async () => { renderEdit(); expect(await screen.findByText('مشخصات سرویس:')).toBeInTheDocument(); expect((screen.getByLabelText('ساعت شروع') as HTMLInputElement).value).toBe('15:00'); expect(screen.getByText('قطعی شده')).toBeInTheDocument(); // react-select single value = confirmed expect(screen.getByDisplayValue('یادداشت')).toBeInTheDocument(); }); // بخش و سرویس هر دو باید برچسب واقعی نشان دهند، نه placeholder. فهرست سرویس‌ها // از بخش می‌آید، پس بخشِ خالی یعنی سرویسِ ذخیره‌شده هم انتخاب‌نشده دیده می‌شود — // همان باگی که در ۲۰۲۶-۰۸-۰۸ گزارش شد. it('shows the stored section and service, not their placeholders', async () => { renderEdit(); await screen.findByText('مشخصات سرویس:'); expect(await screen.findByText('زیبایی')).toBeInTheDocument(); expect(await screen.findByText('لیزر')).toBeInTheDocument(); expect(screen.queryByText('انتخاب بخش')).not.toBeInTheDocument(); expect(screen.queryByText('انتخاب سرویس')).not.toBeInTheDocument(); }); // بلوک بیعانه فعلاً از فرم برداشته شده، ولی مقدارش همچنان ذخیره می‌شود // (اثباتش در تست بعدی: deposit_required در payload هست). it('no longer renders the deposit block', async () => { renderEdit(); await screen.findByText('مشخصات سرویس:'); expect(screen.queryByText('بیعانه:')).not.toBeInTheDocument(); expect(screen.queryByText('بیعانه مورد نیاز است.')).not.toBeInTheDocument(); }); it('patches the general update endpoint with the edited values', async () => { renderEdit(); await screen.findByText('مشخصات سرویس:'); fireEvent.change(screen.getByLabelText('ساعت پایان'), { target: { value: '16:30' } }); fireEvent.click(screen.getByRole('button', { name: 'ثبت اطلاعات' })); await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/appointment/ap1', expect.objectContaining({ slot_start: slotStart, slot_end: Math.floor(new Date('2026-08-01T16:30').getTime() / 1000), service_section_uuid: 'sec1', staff_uuid: 'st1', deposit_required: true, version: 4, }))); }); }); // ── بیمهٔ نوبت ──────────────────────────────────────────────────────────────── const CONTRACT = { insurance_id: 3, insurance_name: 'بیمه ایران', insurance_kind: 'basic', is_active: true, coverage_percent: 70, franchise_percent: 0, annual_ceiling_rials: null, category_coverages: { outpatient: 70, inpatient: 30 }, }; /** همان mock بالا + payload بیمه؛ `enabled` تعیین می‌کند چند نوع خدمت فعال است. */ function mockWithInsurance(enabledCategories: string[]) { const categories = [ { key: 'outpatient', label: 'خدمات سرپایی', enabled: enabledCategories.includes('outpatient') }, { key: 'inpatient', label: 'خدمات بستری', enabled: enabledCategories.includes('inpatient') }, ]; get.mockImplementation((url: string) => { if (url === '/api/v1/appointment/ap1') return Promise.resolve({ success: true, data: { data: { uuid: 'ap1', slot_start: slotStart, slot_end: slotEnd, status: 'confirmed', version: 4, visit_price_rials: 5_952_000, service_items: [], insurance_service_category: 'inpatient', insurance_base_id: 3, } } }); if (url === '/api/v1/insurance-pricing') return Promise.resolve({ success: true, data: { service_categories: categories, default_service_category: enabledCategories.length === 1 ? enabledCategories[0] : null, } }); if (url === '/api/v1/billing/tenant-insurances') return Promise.resolve({ success: true, data: { data: [CONTRACT] } }); return Promise.resolve({ success: true, data: [] }); }); } describe('AppointmentEditPage — بیمه', () => { it('نوع خدمت و بیمه از نوبت پیش‌پر می‌شوند و سهم‌ها نمایش داده می‌شوند', async () => { mockWithInsurance(['outpatient', 'inpatient']); renderEdit(); expect(await screen.findByText('بیمه:')).toBeInTheDocument(); expect(screen.getByText('نوع خدمت')).toBeInTheDocument(); expect(screen.getByText('خدمات بستری')).toBeInTheDocument(); expect(screen.getByText('بیمه ایران')).toBeInTheDocument(); // ۵٬۹۵۲٬۰۰۰ × ۳۰٪ → سهم بیمه ۱٬۷۸۵٬۶۰۰ و سهم بیمار ۴٬۱۶۶٬۴۰۰ expect(screen.getByText('سهم بیمه / سهم بیمار')).toBeInTheDocument(); }); it('با فعال بودن فقط یک نوع، انتخاب نوع خدمت پنهان است', async () => { mockWithInsurance(['outpatient']); renderEdit(); expect(await screen.findByText('بیمه:')).toBeInTheDocument(); expect(screen.queryByText('نوع خدمت')).not.toBeInTheDocument(); }); it('PATCH فیلدهای بیمه را می‌فرستد', async () => { mockWithInsurance(['outpatient', 'inpatient']); renderEdit(); // تا فرم از نوبت پر نشود دکمه غیرفعال است؛ همان را معیار آماده‌بودن می‌گیریم. await waitFor(() => expect(screen.getByRole('button', { name: 'ثبت اطلاعات' })).not.toBeDisabled()); fireEvent.click(screen.getByRole('button', { name: 'ثبت اطلاعات' })); await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/appointment/ap1', expect.objectContaining({ insurance_service_category: 'inpatient', insurance_base_id: 3, }))); }); });