import { describe, it, expect, vi, beforeEach } from 'vitest'; import { screen, 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 {}, })); vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } })); vi.mock('react-router-dom', async () => ({ ...(await vi.importActual('react-router-dom')), useParams: () => ({ courseUuid: 'c-1' }), })); import { api } from '../lib/api'; import TreatmentCoursePage from './TreatmentCoursePage'; const get = api.get as ReturnType; const DAY = 86400; const now = () => Math.floor(Date.now() / 1000); function course(over: Record = {}) { return { uuid: 'c-1', service_name: 'لیزر', status: 'active', min_days: 20, ideal_days: 28, max_days: 40, abandon_reason: null, preferred_resource_uuid: null, preferred_resource_name: null, progress: { completed: 1, booked: 0, planned: 1, total: 2 }, sessions: [ { session_number: 1, status: 'completed', slot_start: now() - 60 * DAY, completed_at: now() - 60 * DAY, params: {} }, { session_number: 2, status: 'planned', slot_start: null, completed_at: null, params: {} }, ], ...over, }; } function mockCourse(payload: Record) { get.mockImplementation((url: string) => { if (url.includes('next-slot-suggestion')) return Promise.resolve({ data: null }); if (url.includes('/branch')) return Promise.resolve({ data: [] }); return Promise.resolve({ data: payload }); }); } describe('TreatmentCoursePage', () => { beforeEach(() => vi.clearAllMocks()); /** ⭐ دوره‌ای که وسطش لغو شده بی‌صدا کِش می‌آید؛ هشدار نباید به انتخاب شعبه وابسته باشد. */ it('warns when the course has run past the protocol maximum', async () => { mockCourse(course()); renderWithProviders(); expect(await screen.findByText(/روز از آخرین جلسه گذشته/)).toBeInTheDocument(); }); it('stays quiet while the course is still inside its window', async () => { mockCourse(course({ sessions: [ { session_number: 1, status: 'completed', slot_start: now() - 5 * DAY, completed_at: now() - 5 * DAY, params: {} }, { session_number: 2, status: 'planned', slot_start: null, completed_at: null, params: {} }, ], })); renderWithProviders(); await waitFor(() => expect(get).toHaveBeenCalled()); expect(screen.queryByText(/روز از آخرین جلسه گذشته/)).toBeNull(); }); /** فاصلهٔ واقعی، نه فاصلهٔ پروتکل — تفاوتشان همان چیزی است که کِش‌آمدن را نشان می‌دهد. */ it('shows the real gap between two dated sessions', async () => { mockCourse(course({ sessions: [ { session_number: 1, status: 'completed', slot_start: now() - 40 * DAY, completed_at: now() - 40 * DAY, params: {} }, { session_number: 2, status: 'completed', slot_start: now() - 10 * DAY, completed_at: now() - 10 * DAY, params: {} }, ], progress: { completed: 2, booked: 0, planned: 0, total: 2 }, })); renderWithProviders(); expect(await screen.findByText('۳۰ روز')).toBeInTheDocument(); }); });