import { describe, it, expect, beforeEach, vi } from 'vitest'; import { screen, fireEvent, waitFor } from '@testing-library/react'; 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 { toast } from 'sonner'; import PatientTreatmentTab from './PatientTreatmentTab'; const get = api.get as ReturnType; const patch = api.patch as ReturnType; const CASE_UUID = 'case-1'; const summary = (over: Record = {}) => ({ uuid: CASE_UUID, status: 'active', total_sessions: 5, completed_sessions: 3, service: { uuid: 'svc-1', name: 'لیزر ناحیه ۱' }, ...over, }); /** فهرست دوره‌ها، تقویم دوره، و بقیهٔ GETها. */ function mockApi(caseOver: Record = {}) { get.mockImplementation((url: string) => { if (url.startsWith('/api/v1/treatment-cases')) { return Promise.resolve({ success: true, data: [summary(caseOver)] }); } if (url.includes('/plan')) { return Promise.resolve({ success: true, data: { case: { ...summary(caseOver), patient_national_code: null }, resource: null, sessions: [], } }); } return Promise.resolve({ success: true, data: [] }); }); } beforeEach(() => { get.mockReset(); patch.mockReset(); patch.mockResolvedValue({ success: true, data: {} }); mockApi(); }); /** دورهٔ انتخاب‌شده در URL می‌نشیند؛ تست هم از همان مسیر شروع می‌کند. */ const render = () => renderWithProviders( , { route: `/admin/patients/p-1?case=${CASE_UUID}` }, ); const plusBtn = () => screen.getByRole('button', { name: 'افزودن یک جلسه' }); const minusBtn = () => screen.getByRole('button', { name: 'کم کردن یک جلسه' }); /** * تعداد جلسات سرِ پروندهٔ بیمار تصمیم گرفته می‌شود، ولی تا این تغییر فقط از صفحهٔ * «دوره‌های درمان» و پشت یک مودال قابل تغییر بود. */ describe('PatientTreatmentTab — تعداد جلسات', () => { it('یک جلسه اضافه می‌کند', async () => { render(); fireEvent.click(await screen.findByRole('button', { name: 'افزودن یک جلسه' })); await waitFor(() => expect(patch).toHaveBeenCalledWith( `/api/v1/treatment-case/${CASE_UUID}`, { total_sessions: 6 }, )); }); it('یک جلسه کم می‌کند', async () => { render(); fireEvent.click(await screen.findByRole('button', { name: 'کم کردن یک جلسه' })); await waitFor(() => expect(patch).toHaveBeenCalledWith( `/api/v1/treatment-case/${CASE_UUID}`, { total_sessions: 4 }, )); }); it('وقتی همهٔ جلسه‌ها انجام شده، کم کردن قفل است', async () => { mockApi({ total_sessions: 4, completed_sessions: 4 }); render(); await waitFor(() => expect(minusBtn()).toBeDisabled()); expect(screen.getByText(/همهٔ جلسه‌ها انجام شده/)).toBeInTheDocument(); // افزودن همچنان آزاد است. expect(plusBtn()).not.toBeDisabled(); }); it('کف دو جلسه رعایت می‌شود', async () => { mockApi({ total_sessions: 2, completed_sessions: 0 }); render(); await waitFor(() => expect(minusBtn()).toBeDisabled()); expect(screen.getByText(/کمتر از ۲ جلسه ممکن نیست/)).toBeInTheDocument(); }); it('سقف شصت جلسه رعایت می‌شود', async () => { mockApi({ total_sessions: 60, completed_sessions: 0 }); render(); await waitFor(() => expect(plusBtn()).toBeDisabled()); expect(minusBtn()).not.toBeDisabled(); }); it('خطای سرور را همان‌طور که آمده نشان می‌دهد', async () => { // سرور دقیق‌تر از هر متن ثابتی می‌گوید چند جلسه قفل است. patch.mockRejectedValue(new Error('۳ جلسه انجام شده یا نوبت دارد؛ تعداد کمتر از آن ممکن نیست')); render(); fireEvent.click(await screen.findByRole('button', { name: 'کم کردن یک جلسه' })); await waitFor(() => expect(toast.error).toHaveBeenCalledWith( '۳ جلسه انجام شده یا نوبت دارد؛ تعداد کمتر از آن ممکن نیست', )); }); it('پیشرفت دوره با نسبت واقعی اعلام می‌شود', async () => { render(); const bar = await screen.findByRole('progressbar', { name: /پیشرفت دوره/ }); expect(bar).toHaveAttribute('aria-valuenow', '3'); expect(bar).toHaveAttribute('aria-valuemax', '5'); }); it('دکمهٔ ویرایش دوره برای بقیهٔ فیلدها هست', async () => { render(); expect(await screen.findByRole('button', { name: /ویرایش دوره/ })).toBeInTheDocument(); }); });