Files
clinicpro/assets/admin/pages/TreatmentCasesPage.test.tsx
hamedandClaude Opus 5 c4099f0f63 feat(admin): default the booking staff from the service's protocol, and call it پرسنل
The staff field started empty even when the service's treatment protocol already
named who may perform it — a decision made once in the service settings and then
asked again on every booking. The modal now reads that protocol and preselects
its first staff member, but only until the user touches the field; otherwise a
manual choice would be wiped on the next service change. A protocol with no
staff leaves it empty and does not block submission.

Renames the four user-facing 'اپراتور' strings to 'پرسنل', matching the record
in /admin/staff that they all refer to. ResourcesPage keeps the word: there it
names a kind of bookable resource (doctor, operator, room, device), not a
ClinicStaff row.

Drops a test whose premise the default invalidated; the two new ones cover both
sides — protocol with staff sends staff_uuid, protocol without staff sends none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:28:13 +03:30

189 lines
7.6 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 TreatmentCasesPage from './TreatmentCasesPage';
const get = api.get as ReturnType<typeof vi.fn>;
function caseRow(over: Record<string, unknown> = {}) {
return {
uuid: 'case-1',
status: 'active',
total_sessions: 3,
completed_sessions: 1,
opened_at: 1_786_000_000,
closed_at: null,
service: { uuid: 'svc-1', name: 'لیزر توتال' },
supervisor: { uuid: 'doc-1', name: 'پزشک مدیسا' },
patient: { record_uuid: 'rec-1', name: 'محمد رسولی', mobile: '09120001111', record_number: '۱۲' },
areas: [{ uuid: 'ca-1', name: 'دست', category_uuid: 'cat-1' }],
assigned_staff: [],
performed_by: [],
...over,
};
}
/** فقط اندپوینت فهرست را جواب می‌دهد؛ بقیه خالی. */
function mockList(rows: unknown[]) {
get.mockImplementation((url: string) => {
if (url.startsWith('/api/v1/treatment-cases')) return Promise.resolve({ success: true, data: rows });
return Promise.resolve({ success: true, data: [] });
});
}
beforeEach(() => {
get.mockReset();
});
describe('صفحهٔ پرونده‌های درمان', () => {
it('نام بیمار سرتیتر کارت است، نه نام سرویس', async () => {
mockList([caseRow()]);
renderWithProviders(<TreatmentCasesPage />);
await screen.findByText('محمد رسولی');
fireEvent.click(screen.getByLabelText('نمایش کارتی'));
const name = await screen.findByText('محمد رسولی');
expect(name.tagName).toBe('STRONG');
expect(screen.getByText('لیزر توتال')).toBeInTheDocument();
});
/** جستجو باید به سرور برود، نه اینکه فهرست را در مرورگر فیلتر کند. */
it('عبارت جستجو را به‌صورت پارامتر q می‌فرستد', async () => {
mockList([caseRow()]);
renderWithProviders(<TreatmentCasesPage />);
await screen.findByText('محمد رسولی');
fireEvent.change(screen.getByLabelText('جستجوی پرونده'), { target: { value: 'رسولی' } });
await waitFor(
() => expect(get.mock.calls.some(
(c: unknown[]) => typeof c[0] === 'string' && c[0].includes('q=%D8%B1%D8%B3%D9%88%D9%84%DB%8C'),
)).toBe(true),
{ timeout: 2000 },
);
});
it('فیلتر وضعیت به‌صورت status می‌رود', async () => {
mockList([caseRow()]);
renderWithProviders(<TreatmentCasesPage />);
await screen.findByText('محمد رسولی');
fireEvent.click(screen.getByRole('button', { name: 'رها شده' }));
await waitFor(() => expect(get.mock.calls.some(
(c: unknown[]) => typeof c[0] === 'string' && c[0].includes('status=abandoned'),
)).toBe(true));
});
/** نتیجهٔ خالیِ جستجو با «هنوز پرونده‌ای ساخته نشده» یکی نیست. */
it('خالیِ جستجو پیام خودش را دارد', async () => {
mockList([]);
renderWithProviders(<TreatmentCasesPage />);
await screen.findByText(/پرونده‌ای یافت نشد/);
fireEvent.change(screen.getByLabelText('جستجوی پرونده'), { target: { value: 'هیچ' } });
expect(await screen.findByText(/برای «هیچ» پرونده‌ای پیدا نشد/, {}, { timeout: 2000 })).toBeInTheDocument();
});
it('خطا را از فهرست خالی جدا می‌کند', async () => {
get.mockRejectedValue(new Error('boom'));
renderWithProviders(<TreatmentCasesPage />);
await waitFor(() => expect(screen.getByText(/خواندن پرونده‌ها ناموفق بود/)).toBeInTheDocument());
expect(screen.queryByText(/پرونده‌ای یافت نشد/)).not.toBeInTheDocument();
});
it('بازهٔ تاریخ به‌صورت from و to می‌رود', async () => {
mockList([caseRow()]);
renderWithProviders(<TreatmentCasesPage />);
await screen.findByText('محمد رسولی');
// PersianDateInput تریگرش input نیست، پس با نام دسترس‌پذیرش پیدایش می‌کنیم.
fireEvent.click(screen.getByLabelText('شروع از تاریخ'));
expect(screen.getByLabelText('شروع تا تاریخ')).toBeInTheDocument();
});
it('تاریخ شروع ساعت هم دارد', async () => {
mockList([caseRow({ opened_at: 1_786_000_000 })]);
renderWithProviders(<TreatmentCasesPage />);
await screen.findByText('محمد رسولی');
fireEvent.click(screen.getByLabelText('نمایش کارتی'));
// formatDateTime ساعت را با «:» می‌آورد؛ formatDate نمی‌آورد.
const start = await screen.findByText(/^شروع:/);
expect(start.textContent).toMatch(/:/);
});
/** «چه کسی انجامش داد» سؤالِ اولِ مدیر است وقتی پرونده را در فهرست می‌بیند. */
it('نام پرسنل انجام‌دهنده را روی کارت می‌آورد', async () => {
mockList([caseRow({ performed_by: [{ uuid: 'st-1', name: 'پرسنل۱' }] })]);
renderWithProviders(<TreatmentCasesPage />);
await screen.findByText('محمد رسولی');
fireEvent.click(screen.getByLabelText('نمایش کارتی'));
expect(await screen.findByText(/انجام‌دهنده: پرسنل۱/)).toBeInTheDocument();
});
/** تا وقتی جلسه‌ای انجام نشده، برنامه را نشان می‌دهیم نه هیچ. */
it('اگر هنوز انجام نشده، پرسنلِ اختصاص‌یافته را نشان می‌دهد', async () => {
mockList([caseRow({ assigned_staff: [{ uuid: 'st-2', name: 'پرسنل۲' }] })]);
renderWithProviders(<TreatmentCasesPage />);
await screen.findByText('محمد رسولی');
fireEvent.click(screen.getByLabelText('نمایش کارتی'));
expect(await screen.findByText(/پرسنل: پرسنل۲/)).toBeInTheDocument();
});
/** نمای پیش‌فرض جدول است — همان الگوی صفحهٔ پرونده‌ها، با سوییچ به کارتی. */
it('پیش‌فرض جدولی است و به کارتی سوییچ می‌شود', async () => {
mockList([caseRow()]);
renderWithProviders(<TreatmentCasesPage />);
await screen.findByText('محمد رسولی');
expect(screen.getByRole('columnheader', { name: 'پرسنل' })).toBeInTheDocument();
fireEvent.click(screen.getByLabelText('نمایش کارتی'));
expect(screen.queryByRole('columnheader', { name: 'پرسنل' })).not.toBeInTheDocument();
fireEvent.click(screen.getByLabelText('نمایش جدولی'));
expect(screen.getByRole('columnheader', { name: 'پرسنل' })).toBeInTheDocument();
});
it('ستون پرسنل در جدول پر می‌شود', async () => {
mockList([caseRow({ performed_by: [{ uuid: 'st-1', name: 'پرسنل۱' }] })]);
renderWithProviders(<TreatmentCasesPage />);
const cell = await screen.findByRole('cell', { name: 'پرسنل۱' });
expect(cell).toBeInTheDocument();
});
it('دکمهٔ ویرایش مودال را باز می‌کند', async () => {
mockList([caseRow()]);
renderWithProviders(<TreatmentCasesPage />);
fireEvent.click(await screen.findByRole('button', { name: /ویرایش/ }));
expect(await screen.findByText('ویرایش پروندهٔ درمان')).toBeInTheDocument();
});
});