Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
170 lines
5.4 KiB
TypeScript
170 lines
5.4 KiB
TypeScript
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()
|
||
})
|
||
})
|