import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useFormContext } from 'react-hook-form' import EventEditForm from '@/features/events/edit/EventEditForm' const push = vi.fn() const addToast = vi.fn() const fetchEventForEdit = vi.fn() const fetchEventCategories = vi.fn() const fetchAllCities = vi.fn() const fetchEventMedia = vi.fn() const fetchEventFaqs = vi.fn() const updateEvent = vi.fn() const updateEventAsAdmin = vi.fn() vi.mock('next/navigation', () => ({ useRouter: () => ({ push }) })) vi.mock('next/dynamic', () => ({ default: () => () => null, })) vi.mock('@/lib/toast', () => ({ addToast: (...args: unknown[]) => addToast(...args) })) vi.mock('@/hooks/useAuth', () => ({ default: () => ({ user: { userId: 'user-id' } }) })) vi.mock('@/services/events', () => ({ fetchEventForEdit: (...args: unknown[]) => fetchEventForEdit(...args), fetchEventCategories: (...args: unknown[]) => fetchEventCategories(...args), updateEvent: (...args: unknown[]) => updateEvent(...args), updateEventAsAdmin: (...args: unknown[]) => updateEventAsAdmin(...args), })) vi.mock('@/services/eventDetail', () => ({ fetchEventMedia: (...args: unknown[]) => fetchEventMedia(...args), fetchEventFaqs: (...args: unknown[]) => fetchEventFaqs(...args), })) vi.mock('@/features/events/edit/syncEventExtras', () => ({ syncEventMedia: vi.fn().mockResolvedValue(undefined), syncEventFaqs: vi.fn().mockResolvedValue(undefined), })) vi.mock('@/services/geography', () => ({ fetchAllCities: (...args: unknown[]) => fetchAllCities(...args), })) vi.mock('@/components/events/create/EventMediaGalleryUploader', () => ({ default: () =>
, })) vi.mock('@/components/events/create/FaqEditor', () => ({ default: () =>
, })) vi.mock('@/components/events/create/CategoryTreeSelect', () => ({ default: function MockCategoryTreeSelect({ name, label }: { name: string; label: string }) { const { register } = useFormContext() return ( ) }, })) vi.mock('@/components/events/create/MapLocationPicker', () => ({ default: () =>
, })) vi.mock('@/components/forms/UnsavedChangesIndicator', () => ({ default: () => null })) vi.mock('@/components/forms/ConsumerFormLayout', () => ({ ConsumerFormSection: ({ title, children }: { title: string; children: React.ReactNode }) => (

{title}

{children}
), ConsumerFormActions: ({ children }: { children: React.ReactNode }) =>
{children}
, })) vi.mock('@/components/forms/AdminFormLayout', () => ({ AdminFormSection: ({ children }: { children: React.ReactNode }) =>
{children}
, AdminFormActions: ({ children }: { children: React.ReactNode }) =>
{children}
, })) vi.mock('@/components/formElements/Button', () => ({ default: ({ children, isLoading, ...props }: React.ButtonHTMLAttributes & { isLoading?: boolean }) => ( ), })) vi.mock('@/components/formElements/Input', () => ({ default: function MockInput({ name, label, disabled, generalType, }: { name: string label: string disabled?: boolean generalType?: string }) { const { register } = useFormContext() const inputType = generalType === 'numberInput' || generalType === 'timePicker' ? 'number' : 'text' if (generalType === 'switch') { return ( ) } return ( ) }, })) const baseEvent = { id: 'event-1', title: 'رویداد تست', slug: 'event-test', status: 'published', organizerId: 'user-id', categoryId: 10, shortDescription: 'کوتاه', description: 'توضیح', startsAt: '2030-01-01T10:00:00.000Z', endsAt: '2030-01-01T12:00:00.000Z', cityId: 2, provinceId: 1, address: 'تهران', lat: 35.6892, lng: 51.389, isFree: false, price: 100_000, capacity: 20, reservedCapacity: 0, genderRestriction: 'open' as const, ageRestriction: 'open' as const, bookedCount: 3, cancellationFeePercent: 10, cancellationFeePercent12To24Hours: 10, cancellationFeePercentMoreThan24Hours: 10, settings: { isDiscoverable: true, autoCreateGroup: true, addressVisibility: 'public' as const, generalArea: null, waitlistAutoOffer: true, sendReviewRequestSms: false, }, } const requiredMedia = [ { id: 'm1', eventId: 'event-1', mediaType: 'image' as const, url: 'https://cdn.test/portrait.jpg', sortOrder: 0, isPoster: true, isSquarePoster: false, }, { id: 'm2', eventId: 'event-1', mediaType: 'image' as const, url: 'https://cdn.test/square.jpg', sortOrder: 1, isPoster: false, isSquarePoster: true, }, ] describe('EventEditForm', () => { afterEach(cleanup) beforeEach(() => { vi.clearAllMocks() fetchEventCategories.mockResolvedValue([{ id: 10, name: 'آموزشی', parentId: null, slug: 'edu', sortOrder: 1 }]) fetchAllCities.mockResolvedValue([{ id: 2, name: 'تهران', provinceId: 1 }]) fetchEventMedia.mockResolvedValue(requiredMedia) fetchEventFaqs.mockResolvedValue([]) updateEvent.mockImplementation(async (_id: string, payload: Record) => ({ ...baseEvent, ...payload, title: typeof payload.title === 'string' ? payload.title : baseEvent.title, })) }) it('shows the booking lock banner when bookings exist and never explains that all fields are editable', async () => { fetchEventForEdit.mockResolvedValue(baseEvent) render( ) expect(await screen.findByText(/3 رزرو فعال دارد/)).toBeInTheDocument() expect(screen.queryByText(/همه فیلدها قابل ویرایش/)).not.toBeInTheDocument() expect(screen.getByLabelText('تاریخ شروع')).toBeDisabled() expect(screen.getByLabelText('آدرس')).toBeDisabled() expect(screen.getByLabelText('قیمت (تومان)')).toBeDisabled() expect(screen.getByLabelText('لغو زودهنگام — بیش از ۲۴ ساعت مانده')).toBeDisabled() expect(screen.getByLabelText('لغو در آستانه برگزاری — ۱۲ تا ۲۴ ساعت مانده')).toBeDisabled() expect(screen.getByLabelText('لغو لحظه آخری — ۱ دقیقه تا ۱۲ ساعت مانده')).toBeDisabled() expect(screen.getByLabelText('محدودیت جنسی')).toBeDisabled() expect(screen.getByLabelText('محدودیت سنی')).toBeDisabled() expect(screen.getByLabelText('عنوان')).not.toBeDisabled() expect(screen.getByLabelText('اسلاگ')).not.toBeDisabled() }) it('does not show a lock banner when published without bookings', async () => { fetchEventForEdit.mockResolvedValue({ ...baseEvent, bookedCount: 0 }) render( ) expect(await screen.findByLabelText('عنوان')).toBeInTheDocument() expect(screen.queryByText(/رزرو فعال دارد/)).not.toBeInTheDocument() expect(screen.queryByText(/همه فیلدها قابل ویرایش/)).not.toBeInTheDocument() expect(screen.getByLabelText('تاریخ شروع')).not.toBeDisabled() expect(screen.getByLabelText('آدرس')).not.toBeDisabled() }) it('omits locked fields from the save payload', async () => { fetchEventForEdit.mockResolvedValue(baseEvent) render( ) const title = await screen.findByLabelText('عنوان') fireEvent.change(title, { target: { value: 'عنوان جدید' } }) fireEvent.click(screen.getByRole('button', { name: 'ذخیره تغییرات' })) await waitFor(() => { expect(updateEvent).toHaveBeenCalledOnce() }) const payload = updateEvent.mock.calls[0][1] as Record expect(payload.title).toBe('عنوان جدید') expect(payload).toHaveProperty('slug') expect(payload).not.toHaveProperty('startsAt') expect(payload).not.toHaveProperty('endsAt') expect(payload).not.toHaveProperty('address') expect(payload).not.toHaveProperty('price') expect(payload).not.toHaveProperty('isFree') expect(payload).not.toHaveProperty('cancellationFeePercent') expect(payload).not.toHaveProperty('cancellationFeePercent12To24Hours') expect(payload).not.toHaveProperty('cancellationFeePercentMoreThan24Hours') expect(payload).not.toHaveProperty('genderRestriction') expect(payload).not.toHaveProperty('ageRestriction') expect(updateEventAsAdmin).not.toHaveBeenCalled() expect(push).toHaveBeenCalledWith('/my-events') }) it('blocks saving when capacity drops below active bookings', async () => { fetchEventForEdit.mockResolvedValue(baseEvent) render( ) const capacity = await screen.findByLabelText('ظرفیت') fireEvent.change(capacity, { target: { value: '2' } }) fireEvent.click(screen.getByRole('button', { name: 'ذخیره تغییرات' })) await waitFor(() => { expect(addToast).toHaveBeenCalledWith( expect.objectContaining({ title: expect.stringContaining('ظرفیت نمی‌تواند کمتر از صندلی‌های اشغال‌شده'), color: 'danger', }) ) }) expect(updateEvent).not.toHaveBeenCalled() }) })