admin/features/events/detail/EventDiscountsPanel.test.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

170 lines
5.4 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 EventDiscountsPanel from '@/features/events/detail/EventDiscountsPanel'
const listCodes = vi.fn()
const listRedemptions = vi.fn()
const getReport = vi.fn()
const getBootstrap = vi.fn()
const bulkCreate = vi.fn()
const setActive = vi.fn()
const removeCode = vi.fn()
const addToast = vi.fn()
vi.mock('@/lib/toast', () => ({ addToast: (...args: unknown[]) => addToast(...args) }))
vi.mock('@/services/discountCodes', () => ({
LIST_DISCOUNT_CODES: (...args: unknown[]) => listCodes(...args),
LIST_DISCOUNT_REDEMPTIONS: (...args: unknown[]) => listRedemptions(...args),
GET_DISCOUNT_REPORT: (...args: unknown[]) => getReport(...args),
GET_DISCOUNT_MANAGEMENT_BOOTSTRAP: (...args: unknown[]) => getBootstrap(...args),
BULK_CREATE_DISCOUNT_CODES: (...args: unknown[]) => bulkCreate(...args),
SET_DISCOUNT_CODE_ACTIVE: (...args: unknown[]) => setActive(...args),
DELETE_DISCOUNT_CODE: (...args: unknown[]) => removeCode(...args),
}))
vi.mock('@/components/formElements/Button', () => ({
default: ({
children,
isLoading,
fullWidth: _fullWidth,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { isLoading?: boolean; fullWidth?: boolean }) => (
<button
{...props}
disabled={props.disabled || isLoading}
>
{children}
</button>
),
}))
const usedCode = {
id: 'code-used',
eventId: 'event-1',
code: 'WELCOME20',
type: 'percent' as const,
value: 20,
bearer: 'organizer' as const,
maxUses: null,
maxUsesPerUser: null,
validFrom: null,
validUntil: null,
generationBatchId: 'batch-1',
isActive: true,
redeemedCount: 2,
totalDiscountAmount: 140000,
createdAt: '2030-01-01T09:00:00.000Z',
}
const freshCode = {
...usedCode,
id: 'code-fresh',
code: 'SAVE50K',
type: 'fixed' as const,
value: 50000,
redeemedCount: 0,
totalDiscountAmount: 0,
}
describe('EventDiscountsPanel', () => {
afterEach(cleanup)
beforeEach(() => {
vi.clearAllMocks()
listCodes.mockResolvedValue({ ok: true, data: { items: [usedCode, freshCode], totalItemsCount: 2, totalPages: 1 } })
listRedemptions.mockResolvedValue({ ok: true, data: { items: [], totalItemsCount: 0, totalPages: 0 } })
getReport.mockResolvedValue({
ok: true,
data: { totalCodes: 2, totalRedemptions: 2, totalDiscountAmount: 140000, totalPlatformAbsorbed: 0, totalOrganizerAbsorbed: 140000 },
})
getBootstrap.mockResolvedValue({
ok: true,
data: {
codes: { items: [usedCode, freshCode], totalItemsCount: 2, totalPages: 1 },
redemptions: { items: [], totalItemsCount: 0, totalPages: 0 },
report: {
totalCodes: 2,
totalRedemptions: 2,
totalDiscountAmount: 140000,
totalPlatformAbsorbed: 0,
totalOrganizerAbsorbed: 140000,
},
},
})
bulkCreate.mockResolvedValue({ ok: true, data: { batchId: 'b', count: 2, codes: ['AAAA1111', 'BBBB2222'], items: [] } })
removeCode.mockResolvedValue({ ok: true, data: undefined })
})
it('renders a message and skips loading for free events', () => {
render(
<EventDiscountsPanel
isFree
eventId="event-1"
/>
)
expect(screen.getByText('این ایونت رایگان است')).toBeInTheDocument()
expect(getBootstrap).not.toHaveBeenCalled()
expect(listCodes).not.toHaveBeenCalled()
})
it('lists codes and disables delete for a used code', async () => {
render(
<EventDiscountsPanel
eventId="event-1"
isFree={false}
/>
)
expect(await screen.findByText('WELCOME20')).toBeInTheDocument()
expect(screen.getByText('SAVE50K')).toBeInTheDocument()
const deleteButtons = screen.getAllByRole('button', { name: 'حذف' })
// First card is the used code — its delete must be disabled.
expect(deleteButtons[0]).toBeDisabled()
// Second card is the unused code — deletable.
expect(deleteButtons[1]).toBeEnabled()
})
it('bulk-creates codes and reveals the generated list', async () => {
render(
<EventDiscountsPanel
eventId="event-1"
isFree={false}
/>
)
await screen.findByText('WELCOME20')
fireEvent.change(screen.getByLabelText('درصد تخفیف (۱ تا ۹۹)'), { target: { value: '25' } })
fireEvent.change(screen.getByLabelText('تعداد کد یکتا'), { target: { value: '2' } })
fireEvent.click(screen.getByRole('button', { name: 'ساخت کد تخفیف' }))
await waitFor(() => {
expect(bulkCreate).toHaveBeenCalledWith('event-1', expect.objectContaining({ type: 'percent', value: 25, quantity: 2 }))
})
expect(await screen.findByText('AAAA1111')).toBeInTheDocument()
expect(screen.getByText('BBBB2222')).toBeInTheDocument()
})
it('rejects an out-of-range percent value before calling the API', async () => {
render(
<EventDiscountsPanel
eventId="event-1"
isFree={false}
/>
)
await screen.findByText('WELCOME20')
fireEvent.change(screen.getByLabelText('درصد تخفیف (۱ تا ۹۹)'), { target: { value: '150' } })
fireEvent.click(screen.getByRole('button', { name: 'ساخت کد تخفیف' }))
await waitFor(() => {
expect(addToast).toHaveBeenCalledWith(expect.objectContaining({ color: 'warning' }))
})
expect(bulkCreate).not.toHaveBeenCalled()
})
})