Files
clinicpro/assets/admin/pages/NewSessionPage.test.tsx
T
hamed 6876135a53 feat: add BlogBodySanitizer for HTML sanitization on article save
- Implemented BlogBodySanitizer to clean HTML content before saving articles, ensuring security against XSS attacks.
- Added tests for BlogBodySanitizer to verify that unsafe tags and attributes are stripped from the content.
- Introduced ApiLeastPrivilegeTest to ensure that unauthorized users cannot access sensitive API routes, maintaining strict access control.
2026-08-07 21:13:38 +03:30

192 lines
8.7 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, waitFor } from '@testing-library/react';
import { Routes, Route } from 'react-router';
import { renderWithProviders } from '../test/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 { formatRial } from '../lib/utils';
import NewSessionPage from './NewSessionPage';
const get = api.get as ReturnType<typeof vi.fn>;
const post = api.post as ReturnType<typeof vi.fn>;
const record = (profileOver: object = {}) => ({
uuid: 'r1',
user_name: 'ساغر صابری',
profile: { full_name: 'ساغر صابری', basic_insurance_id: null, supplementary_insurance_id: null, ...profileOver },
});
function mockGets(over: Partial<Record<string, unknown>> = {}) {
get.mockImplementation((url: string) => {
if (url === '/api/v1/patient/r1') return Promise.resolve({ success: true, data: over.record ?? record() });
if (url === '/api/v1/service-sections') {
return Promise.resolve({ success: true, data: over.sections ?? [{ uuid: 'sec1', name: 'بخش زیبایی' }] });
}
if (url === '/api/v1/service-items/sec1') {
return Promise.resolve({
success: true,
data: over.items ?? [
{ uuid: 'svc1', name: 'کاشت مو', price_rials: 2_400_000, active: true, insurance_covered: false, staff: null },
],
});
}
if (url === '/api/v1/staff') {
return Promise.resolve({ success: true, data: over.staff ?? [{ uuid: 'st1', full_name: 'علی رضایی', active: true }] });
}
if (url === '/api/v1/inventory-items') {
return Promise.resolve({
success: true,
data: { items: over.inventory ?? [{ uuid: 'inv1', name: 'عینک', unit: 'عدد', price: 1_200_000, stock: 10, status: 'in_stock' }], stats: {} },
});
}
if (url === '/api/v1/inventory-packages') {
return Promise.resolve({ success: true, data: over.packages ?? [{ uuid: 'pkg1', title: 'پکیج شماره یک', total: 2_400_000, available: true }] });
}
if (url === '/api/v1/billing/tenant-insurances') {
return Promise.resolve({ success: true, data: { data: over.contracts ?? [] } });
}
if (url === '/api/v1/insurance-pricing') {
return Promise.resolve({ success: true, data: { free_visit_price_rials: 500_000 } });
}
if (url === '/api/v1/patient/r1/sessions') {
return Promise.resolve({ success: true, data: over.sessions ?? [] });
}
if (url === '/api/v1/patient/r1/wallet') {
return Promise.resolve({ success: true, data: { balance_rials: 300_000, recent_transactions: [] } });
}
return Promise.resolve({ success: true, data: [] });
});
}
function renderPage() {
return renderWithProviders(
<Routes>
<Route path="/admin/patients/:recordUuid/session/new" element={<NewSessionPage />} />
</Routes>,
{ route: '/admin/patients/r1/session/new' },
);
}
/** انتخاب گزینه از SearchableSelect (react-select) با inputId */
async function pickOption(inputId: string, option: string) {
const input = document.getElementById(inputId) as HTMLInputElement;
fireEvent.focus(input);
fireEvent.keyDown(input, { key: 'ArrowDown' });
fireEvent.click(await screen.findByText(option));
}
beforeEach(() => {
get.mockReset();
post.mockReset();
});
describe('NewSessionPage (ویزارد ثبت مراجعه — پورت tauri create-service)', () => {
it('renders the 3-step stepper and step-1 fields (tauri Step1Details)', async () => {
mockGets();
renderPage();
// استپر سه گام («ایجاد سرویس» هم برچسب گام است هم دکمه ثبت)
expect((await screen.findAllByText('ایجاد سرویس')).length).toBeGreaterThanOrEqual(2);
expect(screen.getByText('پرداخت')).toBeInTheDocument();
expect(screen.getByText('جزییات')).toBeInTheDocument();
// فیلدهای گام ۱
expect(screen.getByText('تاریخ پذیرش')).toBeInTheDocument();
expect(screen.getByText('ساعت پذیرش')).toBeInTheDocument();
expect(screen.getByText('انتخاب بخش')).toBeInTheDocument();
expect(screen.getByText('انتخاب سرویس')).toBeInTheDocument();
expect(screen.getByText('پرسنل')).toBeInTheDocument();
expect(screen.getByText('انتخاب کالای مصرفی')).toBeInTheDocument();
expect(screen.getByText('انتخاب پکیج')).toBeInTheDocument();
// پذیرش‌کننده + breadcrumb بیمار
expect(screen.getByText(/پذیرش کننده:/)).toBeInTheDocument();
expect((await screen.findAllByText('ساغر صابری')).length).toBeGreaterThan(0);
// بدون سرویس بیمه‌دار و بدون بیمه‌ی پروفایل → بلوک بیمه پنهان
expect(screen.queryByText('بیمه پایه')).not.toBeInTheDocument();
});
it('shows the insurance block when patient profile has insurance', async () => {
mockGets({ record: record({ basic_insurance_id: 7 }) });
renderPage();
expect(await screen.findByText('بیمه پایه')).toBeInTheDocument();
expect(screen.getByText('بیمه تکمیلی')).toBeInTheDocument();
});
it('creates the session with session_at, consumables, package and staff then moves to payment step', async () => {
mockGets({
sessions: [{
uuid: 's-new', services: [], visit_price_rials: 500_000, final_price_rials: 1_700_000,
patient_debt_rials: 1_700_000, discount_rials: 0, payments: [], is_paid: false,
}],
});
post.mockResolvedValue({ success: true, data: { uuid: 's-new' } });
renderPage();
await screen.findByText('انتخاب بخش');
// بخش → سرویس → افزودن
await pickOption('section-select', 'بخش زیبایی');
await waitFor(() => expect(get).toHaveBeenCalledWith('/api/v1/service-items/sec1'));
await pickOption('service-select', 'کاشت مو');
fireEvent.click(screen.getByLabelText('افزودن سرویس'));
expect(screen.getByText('خدمات انتخاب شده')).toBeInTheDocument();
// کالای مصرفی: عینک
await pickOption('consumable-select', 'عینک');
fireEvent.click(screen.getByLabelText('افزودن کالا'));
expect(screen.getByText(`مجموع قیمت کالاها: ${formatRial(1_200_000)}`)).toBeInTheDocument();
// ثبت
fireEvent.click(screen.getByText('ایجاد سرویس', { selector: 'button' }));
await waitFor(() => expect(post).toHaveBeenCalledTimes(1));
const [url, body] = post.mock.calls[0] as [string, any];
expect(url).toBe('/api/v1/patient/r1/session');
expect(typeof body.session_at).toBe('number');
expect(body.payment_method).toBe('pending');
expect(body.services).toEqual([{ service_item_uuid: 'svc1', quantity: 1 }]);
expect(body.consumables).toEqual([{ inventory_item_uuid: 'inv1', quantity: 1 }]);
// بدون بیمه → درصدها صفر و id ارسال نمی‌شود
expect(body.base_insurance_discount_percent).toBe(0);
expect(body.insurance_base_id).toBeUndefined();
// ورود به گام پرداخت با دیتای واقعی session ساخته‌شده
expect(await screen.findByText('هزینه سرویس:')).toBeInTheDocument();
expect(screen.getByText('موجودی کیف پول:')).toBeInTheDocument();
expect(screen.getAllByText(formatRial(1_700_000)).length).toBeGreaterThan(0);
});
it('increments consumable quantity via the counter and reflects the total', async () => {
mockGets();
renderPage();
await screen.findByText('انتخاب کالای مصرفی');
await pickOption('consumable-select', 'عینک');
fireEvent.click(screen.getByLabelText('افزودن کالا'));
fireEvent.click(screen.getByLabelText('افزایش'));
expect(screen.getByText(`مجموع قیمت کالاها: ${formatRial(2_400_000)}`)).toBeInTheDocument();
// کاهش تا صفر → حذف
fireEvent.click(screen.getByLabelText('کاهش'));
fireEvent.click(screen.getByLabelText('کاهش'));
expect(screen.getByText('کالایی انتخاب نشده است')).toBeInTheDocument();
});
it('renders empty option lists without crashing (بدون بخش/کالا/پکیج)', async () => {
mockGets({ sections: [], inventory: [], packages: [], staff: [] });
renderPage();
expect(await screen.findByText('انتخاب بخش')).toBeInTheDocument();
expect(screen.getByText('کالایی انتخاب نشده است')).toBeInTheDocument();
// مجموع صفر
expect(screen.getByText(`مجموع قیمت کالاها: ${formatRial(0)}`)).toBeInTheDocument();
});
});