- Replaced raw input fields for pricing with PriceInput component across various forms and modals to ensure consistent formatting and accessibility. - Updated tests to reflect changes in pricing input handling, ensuring values are displayed in toman with proper formatting. - Enhanced accessibility by adding aria-labels and aria-invalid attributes to PriceInput components. - Adjusted UI elements to improve layout and user experience, particularly in forms related to service items and scheduling. - Changed labels from "نمایش در نوبتدهی" to "نمایش در نوبتدهی آنلاین" for clarity.
258 lines
12 KiB
TypeScript
258 lines
12 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import { renderWithProviders } from '../../test/utils';
|
|
|
|
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 CreateStep, { patientShareOf } from './CreateStep';
|
|
|
|
const get = api.get as ReturnType<typeof vi.fn>;
|
|
const post = api.post as ReturnType<typeof vi.fn>;
|
|
|
|
function mockEndpoints(pricing: { free_visit_price_rials: number; require_visit_price: boolean }) {
|
|
get.mockImplementation((url: string) => {
|
|
if (url === '/api/v1/insurance-pricing') return Promise.resolve({ success: true, data: pricing });
|
|
if (url === '/api/v1/inventory-items') return Promise.resolve({ success: true, data: { items: [] } });
|
|
if (url === '/api/v1/billing/tenant-insurances') return Promise.resolve({ success: true, data: { data: [] } });
|
|
return Promise.resolve({ success: true, data: [] });
|
|
});
|
|
}
|
|
|
|
beforeEach(() => {
|
|
get.mockReset();
|
|
post.mockReset();
|
|
post.mockResolvedValue({ success: true, data: { uuid: 's-1' } });
|
|
});
|
|
|
|
const renderStep = (onCreated = vi.fn()) => {
|
|
renderWithProviders(
|
|
<CreateStep recordUuid="r-1" profile={null} onCreated={onCreated} onCancel={() => {}} />,
|
|
);
|
|
return onCreated;
|
|
};
|
|
|
|
/** PriceInput مبلغ را با جداکنندهٔ fa-IR نشان میدهد، نه رقمِ خام. */
|
|
const fa = (n: number) => new Intl.NumberFormat('fa-IR').format(n);
|
|
|
|
describe('CreateStep — الزامی بودن قیمت ویزیت با فلگ require_visit_price', () => {
|
|
it('فلگ فعال + قیمت صفر → خطای inline، ستاره روی label و عدم ارسال', async () => {
|
|
mockEndpoints({ free_visit_price_rials: 0, require_visit_price: true });
|
|
renderStep();
|
|
await waitFor(() => expect(screen.getByText(/قیمت ویزیت \(تومان\)/)).toBeInTheDocument());
|
|
await waitFor(() => expect(screen.getByText('*')).toBeInTheDocument());
|
|
|
|
fireEvent.click(screen.getByText('ایجاد سرویس'));
|
|
|
|
expect(await screen.findByText('هزینه ویزیت الزامی است')).toBeInTheDocument();
|
|
expect(post).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('فلگ فعال + مقدار معتبر → POST با visit_price_rials ریالی', async () => {
|
|
mockEndpoints({ free_visit_price_rials: 0, require_visit_price: true });
|
|
renderStep();
|
|
await waitFor(() => expect(screen.getByText('*')).toBeInTheDocument());
|
|
|
|
fireEvent.change(screen.getByLabelText('قیمت ویزیت'), { target: { value: '50000' } });
|
|
fireEvent.click(screen.getByText('ایجاد سرویس'));
|
|
|
|
await waitFor(() => expect(post).toHaveBeenCalled());
|
|
expect(post.mock.calls[0][0]).toBe('/api/v1/patient/r-1/session');
|
|
expect(post.mock.calls[0][1]).toMatchObject({ visit_price_rials: 500_000 });
|
|
});
|
|
|
|
it('فلگ غیرفعال + قیمت صفر → رفتار قبلی: ثبت مجاز', async () => {
|
|
mockEndpoints({ free_visit_price_rials: 0, require_visit_price: false });
|
|
const onCreated = renderStep();
|
|
await waitFor(() => expect(get).toHaveBeenCalledWith('/api/v1/insurance-pricing'));
|
|
|
|
fireEvent.click(screen.getByText('ایجاد سرویس'));
|
|
|
|
await waitFor(() => expect(post).toHaveBeenCalled());
|
|
expect(post.mock.calls[0][1]).toMatchObject({ visit_price_rials: 0 });
|
|
await waitFor(() => expect(onCreated).toHaveBeenCalledWith('s-1'));
|
|
expect(screen.queryByText('هزینه ویزیت الزامی است')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('پیشفرض قیمت از «قیمت ویزیت آزاد» پر میشود (ریال → تومان)', async () => {
|
|
mockEndpoints({ free_visit_price_rials: 300_000, require_visit_price: false });
|
|
renderStep();
|
|
|
|
await waitFor(() => expect(screen.getByLabelText('قیمت ویزیت')).toHaveValue(fa(30_000)));
|
|
});
|
|
});
|
|
|
|
describe('patientShareOf — آینهی BillingCalculator', () => {
|
|
it('سناریوی مرجع: ۳۰٪ پوشش پایه روی ۵,۹۵۲,۰۰۰ ریال', () => {
|
|
const share = patientShareOf(5_952_000, { covered: true, percent: 30, franchise: 0, ceiling: null }, null);
|
|
expect(share).toBe(4_166_400);
|
|
});
|
|
|
|
it('فرانشیز بیمهٔ پایه سهم بیمار را زیاد نمیکند', () => {
|
|
const share = patientShareOf(600_000, { covered: true, percent: 100, franchise: 50, ceiling: null }, null);
|
|
expect(share).toBe(0);
|
|
});
|
|
|
|
it('فرانشیز درصدیِ تکمیلی از سهم بیمه کم میشود', () => {
|
|
// پایه ۷۰٪ → ۴۲۰٬۰۰۰؛ باقیمانده ۱۸۰٬۰۰۰؛ تکمیلی ۱۰۰٪ منهای فرانشیز ۱۰٪ → ۱۶۲٬۰۰۰.
|
|
const share = patientShareOf(
|
|
600_000,
|
|
{ covered: true, percent: 70, franchise: 50, ceiling: null },
|
|
{ covered: true, percent: 100, franchise: 10, ceiling: null },
|
|
);
|
|
expect(share).toBe(18_000);
|
|
});
|
|
|
|
it('فرانشیز بزرگتر از تعهد، سهم بیمه را صفر میکند نه منفی', () => {
|
|
const share = patientShareOf(600_000, null, { covered: true, percent: 20, franchise: 80, ceiling: null });
|
|
expect(share).toBe(600_000);
|
|
});
|
|
|
|
it('سقف تعهد سهم بیمه را محدود میکند', () => {
|
|
const share = patientShareOf(600_000, { covered: true, percent: 70, franchise: 0, ceiling: 300_000 }, null);
|
|
expect(share).toBe(300_000);
|
|
});
|
|
|
|
it('notCovered → کل مبلغ سهم بیمار', () => {
|
|
const share = patientShareOf(600_000, { covered: false, percent: 0, franchise: 0, ceiling: null }, null);
|
|
expect(share).toBe(600_000);
|
|
});
|
|
});
|
|
|
|
// ── بیمه: نوع خدمت در ثبت/ویرایش مراجعه ──────────────────────────────────────
|
|
|
|
const BASIC_CONTRACT = {
|
|
uuid: 'c1', insurance_id: 3, insurance_name: 'بیمه ایران', insurance_kind: 'basic',
|
|
is_active: true, coverage_percent: 70, franchise_percent: 0, annual_ceiling_rials: null,
|
|
category_coverages: { outpatient: 70, inpatient: 30 },
|
|
};
|
|
|
|
/** پروفایلِ بیمار با بیمهٔ پایه، تا بلوک بیمه نمایش داده شود. */
|
|
const insuredProfile = { basic_insurance_id: 3 } as never;
|
|
|
|
function mockInsuranceEndpoints(enabled: string[]) {
|
|
const categories = [
|
|
{ key: 'outpatient', label: 'خدمات سرپایی', enabled: enabled.includes('outpatient') },
|
|
{ key: 'inpatient', label: 'خدمات بستری', enabled: enabled.includes('inpatient') },
|
|
];
|
|
get.mockImplementation((url: string) => {
|
|
if (url === '/api/v1/insurance-pricing') return Promise.resolve({ success: true, data: {
|
|
free_visit_price_rials: 5_952_000,
|
|
require_visit_price: false,
|
|
service_categories: categories,
|
|
default_service_category: enabled.length === 1 ? enabled[0] : null,
|
|
} });
|
|
if (url === '/api/v1/inventory-items') return Promise.resolve({ success: true, data: { items: [] } });
|
|
if (url === '/api/v1/billing/tenant-insurances') return Promise.resolve({ success: true, data: { data: [BASIC_CONTRACT] } });
|
|
return Promise.resolve({ success: true, data: [] });
|
|
});
|
|
}
|
|
|
|
/** react-select: منو با ArrowDown باز میشود و گزینه با role=option انتخاب میشود. */
|
|
async function pick(selectLabel: string, optionText: string) {
|
|
fireEvent.keyDown(screen.getByRole('combobox', { name: selectLabel }), { key: 'ArrowDown' });
|
|
fireEvent.click(await screen.findByRole('option', { name: optionText }));
|
|
}
|
|
|
|
describe('CreateStep — نوع خدمت بیمه', () => {
|
|
it('با فعال بودن هر دو نوع، انتخاب نوع خدمت نمایش داده میشود', async () => {
|
|
mockInsuranceEndpoints(['outpatient', 'inpatient']);
|
|
renderWithProviders(
|
|
<CreateStep recordUuid="r-1" profile={insuredProfile} onCreated={vi.fn()} onCancel={() => {}} />,
|
|
);
|
|
|
|
expect(await screen.findByText('نوع خدمت')).toBeInTheDocument();
|
|
});
|
|
|
|
it('با فعال بودن فقط یک نوع، انتخاب نوع خدمت پنهان است', async () => {
|
|
mockInsuranceEndpoints(['outpatient']);
|
|
renderWithProviders(
|
|
<CreateStep recordUuid="r-1" profile={insuredProfile} onCreated={vi.fn()} onCancel={() => {}} />,
|
|
);
|
|
|
|
// ۵٬۹۵۲٬۰۰۰ ریال = ۵۹۵٬۲۰۰ تومان (ورودی قیمت ویزیت رقم خام است)
|
|
await waitFor(() => expect(screen.getByLabelText('قیمت ویزیت')).toHaveValue(fa(595_200)));
|
|
expect(screen.queryByText('نوع خدمت')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('انتخاب نوع خدمت، درصد پوشش همان نوع را روی فرم میگذارد و ارسال میکند', async () => {
|
|
mockInsuranceEndpoints(['outpatient', 'inpatient']);
|
|
renderWithProviders(
|
|
<CreateStep recordUuid="r-1" profile={insuredProfile} onCreated={vi.fn()} onCancel={() => {}} />,
|
|
);
|
|
await screen.findByText('نوع خدمت');
|
|
|
|
await pick('بدون بیمه پایه', 'بیمه ایران');
|
|
await pick('انتخاب نوع خدمت', 'خدمات بستری');
|
|
|
|
// درصد بستری = ۳۰
|
|
await waitFor(() => expect(screen.getByLabelText('تخفیف بیمه پایه')).toHaveValue('30'));
|
|
|
|
fireEvent.click(screen.getByText('ایجاد سرویس'));
|
|
await waitFor(() => expect(post).toHaveBeenCalled());
|
|
expect(post.mock.calls[0][1]).toMatchObject({
|
|
insurance_base_id: 3,
|
|
insurance_service_category: 'inpatient',
|
|
base_insurance_discount_percent: 30,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('CreateStep — نمایش و پیشانتخاب بیمه', () => {
|
|
it('با داشتن قرارداد بیمه، بلوک بیمه نمایش داده میشود (بدون بیمهٔ پروفایل)', async () => {
|
|
mockInsuranceEndpoints(['outpatient', 'inpatient']);
|
|
renderWithProviders(
|
|
<CreateStep recordUuid="r-1" profile={null} onCreated={vi.fn()} onCancel={() => {}} />,
|
|
);
|
|
|
|
expect(await screen.findByText('بیمه پایه')).toBeInTheDocument();
|
|
expect(screen.getByText('بیمه تکمیلی')).toBeInTheDocument();
|
|
});
|
|
|
|
it('بدون هیچ قراردادی، بلوک بیمه نمایش داده نمیشود', async () => {
|
|
mockEndpoints({ free_visit_price_rials: 300_000, require_visit_price: false });
|
|
renderWithProviders(
|
|
<CreateStep recordUuid="r-1" profile={null} onCreated={vi.fn()} onCancel={() => {}} />,
|
|
);
|
|
|
|
await waitFor(() => expect(screen.getByLabelText('قیمت ویزیت')).toHaveValue(fa(30_000)));
|
|
expect(screen.queryByText('بیمه پایه')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('مراجعهٔ جدید: بیمهٔ پروفایل بیمار پیشانتخاب میشود', async () => {
|
|
mockInsuranceEndpoints(['outpatient']);
|
|
renderWithProviders(
|
|
<CreateStep recordUuid="r-1" profile={insuredProfile} onCreated={vi.fn()} onCancel={() => {}} />,
|
|
);
|
|
|
|
// بیمهٔ پروفایل (id=3) قرارداد فعال دارد → انتخاب و درصد سرپایی ۷۰
|
|
expect(await screen.findByText('بیمه ایران')).toBeInTheDocument();
|
|
await waitFor(() => expect(screen.getByLabelText('تخفیف بیمه پایه')).toHaveValue('70'));
|
|
});
|
|
|
|
it('ویرایش مراجعه: بیمهٔ ثبتشده مشخص و قابل تغییر است', async () => {
|
|
mockInsuranceEndpoints(['outpatient', 'inpatient']);
|
|
const editSession = {
|
|
uuid: 's-9', visit_price_rials: 5_952_000, session_at: 1_700_000_000,
|
|
insurance_base_id: 3, base_insurance_discount_percent: 30,
|
|
insurance_service_category: 'inpatient',
|
|
services: [], consumables: [],
|
|
} as never;
|
|
|
|
renderWithProviders(
|
|
<CreateStep recordUuid="r-1" profile={null} editSession={editSession} onCreated={vi.fn()} onCancel={() => {}} />,
|
|
);
|
|
|
|
expect(await screen.findByText('بیمه ایران')).toBeInTheDocument();
|
|
expect(screen.getByText('خدمات بستری')).toBeInTheDocument();
|
|
expect(screen.getByLabelText('تخفیف بیمه پایه')).toHaveValue('30');
|
|
|
|
// قابل تغییر: انتخاب نوع سرپایی درصد را به ۷۰ میبرد
|
|
await pick('انتخاب نوع خدمت', 'خدمات سرپایی');
|
|
await waitFor(() => expect(screen.getByLabelText('تخفیف بیمه پایه')).toHaveValue('70'));
|
|
});
|
|
});
|