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