import { describe, it, expect, beforeEach, vi } from 'vitest'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; 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 ResourceBookingModal from './ResourceBookingModal'; const get = api.get as ReturnType; const post = api.post as ReturnType; const resource = { uuid: 'r-laser', name: 'لیزر CO2', address_uuid: 'addr-1', supervisor: { uuid: 'd-1', name: 'دکتر مرادی' }, } as never; /** یکی از این دو وقت مالِ همین منبع است و دیگری مالِ دستگاه دیگری. */ const slots = [ { start: 1783859400, end: 1783861200, assignment: { device: [{ uuid: 'r-laser', name: 'لیزر CO2' }] } }, { start: 1783863000, end: 1783864800, assignment: { device: [{ uuid: 'r-other', name: 'لیزر دیگر' }] } }, ]; beforeEach(() => { get.mockReset(); post.mockReset(); get.mockImplementation((url: string) => { if (url.includes('/services')) { return Promise.resolve({ success: true, data: [ { service_uuid: 's-1', service_name: 'لیزر صورت', active: true, effective_duration_minutes: 30 }, { service_uuid: 's-2', service_name: 'سرویس غیرفعال', active: false, effective_duration_minutes: 20 }, ] }); } if (url.includes('/my/clinic-doctors')) { return Promise.resolve({ success: true, data: { data: [{ uuid: 'd-1', name: 'دکتر مرادی' }] } }); } return Promise.resolve({ success: true, data: [] }); }); post.mockImplementation((url: string) => { if (url.includes('appointment-availability')) { return Promise.resolve({ success: true, data: { plan: { total_minutes: 30, segments: [] }, slots, reason: null } }); } if (url.includes('appointment-hold')) { return Promise.resolve({ success: true, data: { hold_uuid: 'h-1', assignment: {} } }); } return Promise.resolve({ success: true, data: { uuid: 'appt-1' } }); }); }); describe('ResourceBookingModal', () => { /** فقط سرویس‌هایی که خودِ منبع ارائه می‌دهد — سرور هم بقیه را با ۴۲۲ رد می‌کند. */ it('فقط سرویس‌های فعالِ همین منبع را می‌آورد', async () => { renderWithProviders( , ); expect(await screen.findByText('ثبت نوبت — لیزر CO2')).toBeInTheDocument(); const called = get.mock.calls.map((c: any[]) => c[0]); expect(called.some((u: string) => u.includes('/api/v1/resource/r-laser/services'))).toBe(true); }); /** * هستهٔ منبع‌محور بودن: موتور خدمت‌محور جواب می‌دهد، پس وقتی که این منبع در آن * پیشنهاد نشده باید حذف شود — نشان دادنش یعنی اپراتور چیزی بگیرد که ۴۰۹ می‌شود. */ it('فقط وقت‌هایی را نشان می‌دهد که موتور همین منبع را در آن‌ها داده', async () => { const user = userEvent.setup(); renderWithProviders( , ); await screen.findByText('ثبت نوبت — لیزر CO2'); await user.click(await screen.findByText('سرویس را انتخاب کنید')); await user.click(await screen.findByText(/لیزر صورت/)); await waitFor(() => expect(screen.getByText(/زمان‌های خالی این منبع/)).toBeInTheDocument()); // وقتِ منبعِ دیگر (۱۷۸۳۸۶۳۰۰۰) نباید دکمه‌ای داشته باشد. const buttons = screen.getAllByRole('button').map((b) => b.textContent ?? ''); const timeButtons = buttons.filter((t) => /\d{2}:\d{2}/.test(t)); expect(timeButtons).toHaveLength(1); }); /** * پزشک از ناظرِ خودِ منبع می‌آید، نه از یک انتخابگر دیگر — ارتباط پزشک↔منبع یک جا * تعریف شده و تکرارش اینجا یعنی دو منبعِ حقیقت. */ it('پزشک را از ناظرِ منبع می‌گیرد و انتخابگر پزشک ندارد', async () => { const user = userEvent.setup(); renderWithProviders( , ); await screen.findByText('ثبت نوبت — لیزر CO2'); expect(screen.getByText(/پزشک ناظر: دکتر مرادی/)).toBeInTheDocument(); expect(screen.queryByText('پزشک انجام‌دهنده')).toBeNull(); // بدون سرویس و زمان هنوز غیرفعال است. expect(screen.getByRole('button', { name: 'ثبت نوبت' })).toBeDisabled(); await user.click(await screen.findByText('سرویس را انتخاب کنید')); await user.click(await screen.findByText(/لیزر صورت/)); await waitFor(() => expect(screen.getByText(/زمان‌های خالی این منبع/)).toBeInTheDocument()); const timeBtn = screen.getAllByRole('button').find((b) => /\d{2}:\d{2}/.test(b.textContent ?? '')); await user.click(timeBtn!); // سرویس + زمان + ناظر ⇒ حالا قابل ثبت است. await waitFor(() => expect(screen.getByRole('button', { name: 'ثبت نوبت' })).toBeEnabled()); }); /** منبعی که ناظرش حذف شده نباید بی‌صدا غیرقابل‌ثبت بماند. */ it('منبع بدون ناظر را با پیام روشن مسدود می‌کند', async () => { const orphan = { uuid: 'r-x', name: 'لیزر یتیم', address_uuid: 'addr-1', supervisor: null } as never; renderWithProviders( , ); expect(await screen.findByText(/این منبع پزشک ناظر ندارد/)).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'ثبت نوبت' })).toBeDisabled(); }); });