admin/app/(dashboard)/notifications/_components/NotificationRulesPanel.test.tsx
alisaza 96603d31f7 feat(alert-modal): integrate alert modal for confirmation actions
Enhanced various components to utilize the alert modal for user confirmations before executing critical actions. This includes marking messages as read, saving notification rules, restoring reviews, sending replies, changing ticket statuses, and managing event commissions. The integration improves user experience by ensuring actions are intentional and provides clear feedback on the outcomes.
2026-09-13 10:14:18 +03:30

167 lines
5.6 KiB
TypeScript

import type { ReactNode } from 'react'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import NotificationRulesPanel from '@/app/(dashboard)/notifications/_components/NotificationRulesPanel'
const axiosMocks = vi.hoisted(() => ({
get: vi.fn(),
patch: vi.fn(),
}))
vi.mock('@/config/axios', () => ({
default: {
get: axiosMocks.get,
patch: axiosMocks.patch,
},
}))
const showAlert = vi.fn((_message: string, onConfirm?: () => unknown) => {
void onConfirm?.()
})
vi.mock('@/lib/toast', () => ({ addToast: vi.fn() }))
vi.mock('@/hooks/useAlertModal', () => ({ default: () => ({ showAlert }) }))
vi.mock('@/components/formElements/Input', () => ({
default: ({
description,
generalType,
label,
selectOptions,
value,
onValueChange,
}: {
description?: ReactNode
generalType?: string
label: string
selectOptions?: { code: string; name: string }[]
value?: unknown
onValueChange?: (value: unknown) => void
}) => (
<label>
{label}
{generalType === 'select' ? (
<select
aria-label={label}
value={String(value ?? '')}
onChange={(event) => onValueChange?.(event.target.value)}
>
{(selectOptions ?? []).map((option) => (
<option
key={option.code}
value={option.code}
>
{option.name}
</option>
))}
</select>
) : (
<input
aria-label={label}
value={String(value ?? '')}
onChange={(event) => onValueChange?.(generalType === 'switch' ? event.target.value === 'true' : event.target.value)}
/>
)}
{description ? <span>{description}</span> : null}
</label>
),
}))
vi.mock('@/components/formElements/Button', () => ({
default: ({ children, disabled, onClick }: { children: ReactNode; disabled?: boolean; onClick?: () => void }) => (
<button
disabled={disabled}
type="button"
onClick={onClick}
>
{children}
</button>
),
}))
const bookingRule = {
eventKey: 'booking_confirmed_guest',
displayName: 'تأیید رزرو برای مهمان',
description: 'پس از قطعی‌شدن رزرو',
enabled: true,
channel: 'in_app',
titleTemplate: 'رزرو شما تأیید شد',
bodyTemplate: 'رزرو شما برای «{{eventTitle}}» با موفقیت تأیید شد.',
actionUrlTemplate: '/bookings/{{bookingId}}',
allowedVariables: ['eventTitle', 'bookingId'],
sampleVariables: { eventTitle: 'دورهمی آخر هفته', bookingId: 'booking-sample' },
smsAllowed: true,
updatedAt: '2026-08-18T00:00:00.000Z',
}
const chatRule = {
eventKey: 'chat_message',
displayName: 'پیام جدید چت',
description: 'برای سایر اعضای گفتگو',
enabled: true,
channel: 'in_app',
titleTemplate: 'پیام جدید',
bodyTemplate: '{{messagePreview}}',
actionUrlTemplate: '/chats/{{conversationId}}',
allowedVariables: ['messagePreview', 'conversationId'],
sampleVariables: { messagePreview: 'سلام، ساعت را هماهنگ کنیم؟', conversationId: 'chat-sample' },
smsAllowed: false,
updatedAt: '2026-08-18T00:00:00.000Z',
}
describe('NotificationRulesPanel', () => {
afterEach(() => {
cleanup()
})
beforeEach(() => {
showAlert.mockImplementation((_message: string, onConfirm?: () => unknown) => {
void onConfirm?.()
})
axiosMocks.get.mockReset()
axiosMocks.patch.mockReset()
axiosMocks.get.mockResolvedValue({
data: { success: true, data: [bookingRule, chatRule] },
})
})
it('shows allowed variables, sample preview, and keeps save disabled until dirty', async () => {
render(<NotificationRulesPanel />)
expect(await screen.findByText('{{eventTitle}}')).toBeInTheDocument()
expect(screen.getByText('رزرو شما برای «دورهمی آخر هفته» با موفقیت تأیید شد.')).toBeInTheDocument()
expect(screen.getByText('/bookings/booking-sample')).toBeInTheDocument()
expect(screen.queryByText('ذخیره نشده')).not.toBeInTheDocument()
const saveButtons = screen.getAllByRole('button', { name: 'ذخیره' })
expect(saveButtons[0]).toBeDisabled()
fireEvent.change(screen.getAllByLabelText('عنوان')[0], {
target: { value: 'رزرو قطعی شد' },
})
expect(await screen.findByText('ذخیره نشده')).toBeInTheDocument()
expect(screen.getByText('رزرو قطعی شد')).toBeInTheDocument()
expect(screen.getAllByRole('button', { name: 'ذخیره' })[0]).not.toBeDisabled()
})
it('hides SMS channels for chat and warns when SMS-only is selected', async () => {
render(<NotificationRulesPanel />)
expect(await screen.findByText('پیام جدید چت')).toBeInTheDocument()
expect(screen.getByText('پیامک برای پیام‌های چت ارسال نمی‌شود؛ فقط اعلان درون‌برنامه‌ای و پوش.')).toBeInTheDocument()
const chatChannel = screen.getAllByLabelText('کانال ارسال')[1]
expect(chatChannel.querySelector('option[value="sms"]')).toBeNull()
expect(chatChannel.querySelector('option[value="in_app"]')).not.toBeNull()
fireEvent.change(screen.getAllByLabelText('کانال ارسال')[0], {
target: { value: 'sms' },
})
expect(await screen.findByText('با انتخاب فقط پیامک، اعلان درون‌برنامه‌ای و پوش ارسال نمی‌شود.')).toBeInTheDocument()
expect(screen.getByText(/کاراکتر/)).toBeInTheDocument()
})
})