Files
clinicpro/assets/admin/pages/SubscriptionPage.test.tsx
T

139 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { screen, fireEvent, within } from '@testing-library/react';
import { renderWithProviders } from '../test/utils';
import { formatRial, formatDate } from '../lib/utils';
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
vi.mock('../lib/api', () => ({
api: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
ApiError: class extends Error {},
}));
import { api } from '../lib/api';
import SubscriptionPage from './SubscriptionPage';
const get = api.get as ReturnType<typeof vi.fn>;
const PLANS = [
{ uuid: 'p-free', name: 'free', level: 0, max_secretaries: 1, max_resources: 1,
features: { patient_records: false, services: false, sms_panel: false }, active: true, periods: [] },
{ uuid: 'p-basic', name: 'basic', level: 1, max_secretaries: 3, max_resources: 3,
features: { patient_records: true, services: true, sms_panel: false }, active: true,
periods: [
{ uuid: 'per-1m', label: 'یک ماهه', duration_months: 1, price_rials: 1000000, is_trial: false },
{ uuid: 'per-12m', label: 'یک ساله', duration_months: 12, price_rials: 9000000, is_trial: false },
] },
{ uuid: 'p-pro', name: 'professional', level: 2, max_secretaries: 10, max_resources: -1,
features: { patient_records: true, services: true, sms_panel: true }, active: true, periods: [] },
];
const DEFAULT_MY = {
subscription: { plan: { name: 'basic', level: 1, max_secretaries: 3, max_resources: 3, features: {} }, is_trial: false, days_remaining: 25 },
used_trial: true, effective_plan: null,
};
function mockApi({ my = DEFAULT_MY, plans = PLANS }: { my?: any; plans?: any[] } = {}) {
get.mockImplementation((url: string) => {
if (url.includes('/subscription/plans')) return Promise.resolve({ success: true, data: plans });
if (url.includes('/subscription/my')) return Promise.resolve({ success: true, data: my });
if (url.includes('/payment/config')) return Promise.resolve({ success: true, data: { test_mode: true, appointment_fee_rials: 0, gateways: [] } });
return Promise.resolve({ success: true, data: null });
});
}
beforeEach(() => { get.mockReset(); mockApi(); });
describe('SubscriptionPage', () => {
it('renders the three plans with the most-popular badge on the professional plan', async () => {
renderWithProviders(<SubscriptionPage />, { route: '/admin/subscription' });
expect(await screen.findByText('پلن پایه')).toBeInTheDocument();
expect(screen.getByText('پلن رایگان')).toBeInTheDocument();
expect(screen.getByText('پلن حرفه‌ای')).toBeInTheDocument();
// most-popular badge sits on the professional (top) card, matching tauri
const proCard = within(screen.getByTestId('plan-card-professional'));
expect(proCard.getByText('محبوب‌ترین')).toBeInTheDocument();
});
/** سقف منابع کنار سقف منشی روی هر کارت پلن می‌نشیند؛ `-1` یعنی «نامحدود». */
it('shows the resource quota on every plan card', async () => {
renderWithProviders(<SubscriptionPage />, { route: '/admin/subscription' });
await screen.findByText('پلن پایه');
expect(within(screen.getByTestId('plan-card-free')).getByText('۱ منبع')).toBeInTheDocument();
expect(within(screen.getByTestId('plan-card-basic')).getByText('۳ منبع')).toBeInTheDocument();
expect(within(screen.getByTestId('plan-card-professional')).getByText('نامحدود منبع')).toBeInTheDocument();
});
it('shows the current-plan card without an expiry warning when the plan is healthy', async () => {
renderWithProviders(<SubscriptionPage />, { route: '/admin/subscription' });
expect(await screen.findByText('پلن فعلی شما:')).toBeInTheDocument();
// 25 days remaining → no expiry warning (source shows it only when < 3 days)
expect(screen.queryByText(/روز تا پایان اشتراک/)).not.toBeInTheDocument();
});
it('warns when the subscription is expiring within 3 days', async () => {
mockApi({ my: { ...DEFAULT_MY, subscription: { ...DEFAULT_MY.subscription, days_remaining: 2 } } });
renderWithProviders(<SubscriptionPage />, { route: '/admin/subscription' });
expect(await screen.findByText(/روز تا پایان اشتراک/)).toBeInTheDocument();
});
it('shows the jalali expiry date on the current-plan card', async () => {
// 1767225600 = 2026-01-01T00:00:00Z → ۱۴۰۴/۱۰/۱۱ in Tehran
mockApi({ my: { ...DEFAULT_MY, subscription: { ...DEFAULT_MY.subscription, expires_at: 1767225600 } } });
renderWithProviders(<SubscriptionPage />, { route: '/admin/subscription' });
expect(await screen.findByText('زمان پایان اشتراک:')).toBeInTheDocument();
expect(screen.getByText(formatDate(1767225600))).toBeInTheDocument();
expect(screen.getByText(/روز مانده/)).toBeInTheDocument();
});
it('falls back to «بدون تاریخ انقضا» when the subscription has no expiry', async () => {
renderWithProviders(<SubscriptionPage />, { route: '/admin/subscription' });
expect(await screen.findByText('بدون تاریخ انقضا')).toBeInTheDocument();
});
it('renders the current-plan card right-to-left', async () => {
renderWithProviders(<SubscriptionPage />, { route: '/admin/subscription' });
const label = await screen.findByText('پلن فعلی شما:');
expect(label.closest('[dir]')).toHaveAttribute('dir', 'rtl');
});
it('shows the expired message when no days remain', async () => {
mockApi({ my: { ...DEFAULT_MY, subscription: { ...DEFAULT_MY.subscription, days_remaining: 0 } } });
renderWithProviders(<SubscriptionPage />, { route: '/admin/subscription' });
expect(await screen.findByText('اشتراک شما به پایان رسیده است')).toBeInTheDocument();
});
it('shows "no subscription" when the user has none', async () => {
mockApi({ my: { subscription: null, used_trial: false, effective_plan: null } });
renderWithProviders(<SubscriptionPage />, { route: '/admin/subscription' });
expect(await screen.findByText('اشتراک ندارید')).toBeInTheDocument();
});
it('the period toggle switches the displayed price', async () => {
renderWithProviders(<SubscriptionPage />, { route: '/admin/subscription' });
await screen.findByText('پلن پایه');
const card = within(screen.getByTestId('plan-card-basic'));
// default = longest period (یک ساله)
expect(card.getByText(formatRial(9000000))).toBeInTheDocument();
// switch to monthly
fireEvent.click(card.getByRole('tab', { name: 'یک ماهه' }));
expect(card.getByText(formatRial(1000000))).toBeInTheDocument();
});
it('clicking the renew button opens the payment modal with the selected amount', async () => {
renderWithProviders(<SubscriptionPage />, { route: '/admin/subscription' });
// basic is the current plan → its card button reads "تمدید اشتراک"
const card = within(await screen.findByTestId('plan-card-basic'));
fireEvent.click(card.getByText('تمدید اشتراک'));
expect(await screen.findByText('پرداخت اشتراک')).toBeInTheDocument();
expect(screen.getByText('درگاه آزمایشی فعال است')).toBeInTheDocument();
});
it('renders an empty state when there are no plans', async () => {
mockApi({ plans: [] });
renderWithProviders(<SubscriptionPage />, { route: '/admin/subscription' });
expect(await screen.findByText('در حال حاضر پلنی برای نمایش وجود ندارد.')).toBeInTheDocument();
});
});