import { describe, it, expect, beforeEach, vi } from 'vitest'; import { screen, fireEvent } 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 { useAuthStore } from '../stores/authStore'; import ServiceDetailPage from './ServiceDetailPage'; const get = api.get as ReturnType; const ITEM = { uuid: 'it1', name: 'سرم ۵۰۰cc', section_uuid: 'sec1', section_name: 'تزریقات', price_rials: 8_500_000, active: true, bookable: true, duration_minutes: 30, insurance_covered: true, staff: { uuid: 'st1', full_name: 'مریم امینی' }, staff_members: [{ uuid: 'st1', full_name: 'مریم امینی' }], created_at: 1_700_000_000, updated_at: 1_800_000_000, }; const mockApi = (item: unknown = ITEM, notFound = false) => { get.mockImplementation((url: string) => { if (url.includes('/subscription/my')) return Promise.resolve({ success: true, data: { subscription: null, used_trial: false, effective_plan: { name: 'professional', level: 2, max_secretaries: 10, features: { services: true } }, } }); if (url.includes('/payment/config')) return Promise.resolve({ success: true, data: { test_mode: true, gateways: [] } }); if (url.includes('/audit-logs')) return Promise.resolve({ success: true, data: [ { uuid: 'l1', field: 'price_rials', operation: 'update', old_value: '7000000', new_value: '8500000', actor_name: 'دکتر تست', created_at: 1_800_000_000 }, { uuid: 'l2', field: 'name', operation: 'create', old_value: null, new_value: 'سرم ۵۰۰cc', actor_name: 'دکتر تست', created_at: 1_700_000_000 }, ] }); if (url.includes('/service-item/')) { return notFound ? Promise.reject(new Error('یافت نشد')) : Promise.resolve({ success: true, data: item }); } if (url.includes('/tariffs')) return Promise.resolve({ success: true, data: { current_year: 1404, default_price_rials: 8_500_000, data: [ { uuid: 't1', year: 1404, price_rials: 8_500_000, is_active: true }, { uuid: 't2', year: 1403, price_rials: 7_000_000, is_active: false }, ], } }); if (url.includes('/tenant-insurances')) return Promise.resolve({ data: { data: [ { uuid: 'ins1', insurance_name: 'بیمه ایران', insurance_kind: 'basic', coverage_percent: 70 }, ] } }); if (url.includes('/inventory-items')) return Promise.resolve({ success: true, data: { items: [ { uuid: 'inv1', name: 'گاز استریل', unit: 'عدد', price: 50_000, stock: 100 }, ] } }); if (url.includes('/inventory-packages')) return Promise.resolve({ success: true, data: [ { uuid: 'pkg1', title: 'پکیج سرم', total: 1_200_000, available: true, items: [{ itemUuid: 'i1', name: 'سرم رینگر', unit: 'عدد', price: 600_000, amount: 2 }], }, ] }); return Promise.resolve({ success: true, data: [] }); }); }; beforeEach(() => { useAuthStore.setState({ primaryRole: 'doctor' }); get.mockReset(); mockApi(); }); // صفحه uuid را از useParams می‌گیرد، پس به یک Route واقعی نیاز دارد. const render = () => renderWithProviders( } /> , { route: '/admin/clinic-services/it1' }, ); describe('ServiceDetailPage (جزئیات سرویس)', () => { it('اطلاعات پایه سرویس را با breadcrumb نمایش می‌دهد', async () => { render(); expect(await screen.findByRole('heading', { name: 'سرم ۵۰۰cc' })).toBeInTheDocument(); expect(screen.getByText('سرویس‌ها')).toBeInTheDocument(); expect(screen.getAllByText('تزریقات').length).toBeGreaterThan(0); expect(screen.getByText('مریم امینی')).toBeInTheDocument(); expect(screen.getByText('۳۰ دقیقه')).toBeInTheDocument(); expect(screen.getByText('تحت پوشش')).toBeInTheDocument(); }); it('تاریخ ایجاد و آخرین ویرایش را شمسی نشان می‌دهد', async () => { render(); expect(await screen.findByText('تاریخ ایجاد')).toBeInTheDocument(); expect(screen.getByText('آخرین ویرایش')).toBeInTheDocument(); // سال شمسی معادل ۱۷۰۰۰۰۰۰۰۰ ⇒ ۱۴۰۲ expect(screen.getByText(/۱۴۰۲/)).toBeInTheDocument(); }); it('تب تعرفه‌ها فهرست سال‌ها را با نشان «سال جاری» می‌آورد', async () => { render(); fireEvent.click(await screen.findByText('تعرفه‌ها')); expect(await screen.findByText('سال جاری')).toBeInTheDocument(); expect(screen.getByText('۱۴۰۴')).toBeInTheDocument(); expect(screen.getByText('۱۴۰۳')).toBeInTheDocument(); }); it('تب بیمه‌ها قراردادها و درصد پوشش را می‌آورد', async () => { render(); fireEvent.click(await screen.findByText('بیمه‌ها')); expect(await screen.findByText('بیمه ایران')).toBeInTheDocument(); expect(screen.getByText(/پوشش ۷۰٪/)).toBeInTheDocument(); }); it('سرویس ناموجود → پیام خطا و دکمه بازگشت', async () => { mockApi(null, true); render(); expect(await screen.findByText('سرویس یافت نشد')).toBeInTheDocument(); expect(screen.getByText('بازگشت به سرویس‌ها')).toBeInTheDocument(); }); it('دکمه ویرایش، فرم مشترک سرویس را باز می‌کند', async () => { render(); fireEvent.click(await screen.findByText('ویرایش')); expect(await screen.findByText('ویرایش سرویس')).toBeInTheDocument(); // تنظیمات بیمه نباید در فرم باشد expect(screen.queryByText('این خدمت شامل بیمه می‌شود')).not.toBeInTheDocument(); }); it('تب کالاهای مرتبط، اقلام پکیج متصل را نشان می‌دهد', async () => { mockApi({ ...ITEM, inventory_package_uuid: 'pkg1', inventory_package_title: 'پکیج سرم' }); render(); fireEvent.click(await screen.findByText('کالاهای مرتبط')); expect(await screen.findByText('پکیج سرم')).toBeInTheDocument(); expect(screen.getByText('سرم رینگر')).toBeInTheDocument(); expect(screen.getByText('۲ عدد')).toBeInTheDocument(); }); it('سرویس بدون هیچ کالایی، حالت خالی با لینک انبار می‌دهد', async () => { render(); fireEvent.click(await screen.findByText('کالاهای مرتبط')); expect(await screen.findByText('کالایی به این سرویس متصل نیست')).toBeInTheDocument(); expect(screen.getByText('مدیریت انبار')).toBeInTheDocument(); }); it('پکیج و کالای تکی هم‌زمان در تب کالاها نمایش داده می‌شوند', async () => { mockApi({ ...ITEM, inventory_package_uuid: 'pkg1', inventory_package_title: 'پکیج سرم', consumables: [{ item_uuid: 'inv1', name: 'گاز استریل', unit: 'عدد', price: 50_000, stock: 100, amount: 3 }], }); render(); fireEvent.click(await screen.findByText('کالاهای مرتبط')); expect(await screen.findByText('پکیج')).toBeInTheDocument(); expect(screen.getByText('پکیج سرم')).toBeInTheDocument(); expect(screen.getByText('کالاهای تکی')).toBeInTheDocument(); expect(screen.getByText('گاز استریل')).toBeInTheDocument(); expect(screen.getByText('۳ عدد')).toBeInTheDocument(); }); it('کالای تکی با موجودی ناکافی نشان‌دار می‌شود', async () => { mockApi({ ...ITEM, consumables: [{ item_uuid: 'inv1', name: 'گاز استریل', unit: 'عدد', price: 50_000, stock: 1, amount: 5 }], }); render(); fireEvent.click(await screen.findByText('کالاهای مرتبط')); expect(await screen.findByText('موجودی ناکافی')).toBeInTheDocument(); }); it('تب لاگ تغییرات، مقدار قبل و بعد را خوانا نشان می‌دهد', async () => { render(); fireEvent.click(await screen.findByText('لاگ تغییرات')); expect(await screen.findByText('قیمت پایه')).toBeInTheDocument(); expect(screen.getByText('۷۰۰٬۰۰۰ تومان')).toBeInTheDocument(); expect(screen.getByText('۸۵۰٬۰۰۰ تومان')).toBeInTheDocument(); expect(screen.getByText('ایجاد')).toBeInTheDocument(); expect(screen.getAllByText(/توسط دکتر تست/).length).toBeGreaterThan(0); }); it('سرویس غیرفعال نشان «غیرفعال» و دکمه فعال‌کردن دارد', async () => { mockApi({ ...ITEM, active: false }); render(); expect(await screen.findByText('غیرفعال')).toBeInTheDocument(); expect(screen.getByText('فعال‌کردن')).toBeInTheDocument(); }); });