import { describe, it, expect, beforeEach, vi } from 'vitest'; import { screen, fireEvent, waitFor } from '@testing-library/react'; import { Routes, Route } from 'react-router-dom'; import { renderWithProviders } from '../test/utils'; vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } })); 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 { formatRial } from '../lib/utils'; import NewSessionPage from './NewSessionPage'; const get = api.get as ReturnType; const post = api.post as ReturnType; const record = (profileOver: object = {}) => ({ uuid: 'r1', user_name: 'ساغر صابری', profile: { full_name: 'ساغر صابری', basic_insurance_id: null, supplementary_insurance_id: null, ...profileOver }, }); function mockGets(over: Partial> = {}) { get.mockImplementation((url: string) => { if (url === '/api/v1/patient/r1') return Promise.resolve({ success: true, data: over.record ?? record() }); if (url === '/api/v1/service-sections') { return Promise.resolve({ success: true, data: over.sections ?? [{ uuid: 'sec1', name: 'بخش زیبایی' }] }); } if (url === '/api/v1/service-items/sec1') { return Promise.resolve({ success: true, data: over.items ?? [ { uuid: 'svc1', name: 'کاشت مو', price_rials: 2_400_000, active: true, insurance_covered: false, staff: null }, ], }); } if (url === '/api/v1/staff') { return Promise.resolve({ success: true, data: over.staff ?? [{ uuid: 'st1', full_name: 'علی رضایی', active: true }] }); } if (url === '/api/v1/inventory-items') { return Promise.resolve({ success: true, data: { items: over.inventory ?? [{ uuid: 'inv1', name: 'عینک', unit: 'عدد', price: 1_200_000, stock: 10, status: 'in_stock' }], stats: {} }, }); } if (url === '/api/v1/inventory-packages') { return Promise.resolve({ success: true, data: over.packages ?? [{ uuid: 'pkg1', title: 'پکیج شماره یک', total: 2_400_000, available: true }] }); } if (url === '/api/v1/billing/tenant-insurances') { return Promise.resolve({ success: true, data: { data: over.contracts ?? [] } }); } if (url === '/api/v1/insurance-pricing') { return Promise.resolve({ success: true, data: { free_visit_price_rials: 500_000 } }); } if (url === '/api/v1/patient/r1/sessions') { return Promise.resolve({ success: true, data: over.sessions ?? [] }); } if (url === '/api/v1/patient/r1/wallet') { return Promise.resolve({ success: true, data: { balance_rials: 300_000, recent_transactions: [] } }); } return Promise.resolve({ success: true, data: [] }); }); } function renderPage() { return renderWithProviders( } /> , { route: '/admin/patients/r1/session/new' }, ); } /** انتخاب گزینه از SearchableSelect (react-select) با inputId */ async function pickOption(inputId: string, option: string) { const input = document.getElementById(inputId) as HTMLInputElement; fireEvent.focus(input); fireEvent.keyDown(input, { key: 'ArrowDown' }); fireEvent.click(await screen.findByText(option)); } beforeEach(() => { get.mockReset(); post.mockReset(); }); describe('NewSessionPage (ویزارد ثبت مراجعه — پورت tauri create-service)', () => { it('renders the 3-step stepper and step-1 fields (tauri Step1Details)', async () => { mockGets(); renderPage(); // استپر سه گام («ایجاد سرویس» هم برچسب گام است هم دکمه ثبت) expect((await screen.findAllByText('ایجاد سرویس')).length).toBeGreaterThanOrEqual(2); expect(screen.getByText('پرداخت')).toBeInTheDocument(); expect(screen.getByText('جزییات')).toBeInTheDocument(); // فیلدهای گام ۱ expect(screen.getByText('تاریخ پذیرش')).toBeInTheDocument(); expect(screen.getByText('ساعت پذیرش')).toBeInTheDocument(); expect(screen.getByText('انتخاب بخش')).toBeInTheDocument(); expect(screen.getByText('انتخاب سرویس')).toBeInTheDocument(); expect(screen.getByText('پرسنل')).toBeInTheDocument(); expect(screen.getByText('انتخاب کالای مصرفی')).toBeInTheDocument(); expect(screen.getByText('انتخاب پکیج')).toBeInTheDocument(); // پذیرش‌کننده + breadcrumb بیمار expect(screen.getByText(/پذیرش کننده:/)).toBeInTheDocument(); expect((await screen.findAllByText('ساغر صابری')).length).toBeGreaterThan(0); // بدون سرویس بیمه‌دار و بدون بیمه‌ی پروفایل → بلوک بیمه پنهان expect(screen.queryByText('بیمه پایه')).not.toBeInTheDocument(); }); it('shows the insurance block when patient profile has insurance', async () => { mockGets({ record: record({ basic_insurance_id: 7 }) }); renderPage(); expect(await screen.findByText('بیمه پایه')).toBeInTheDocument(); expect(screen.getByText('بیمه تکمیلی')).toBeInTheDocument(); }); it('creates the session with session_at, consumables, package and staff then moves to payment step', async () => { mockGets({ sessions: [{ uuid: 's-new', services: [], visit_price_rials: 500_000, final_price_rials: 1_700_000, patient_debt_rials: 1_700_000, discount_rials: 0, payments: [], is_paid: false, }], }); post.mockResolvedValue({ success: true, data: { uuid: 's-new' } }); renderPage(); await screen.findByText('انتخاب بخش'); // بخش → سرویس → افزودن await pickOption('section-select', 'بخش زیبایی'); await waitFor(() => expect(get).toHaveBeenCalledWith('/api/v1/service-items/sec1')); await pickOption('service-select', 'کاشت مو'); fireEvent.click(screen.getByLabelText('افزودن سرویس')); expect(screen.getByText('خدمات انتخاب شده')).toBeInTheDocument(); // کالای مصرفی: عینک await pickOption('consumable-select', 'عینک'); fireEvent.click(screen.getByLabelText('افزودن کالا')); expect(screen.getByText(`مجموع قیمت کالاها: ${formatRial(1_200_000)}`)).toBeInTheDocument(); // ثبت fireEvent.click(screen.getByText('ایجاد سرویس', { selector: 'button' })); await waitFor(() => expect(post).toHaveBeenCalledTimes(1)); const [url, body] = post.mock.calls[0] as [string, any]; expect(url).toBe('/api/v1/patient/r1/session'); expect(typeof body.session_at).toBe('number'); expect(body.payment_method).toBe('pending'); expect(body.services).toEqual([{ service_item_uuid: 'svc1', quantity: 1 }]); expect(body.consumables).toEqual([{ inventory_item_uuid: 'inv1', quantity: 1 }]); // بدون بیمه → درصدها صفر و id ارسال نمی‌شود expect(body.base_insurance_discount_percent).toBe(0); expect(body.insurance_base_id).toBeUndefined(); // ورود به گام پرداخت با دیتای واقعی session ساخته‌شده expect(await screen.findByText('هزینه سرویس:')).toBeInTheDocument(); expect(screen.getByText('موجودی کیف پول:')).toBeInTheDocument(); expect(screen.getAllByText(formatRial(1_700_000)).length).toBeGreaterThan(0); }); it('increments consumable quantity via the counter and reflects the total', async () => { mockGets(); renderPage(); await screen.findByText('انتخاب کالای مصرفی'); await pickOption('consumable-select', 'عینک'); fireEvent.click(screen.getByLabelText('افزودن کالا')); fireEvent.click(screen.getByLabelText('افزایش')); expect(screen.getByText(`مجموع قیمت کالاها: ${formatRial(2_400_000)}`)).toBeInTheDocument(); // کاهش تا صفر → حذف fireEvent.click(screen.getByLabelText('کاهش')); fireEvent.click(screen.getByLabelText('کاهش')); expect(screen.getByText('کالایی انتخاب نشده است')).toBeInTheDocument(); }); it('renders empty option lists without crashing (بدون بخش/کالا/پکیج)', async () => { mockGets({ sections: [], inventory: [], packages: [], staff: [] }); renderPage(); expect(await screen.findByText('انتخاب بخش')).toBeInTheDocument(); expect(screen.getByText('کالایی انتخاب نشده است')).toBeInTheDocument(); // مجموع صفر expect(screen.getByText(`مجموع قیمت کالاها: ${formatRial(0)}`)).toBeInTheDocument(); }); });