The backend already accepted staff_uuid on POST /api/v1/my/appointment; only the booking modal never sent it, so the pre-assignment half of the operator model had no way in. The field sits in step 1 next to the section select and is optional by design: left empty, the session stays in the shared queue that every allowed operator sees. Its placeholder says so rather than leaving the blank state unexplained. The section select is no longer the only combobox in the modal, so the tests target it by its label instead of by role. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
192 lines
9.1 KiB
TypeScript
192 lines
9.1 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 { useAuthStore } from '../../stores/authStore';
|
|
import NewAppointmentModal from './NewAppointmentModal';
|
|
|
|
const get = api.get as ReturnType<typeof vi.fn>;
|
|
const post = api.post as ReturnType<typeof vi.fn>;
|
|
|
|
const START = 1_900_000_000;
|
|
|
|
const slot = {
|
|
start: 0, end: 0, start_time: '', end_time: '',
|
|
doctor_uuid: 'doc1', doctor_name: 'مینا یوسفی',
|
|
};
|
|
|
|
const resource = { uuid: 'r-2', name: 'لیزر CO2' };
|
|
|
|
const services = [
|
|
{ uuid: 's-1', name: 'کرایوتراپی', duration_minutes: 25, price_rials: 1_000_000, service_section: { uuid: 'sec-1', name: 'خدمات درمانگاه' } },
|
|
{ uuid: 's-2', name: 'RF فرکشنال', duration_minutes: 50, price_rials: 2_000_000, service_section: { uuid: 'sec-1', name: 'خدمات درمانگاه' } },
|
|
];
|
|
|
|
/** بیمارِ یافتشده تا فرم معتبر شود؛ زمانهای خالی از تقویم منبع. */
|
|
function mockApi() {
|
|
get.mockImplementation((url: string) => {
|
|
if (url.includes('patient-lookup')) {
|
|
return Promise.resolve({ success: true, data: { found: true, name: 'رضا رحیمی', mobile: '09120001307', national_code: '0012345678' } });
|
|
}
|
|
if (url === '/api/v1/staff') {
|
|
return Promise.resolve({ success: true, data: [{ uuid: 'st-1', full_name: 'پرسنل یک' }] });
|
|
}
|
|
if (url.includes('/service-slots')) {
|
|
return Promise.resolve({ success: true, data: {
|
|
total_duration_minutes: 25,
|
|
start_times: [{ start: START, end: START + 1500, start_time: '12:00', end_time: '12:25' }],
|
|
} });
|
|
}
|
|
return Promise.resolve({ success: true, data: {} });
|
|
});
|
|
}
|
|
|
|
/** انتخاب بخش → سرویس → زمان، همان مسیرِ مودالِ طرح. */
|
|
async function pickServiceAndTime() {
|
|
// مودال حالا دو combobox دارد — بخش و اپراتور. سراغ بخش با لیبل خودش میرویم.
|
|
fireEvent.keyDown(screen.getByLabelText('بخش'), { key: 'ArrowDown' });
|
|
fireEvent.click(await screen.findByText('خدمات درمانگاه'));
|
|
// ردیف سرویس یک checkbox است نه دکمه: انتخابش حالت دارد و باید برای screen reader
|
|
// «انتخابشده/نشده» اعلام شود.
|
|
fireEvent.click(await screen.findByRole('checkbox', { name: /کرایوتراپی/ }));
|
|
fireEvent.click(await screen.findByRole('button', { name: '12:00' }));
|
|
}
|
|
|
|
beforeEach(() => {
|
|
get.mockReset();
|
|
post.mockReset();
|
|
useAuthStore.setState({ primaryRole: 'clinic' } as any);
|
|
post.mockResolvedValue({ success: true, data: { uuid: 'new1' } });
|
|
mockApi();
|
|
});
|
|
|
|
describe('مودال ثبت نوبتِ منبع', () => {
|
|
it('نام منبع و پزشک ناظر بالای فرم میآید و زمانها از تقویم منبع خوانده میشوند', async () => {
|
|
renderWithProviders(
|
|
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
|
);
|
|
|
|
expect(screen.getByText('لیزر CO2')).toBeInTheDocument();
|
|
expect(screen.getByText('مینا یوسفی')).toBeInTheDocument();
|
|
|
|
await pickServiceAndTime();
|
|
|
|
await waitFor(() => expect(
|
|
get.mock.calls.some((c: any[]) => typeof c[0] === 'string' && c[0].includes('/api/v1/resource/r-2/service-slots')),
|
|
).toBe(true));
|
|
});
|
|
|
|
it('ثبت، نوبت را با resource_uuid و مدتِ سرویسها میفرستد', async () => {
|
|
renderWithProviders(
|
|
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
|
);
|
|
|
|
await pickServiceAndTime();
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } });
|
|
fireEvent.click(screen.getByRole('button', { name: 'جستجو' }));
|
|
await screen.findByText('بیمار یافت شد');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
|
|
|
await waitFor(() => expect(post).toHaveBeenCalled());
|
|
const [url, body] = post.mock.calls[0];
|
|
expect(url).toBe('/api/v1/my/appointment');
|
|
expect(body).toMatchObject({
|
|
resource_uuid: 'r-2',
|
|
duration_from_services: true,
|
|
service_item_uuids: ['s-1'],
|
|
service_durations: { 's-1': 25 },
|
|
slot_start: START,
|
|
patient_national_code: '0012345678',
|
|
});
|
|
});
|
|
|
|
it('بدون انتخاب سرویس و زمان، ثبت غیرفعال میماند', async () => {
|
|
renderWithProviders(
|
|
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
|
);
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } });
|
|
fireEvent.click(screen.getByRole('button', { name: 'جستجو' }));
|
|
await screen.findByText('بیمار یافت شد');
|
|
|
|
expect(screen.getByRole('button', { name: 'ثبت نوبت' })).toBeDisabled();
|
|
});
|
|
|
|
it('دلیلِ غیرفعال بودنِ ثبت را میگوید و با پیشرفتِ فرم عوض میشود', async () => {
|
|
renderWithProviders(
|
|
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
|
);
|
|
|
|
expect(await screen.findByText(/یک سرویس انتخاب کنید/)).toBeInTheDocument();
|
|
|
|
// مودال حالا دو combobox دارد — بخش و اپراتور. سراغ بخش با لیبل خودش میرویم.
|
|
fireEvent.keyDown(screen.getByLabelText('بخش'), { key: 'ArrowDown' });
|
|
fireEvent.click(await screen.findByText('خدمات درمانگاه'));
|
|
fireEvent.click(await screen.findByRole('checkbox', { name: /کرایوتراپی/ }));
|
|
|
|
// سرویس هست، زمان نه — پیام باید مرحلهٔ بعد را نشان دهد نه همان قبلی.
|
|
expect(await screen.findByText(/ساعت شروع را انتخاب کنید/)).toBeInTheDocument();
|
|
|
|
fireEvent.click(await screen.findByRole('button', { name: '12:00' }));
|
|
expect(await screen.findByText(/ابتدا بیمار را جستجو کنید/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('تاریخ نوبت بهصورت جلالی بالای فرم میآید', () => {
|
|
renderWithProviders(
|
|
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
|
);
|
|
|
|
// بدون این، کاربر روزِ در حال رزرو را هیچجای مودال نمیدید.
|
|
expect(screen.getByText('تاریخ:')).toBeInTheDocument();
|
|
expect(screen.getByText('۱۴۰۵/۰۵/۱۳')).toBeInTheDocument();
|
|
});
|
|
|
|
it('معیار جستجو یک seg با حالتِ اعلامشده است', () => {
|
|
renderWithProviders(
|
|
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
|
);
|
|
|
|
const national = screen.getByRole('button', { name: 'کد ملی' });
|
|
const mobile = screen.getByRole('button', { name: 'شماره موبایل' });
|
|
expect(national).toHaveAttribute('aria-pressed', 'true');
|
|
expect(mobile).toHaveAttribute('aria-pressed', 'false');
|
|
|
|
fireEvent.click(mobile);
|
|
expect(screen.getByRole('button', { name: 'شماره موبایل' })).toHaveAttribute('aria-pressed', 'true');
|
|
expect(screen.getByPlaceholderText('مثال: 09123456789')).toBeInTheDocument();
|
|
});
|
|
|
|
it('اپراتور اختیاری است و فقط وقتی انتخاب شود staff_uuid میرود', async () => {
|
|
renderWithProviders(
|
|
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
|
);
|
|
|
|
await pickServiceAndTime();
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } });
|
|
fireEvent.click(screen.getByRole('button', { name: 'جستجو' }));
|
|
await screen.findByText('بیمار یافت شد');
|
|
|
|
// بدون انتخاب اپراتور، جلسه باید در صفِ مشترک بماند — پس کلید اصلاً نرود.
|
|
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
|
await waitFor(() => expect(post).toHaveBeenCalled());
|
|
expect(post.mock.calls[0][1]).not.toHaveProperty('staff_uuid');
|
|
});
|
|
|
|
it('منبعِ بدون سرویس، پیام راهنما میدهد نه فهرست خالی', () => {
|
|
renderWithProviders(
|
|
<NewAppointmentModal slot={slot} resource={resource} services={[]} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
|
);
|
|
|
|
expect(screen.getByText(/برای این منبع سرویسی تعریف نشده است/)).toBeInTheDocument();
|
|
});
|
|
});
|