admin/app/(dashboard)/notifications/_components/NotificationRulesPanel.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

159 lines
5.3 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,
},
}))
vi.mock('@/lib/toast', () => ({ addToast: vi.fn() }))
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(() => {
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()
})
})