import { describe, it, expect, beforeEach, vi } from 'vitest'; import { screen, fireEvent, 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 {}, })); import { api } from '../lib/api'; import AppointmentActionsMenu from './AppointmentActions'; import type { Appointment } from '../types'; const get = api.get as ReturnType; const patch = api.patch as ReturnType; const post = api.post as ReturnType; const navigate = vi.fn(); vi.mock('react-router-dom', async () => ({ ...(await vi.importActual('react-router-dom')), useNavigate: () => navigate, })); const appt: Appointment = { uuid: 'ap1', patient_name: 'مریم خلیلی', patient_mobile: '09136549874', doctor_uuid: 'd1', doctor_name: 'دکتر احمدی', slot_start: 1735639200, slot_end: 1735641900, // 45 min appointment_date: '2024-12-31', appointment_time: '09:00', end_time: '09:45', status: 'confirmed', version: 3, created_at: '', service_section: { uuid: 's1', name: 'زیبایی' }, service_item: { uuid: 'i1', name: 'لیزر توتال' }, staff: { uuid: 'st1', full_name: 'دکتر حمیدی' }, }; beforeEach(() => { get.mockReset(); patch.mockReset(); get.mockImplementation((url: string) => { if (url.startsWith('/api/v1/patients?search=')) return Promise.resolve({ success: true, data: [{ uuid: 'rec1' }] }); if (url === '/api/v1/patient/rec1/wallet') return Promise.resolve({ success: true, data: { balance_rials: 500000, recent_transactions: [] } }); return Promise.resolve({ success: true, data: [] }); }); patch.mockResolvedValue({ success: true, data: {} }); post.mockReset(); navigate.mockReset(); }); function openMenu() { renderWithProviders(); fireEvent.click(screen.getByRole('button', { name: 'عملیات' })); } describe('AppointmentActionsMenu (عملیات نوبت)', () => { it('lists all six actions from the Figma menu', () => { openMenu(); for (const label of ['ویرایش', 'ثبت سرویس', 'مشاهده', 'جا به جایی نوبت', 'انتقال به لیست رزرو', 'جایگزینی نوبت']) { expect(screen.getByText(label)).toBeInTheDocument(); } }); it('«ثبت سرویس» پروندهٔ موجود بیمار را باز می‌کند', async () => { openMenu(); fireEvent.click(screen.getByText('ثبت سرویس')); await waitFor(() => expect(navigate).toHaveBeenCalledWith('/admin/patients/rec1/session/new')); // مسیر جستجو باید همان endpoint واقعی باشد، وگرنه ۴۰۴ می‌گیرد و «خطا در یافتن پرونده» می‌دهد. expect(get).toHaveBeenCalledWith(expect.stringContaining('/api/v1/patients?search=')); expect(post).not.toHaveBeenCalled(); }); it('«ثبت سرویس» برای بیمارِ بدون پرونده، اول پرونده می‌سازد', async () => { get.mockImplementation((url: string) => { if (url.startsWith('/api/v1/patients?search=')) return Promise.resolve({ success: true, data: [] }); return Promise.resolve({ success: true, data: [] }); }); post.mockResolvedValue({ success: true, data: { uuid: 'rec-new' } }); openMenu(); fireEvent.click(screen.getByText('ثبت سرویس')); await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/patient', expect.objectContaining({ mobile: '09136549874', name: 'مریم خلیلی', }))); expect(navigate).toHaveBeenCalledWith('/admin/patients/rec-new/session/new'); }); it('info modal shows appointment details and patient wallet balance', async () => { openMenu(); fireEvent.click(screen.getByText('مشاهده')); expect(await screen.findByText('ساعت شروع:')).toBeInTheDocument(); expect(screen.getByText('۴۵ دقیقه')).toBeInTheDocument(); expect(screen.getByText('لیزر توتال')).toBeInTheDocument(); expect(screen.getByText('دکتر حمیدی')).toBeInTheDocument(); // wallet resolved through record search → balance shown (rial → toman) expect(await screen.findByText(/تومان/)).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'مشاهده پرونده' })).toBeInTheDocument(); }); it('move modal patches new slot times', async () => { openMenu(); fireEvent.click(screen.getByText('جا به جایی نوبت')); expect(await screen.findByText('اعمال تغییرات')).toBeInTheDocument(); fireEvent.change(screen.getByLabelText('ساعت شروع'), { target: { value: '15:00' } }); fireEvent.change(screen.getByLabelText('ساعت پایان'), { target: { value: '16:00' } }); fireEvent.click(screen.getByText('اعمال تغییرات')); await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/appointment/ap1', expect.objectContaining({ slot_start: Math.floor(new Date('2024-12-31T15:00').getTime() / 1000), slot_end: Math.floor(new Date('2024-12-31T16:00').getTime() / 1000), version: 3, }))); }); it('transfer modal flips is_reserve with a day-level slot', async () => { openMenu(); fireEvent.click(screen.getByText('انتقال به لیست رزرو')); expect(await screen.findByText(/به لیست نوبت های رزرو شده منتقل می شود/)).toBeInTheDocument(); fireEvent.click(screen.getByText('انتقال و حذف از لیست')); const day = Math.floor(new Date('2024-12-31T00:00').getTime() / 1000); await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/appointment/ap1', expect.objectContaining({ is_reserve: true, slot_start: day, slot_end: day, version: 3, }))); }); it('replace modal swaps the patient and keeps the slot locked', async () => { openMenu(); fireEvent.click(screen.getByText('جایگزینی نوبت')); expect(await screen.findByPlaceholderText('نام و نام خانوادگی')).toBeInTheDocument(); // the original slot is shown read-only expect(screen.getByDisplayValue('2024-12-31')).toBeDisabled(); expect(screen.getByDisplayValue('09:00')).toBeDisabled(); // prefilled from the appointment's current specs (react-select single value) expect(screen.getByText('قطعی شده')).toBeInTheDocument(); fireEvent.change(screen.getByPlaceholderText('نام و نام خانوادگی'), { target: { value: 'ساغر صابری' } }); fireEvent.change(screen.getByPlaceholderText('شماره تماس'), { target: { value: '09356619438' } }); fireEvent.click(screen.getByText('ثبت نوبت')); await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/appointment/ap1', expect.objectContaining({ patient_name: 'ساغر صابری', patient_mobile: '09356619438', service_section_uuid: 's1', service_item_uuid: 'i1', staff_uuid: 'st1', version: 3, }))); }); it('replace modal picks an existing patient from the record search', async () => { get.mockImplementation((url: string) => { if (url.startsWith('/api/v1/patients?search=')) return Promise.resolve({ success: true, data: [ { uuid: 'rec9', user_name: 'پریسا همتی', user_mobile: '09120009999' }, ] }); return Promise.resolve({ success: true, data: [] }); }); openMenu(); fireEvent.click(screen.getByText('جایگزینی نوبت')); fireEvent.change(await screen.findByPlaceholderText('جستجوی نام، شماره تماس، شماره پرونده...'), { target: { value: 'پریسا' } }); fireEvent.click(await screen.findByText('پریسا همتی')); fireEvent.click(screen.getByText('ثبت نوبت')); await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/appointment/ap1', expect.objectContaining({ patient_name: 'پریسا همتی', patient_mobile: '09120009999', }))); }); });