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.
167 lines
5.6 KiB
TypeScript
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()
|
|
})
|
|
})
|