admin/features/events/edit/EventEditForm.test.tsx
alisaza 82593dc18c refactor(events): update event management components and remove unused files
- Replaced `LIST_PUBLIC_CATEGORIES` with `LIST_ADMIN_CATEGORIES_FLAT` in `ArticleFormModal.tsx`.
- Removed the "Add Event" button from the dashboard page.
- Simplified the `EventsPage` by removing the button and adjusting the layout.
- Cleaned up the `AdminEventEditPage` by removing unnecessary props.
- Deleted unused layout and page files related to event creation.
- Updated `AdminAuthContent` to use `useAdminCitiesQuery` instead of `useDiscoveryCitiesQuery`.
- Refactored `EventGuestListAccessPanel` to remove the `accessMode` prop and adjust API calls accordingly.
- Removed several unused components and tests related to event creation, enhancing project maintainability.
2026-09-07 18:09:55 +03:30

302 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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('@/services/events', () => ({
fetchEventForEdit: (...args: unknown[]) => fetchEventForEdit(...args),
fetchEventCategories: (...args: unknown[]) => fetchEventCategories(...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: () => <div data-testid="media-uploader" />,
}))
vi.mock('@/components/events/create/FaqEditor', () => ({
default: () => <div data-testid="faq-editor" />,
}))
vi.mock('@/components/events/create/CategoryTreeSelect', () => ({
default: function MockCategoryTreeSelect({ name, label }: { name: string; label: string }) {
const { register } = useFormContext()
return (
<label>
{label}
<input
aria-label={label}
{...register(name)}
/>
</label>
)
},
}))
vi.mock('@/components/events/create/MapLocationPicker', () => ({
default: () => <div data-testid="map-picker" />,
}))
vi.mock('@/components/forms/UnsavedChangesIndicator', () => ({ default: () => null }))
vi.mock('@/components/forms/AdminFormLayout', () => ({
AdminFormSection: ({ title, children }: { title: string; children: React.ReactNode }) => (
<section>
<h2>{title}</h2>
{children}
</section>
),
AdminFormActions: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))
vi.mock('@/components/formElements/Button', () => ({
default: ({ children, isLoading, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement> & { isLoading?: boolean }) => (
<button
{...props}
disabled={props.disabled || isLoading}
>
{children}
</button>
),
}))
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 (
<label>
{label}
<input
aria-label={label}
disabled={disabled}
type="checkbox"
{...register(name)}
/>
</label>
)
}
return (
<label>
{label}
<input
aria-label={label}
disabled={disabled}
type={inputType}
{...register(name)}
/>
</label>
)
},
}))
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([])
updateEventAsAdmin.mockImplementation(async (_id: string, payload: Record<string, unknown>) => ({
...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(
<EventEditForm
cancelHref="/my-events"
eventId="event-1"
successHref="/my-events"
/>
)
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(
<EventEditForm
cancelHref="/my-events"
eventId="event-1"
successHref="/my-events"
/>
)
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(
<EventEditForm
cancelHref="/my-events"
eventId="event-1"
successHref="/my-events"
/>
)
const title = await screen.findByLabelText('عنوان')
fireEvent.change(title, { target: { value: 'عنوان جدید' } })
fireEvent.click(screen.getByRole('button', { name: 'ذخیره تغییرات' }))
await waitFor(() => {
expect(updateEventAsAdmin).toHaveBeenCalledOnce()
})
const payload = updateEventAsAdmin.mock.calls[0][1] as Record<string, unknown>
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(push).toHaveBeenCalledWith('/my-events')
})
it('blocks saving when capacity drops below active bookings', async () => {
fetchEventForEdit.mockResolvedValue(baseEvent)
render(
<EventEditForm
cancelHref="/my-events"
eventId="event-1"
successHref="/my-events"
/>
)
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(updateEventAsAdmin).not.toHaveBeenCalled()
})
})