feat(appointments): implement multi-step wizard for appointment booking modal
This commit is contained in:
@@ -0,0 +1,107 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
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 NewAppointmentModal from './NewAppointmentModal';
|
||||||
|
|
||||||
|
const get = api.get as ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
|
const slot = {
|
||||||
|
start: 1_780_000_000,
|
||||||
|
end: 1_780_001_800,
|
||||||
|
start_time: '۰۹:۰۰',
|
||||||
|
end_time: '۰۹:۳۰',
|
||||||
|
doctor_uuid: 'doc-1',
|
||||||
|
doctor_name: 'دکتر رضایی',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
get.mockReset();
|
||||||
|
get.mockResolvedValue({ success: true, data: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = (overrides: Record<string, unknown> = {}) => ({
|
||||||
|
slot,
|
||||||
|
onClose: vi.fn(),
|
||||||
|
onSuccess: vi.fn(),
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** عنوان مرحلههای نوار بالای مودال. */
|
||||||
|
const stepLabels = () =>
|
||||||
|
Array.from(document.querySelectorAll('ol[aria-label="مراحل ثبت نوبت"] li'))
|
||||||
|
.map(li => li.textContent?.replace(/^\d+/, '').trim());
|
||||||
|
|
||||||
|
const pickerProps = () => props({ serviceMode: true, date: '1405-05-18', services: [] });
|
||||||
|
|
||||||
|
describe('NewAppointmentModal — ویزارد', () => {
|
||||||
|
/**
|
||||||
|
* فرم قبلاً یک صفحهٔ بلند بود: خدمت و زمان، بیمار و هزینه با هم. هر بار یک مرحله
|
||||||
|
* دیده میشود تا مودال از ارتفاع صفحه بلندتر نشود.
|
||||||
|
*/
|
||||||
|
it('حالت انتخابگر سه مرحله دارد و از مرحلهٔ اول شروع میشود', () => {
|
||||||
|
renderWithProviders(<NewAppointmentModal {...pickerProps()} />);
|
||||||
|
|
||||||
|
expect(stepLabels()).toEqual(['خدمت و زمان', 'بیمار', 'تأیید و ثبت']);
|
||||||
|
expect(screen.getByRole('heading', { name: 'خدمت و زمان' })).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole('heading', { name: 'بیمار' })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('مرحلهای که دادهاش از قبل معلوم است ساخته نمیشود', () => {
|
||||||
|
// نوبت اسلاتی: زمان از خودِ اسلات میآید، پس مرحلهٔ «خدمت و زمان» بیمعناست.
|
||||||
|
renderWithProviders(<NewAppointmentModal {...props()} />);
|
||||||
|
|
||||||
|
expect(stepLabels()).toEqual(['بیمار', 'تأیید و ثبت']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('بیمارِ از پیش معلوم، مرحلهٔ بیمار را حذف میکند', () => {
|
||||||
|
renderWithProviders(<NewAppointmentModal {...props({
|
||||||
|
patient: { name: 'علی محمدی', mobile: '09123456789', national_code: '1234567890' },
|
||||||
|
})} />);
|
||||||
|
|
||||||
|
// تنها مرحلهٔ باقیمانده «تأیید و ثبت» است، پس نوار مراحل هم لازم نیست.
|
||||||
|
expect(document.querySelector('ol[aria-label="مراحل ثبت نوبت"]')).toBeNull();
|
||||||
|
expect(screen.getByRole('heading', { name: 'تأیید و ثبت' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('تا وقتی مرحله کامل نشده، دکمهٔ بعدی غیرفعال است و دلیلش را میگوید', () => {
|
||||||
|
renderWithProviders(<NewAppointmentModal {...pickerProps()} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole('button', { name: 'مرحلهٔ بعد' })).toBeDisabled();
|
||||||
|
expect(screen.getByText(/یک سرویس انتخاب کنید/)).toBeInTheDocument();
|
||||||
|
// دکمهٔ ثبت فقط در مرحلهٔ آخر وجود دارد.
|
||||||
|
expect(screen.queryByRole('button', { name: 'ثبت نوبت' })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('در مرحلهٔ اول دکمهٔ «مرحلهٔ قبل» نیست، ولی «انصراف» همیشه هست', () => {
|
||||||
|
renderWithProviders(<NewAppointmentModal {...pickerProps()} />);
|
||||||
|
|
||||||
|
expect(screen.queryByRole('button', { name: 'مرحلهٔ قبل' })).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: 'انصراف' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('مرحلهٔ آخر خلاصهٔ نوبت را نشان میدهد', () => {
|
||||||
|
renderWithProviders(<NewAppointmentModal {...props({
|
||||||
|
patient: { name: 'علی محمدی', mobile: '09123456789', national_code: '1234567890' },
|
||||||
|
})} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('علی محمدی')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('09123456789')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('دکمهٔ اصلی در فوتر ثابت است، نه داخل ناحیهٔ اسکرول', () => {
|
||||||
|
renderWithProviders(<NewAppointmentModal {...pickerProps()} />);
|
||||||
|
|
||||||
|
const next = screen.getByRole('button', { name: 'مرحلهٔ بعد' });
|
||||||
|
|
||||||
|
expect(next.closest('.modal-foot')).not.toBeNull();
|
||||||
|
expect(next.closest('.modal-body')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { ChevronDownIcon, ClockIcon, MagnifyingGlassIcon, CheckCircleIcon, CpuChipIcon, ExclamationCircleIcon } from '@heroicons/react/24/outline';
|
import { ChevronDownIcon, ClockIcon, MagnifyingGlassIcon, CheckCircleIcon, CpuChipIcon, ExclamationCircleIcon } from '@heroicons/react/24/outline';
|
||||||
@@ -24,6 +24,14 @@ export interface BookingSlot {
|
|||||||
doctor_name: string;
|
doctor_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StepKey = 'service' | 'patient' | 'confirm';
|
||||||
|
|
||||||
|
const STEP_TITLES: Record<StepKey, string> = {
|
||||||
|
service: 'خدمت و زمان',
|
||||||
|
patient: 'بیمار',
|
||||||
|
confirm: 'تأیید و ثبت',
|
||||||
|
};
|
||||||
|
|
||||||
interface PatientLookup { found: boolean; name?: string | null; mobile?: string; national_code?: string | null }
|
interface PatientLookup { found: boolean; name?: string | null; mobile?: string; national_code?: string | null }
|
||||||
|
|
||||||
/** منبعِ هدفِ نوبت — دستگاه/اتاق/پرسنلی که نوبت رویش مینشیند. */
|
/** منبعِ هدفِ نوبت — دستگاه/اتاق/پرسنلی که نوبت رویش مینشیند. */
|
||||||
@@ -160,6 +168,7 @@ export default function NewAppointmentModal({
|
|||||||
// هزینه ویزیت اختیاری داخل کلپسِ بسته مینشیند؛ وقتی الزامی است کلپس همیشه باز است.
|
// هزینه ویزیت اختیاری داخل کلپسِ بسته مینشیند؛ وقتی الزامی است کلپس همیشه باز است.
|
||||||
const [visitPriceOpen, setVisitPriceOpen] = useState(false);
|
const [visitPriceOpen, setVisitPriceOpen] = useState(false);
|
||||||
const visitPriceExpanded = requireVisit || visitPriceOpen;
|
const visitPriceExpanded = requireVisit || visitPriceOpen;
|
||||||
|
const [stepIdx, setStepIdx] = useState(0);
|
||||||
|
|
||||||
const mobileValid = /^09\d{9}$/.test(mobile);
|
const mobileValid = /^09\d{9}$/.test(mobile);
|
||||||
const nationalCodeValid = /^\d{10}$/.test(nationalCode);
|
const nationalCodeValid = /^\d{10}$/.test(nationalCode);
|
||||||
@@ -264,19 +273,51 @@ export default function NewAppointmentModal({
|
|||||||
...(activeDate && !pickerMode ? [{ label: 'تاریخ', value: formatDate(activeDate) }] : []),
|
...(activeDate && !pickerMode ? [{ label: 'تاریخ', value: formatDate(activeDate) }] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
// اولین چیزی که جلوی ثبت را گرفته، به ترتیبِ همان مراحلِ فرم. دکمهٔ خاکستریِ
|
/**
|
||||||
|
* مراحلِ ویزارد — فقط مراحلی که واقعاً تصمیمی دارند.
|
||||||
|
*
|
||||||
|
* فرم قبلاً یک صفحهٔ بلند بود: انتخاب سرویس و زمان، جستجوی بیمار و هزینه همه با هم.
|
||||||
|
* مرحلهای که دادهاش از قبل معلوم است ساخته نمیشود — نوبتِ اسلاتی زمان دارد و
|
||||||
|
* فرمِ بازشده از پروندهٔ بیمار، بیمار.
|
||||||
|
*/
|
||||||
|
const stepKeys: StepKey[] = [
|
||||||
|
...(pickerMode && activeDate ? (['service'] as StepKey[]) : []),
|
||||||
|
...(patient === null ? (['patient'] as StepKey[]) : []),
|
||||||
|
'confirm',
|
||||||
|
];
|
||||||
|
const currentStep = stepKeys[Math.min(stepIdx, stepKeys.length - 1)];
|
||||||
|
const isLastStep = currentStep === 'confirm';
|
||||||
|
|
||||||
|
// اولین چیزی که جلوی رفتن به مرحلهٔ بعد (یا ثبت) را گرفته. دکمهٔ خاکستریِ
|
||||||
// بیتوضیح یعنی کاربر باید حدس بزند چه چیزی کم است.
|
// بیتوضیح یعنی کاربر باید حدس بزند چه چیزی کم است.
|
||||||
const blockReason = !serviceTimingValid
|
const stepBlockReason: Record<StepKey, string | null> = {
|
||||||
? (pick.serviceUuids.length === 0 ? 'یک سرویس انتخاب کنید' : 'ساعت شروع را انتخاب کنید')
|
service: !serviceTimingValid
|
||||||
: lookup === null
|
? (pick.serviceUuids.length === 0 ? 'یک سرویس انتخاب کنید' : 'ساعت شروع را انتخاب کنید')
|
||||||
|
: null,
|
||||||
|
patient: lookup === null
|
||||||
? 'ابتدا بیمار را جستجو کنید'
|
? 'ابتدا بیمار را جستجو کنید'
|
||||||
: needsDetails && !detailsValid
|
: needsDetails && !detailsValid
|
||||||
? 'مشخصات بیمار را کامل کنید'
|
? 'مشخصات بیمار را کامل کنید'
|
||||||
: !mobileValid
|
: !mobileValid
|
||||||
? 'شماره موبایل بیمار معتبر نیست'
|
? 'شماره موبایل بیمار معتبر نیست'
|
||||||
: !visitPriceValid
|
: null,
|
||||||
? 'هزینه ویزیت الزامی است'
|
confirm: !visitPriceValid ? 'هزینه ویزیت الزامی است' : null,
|
||||||
: null;
|
};
|
||||||
|
const blockReason = stepBlockReason[currentStep];
|
||||||
|
|
||||||
|
// خلاصهٔ مرحلهٔ آخر: همان چیزی که ثبت میشود، پیش از ثبت.
|
||||||
|
const pickedServiceNames = pick.serviceUuids
|
||||||
|
.map(uuid => services.find(sv => sv.uuid === uuid)?.name ?? uuid);
|
||||||
|
const staffName = (staffQ.data?.data ?? []).find(st => st.uuid === staffUuid)?.full_name ?? null;
|
||||||
|
const summaryRows: { label: string; value: string }[] = [
|
||||||
|
...targetFacts.map(f => ({ label: f.label, value: f.value })),
|
||||||
|
...(pickerMode && activeDate ? [{ label: 'تاریخ', value: formatDate(activeDate) }] : []),
|
||||||
|
...(pickerMode && pick.slot ? [{ label: 'ساعت', value: pick.slot.start_time }] : []),
|
||||||
|
...(pickedServiceNames.length > 0 ? [{ label: 'سرویس', value: pickedServiceNames.join('، ') }] : []),
|
||||||
|
...(staffName ? [{ label: 'پرسنل', value: staffName }] : []),
|
||||||
|
{ label: 'بیمار', value: effectiveName || (patient?.name ?? '—') },
|
||||||
|
{ label: 'موبایل', value: mobile || (patient?.mobile ?? '—') },
|
||||||
|
];
|
||||||
|
|
||||||
const priceHint = pricingLoading
|
const priceHint = pricingLoading
|
||||||
? 'در حال خواندن تعرفهٔ پزشک…'
|
? 'در حال خواندن تعرفهٔ پزشک…'
|
||||||
@@ -288,18 +329,36 @@ export default function NewAppointmentModal({
|
|||||||
<Modal
|
<Modal
|
||||||
open
|
open
|
||||||
title="ثبت نوبت"
|
title="ثبت نوبت"
|
||||||
size="sm"
|
// هر بار یک مرحله دیده میشود، پس ستون تکی کافی است؛ `md` عرضِ راحتِ
|
||||||
|
// انتخابگر سرویس و اسلات است بدون فضای خالی.
|
||||||
|
size="md"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
|
{/* «قبلی» جای «انصراف» را نمیگیرد: بستنِ فرم همیشه باید یک کلیک باشد. */}
|
||||||
<button className="btn ghost" onClick={onClose}>انصراف</button>
|
<button className="btn ghost" onClick={onClose}>انصراف</button>
|
||||||
<button
|
{stepIdx > 0 && (
|
||||||
className="btn primary"
|
<button className="btn secondary" onClick={() => setStepIdx(i => i - 1)}>
|
||||||
onClick={() => mutation.mutate()}
|
مرحلهٔ قبل
|
||||||
disabled={!isValid || mutation.isPending}
|
</button>
|
||||||
>
|
)}
|
||||||
{mutation.isPending ? 'در حال ثبت…' : 'ثبت نوبت'}
|
{isLastStep ? (
|
||||||
</button>
|
<button
|
||||||
|
className="btn primary"
|
||||||
|
onClick={() => mutation.mutate()}
|
||||||
|
disabled={!isValid || mutation.isPending}
|
||||||
|
>
|
||||||
|
{mutation.isPending ? 'در حال ثبت…' : 'ثبت نوبت'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="btn primary"
|
||||||
|
onClick={() => setStepIdx(i => i + 1)}
|
||||||
|
disabled={blockReason !== null}
|
||||||
|
>
|
||||||
|
مرحلهٔ بعد
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -319,8 +378,12 @@ export default function NewAppointmentModal({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{pickerMode && activeDate && (
|
{stepKeys.length > 1 && (
|
||||||
<Step n={1} title="خدمت و زمان">
|
<Stepper keys={stepKeys} current={currentStep} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 'service' && (
|
||||||
|
<Step title={STEP_TITLES.service}>
|
||||||
<div className="field-block" style={{ marginBottom: 12, maxWidth: 220 }}>
|
<div className="field-block" style={{ marginBottom: 12, maxWidth: 220 }}>
|
||||||
<label htmlFor="appt-date">تاریخ نوبت</label>
|
<label htmlFor="appt-date">تاریخ نوبت</label>
|
||||||
<PersianDateInput
|
<PersianDateInput
|
||||||
@@ -357,7 +420,8 @@ export default function NewAppointmentModal({
|
|||||||
</Step>
|
</Step>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Step n={pickerMode ? 2 : null} title="بیمار">
|
{currentStep === 'patient' && (
|
||||||
|
<Step title={STEP_TITLES.patient}>
|
||||||
{/* بیمارِ از پیش معلوم: فقط تأیید میشود، جستجو لازم نیست. */}
|
{/* بیمارِ از پیش معلوم: فقط تأیید میشود، جستجو لازم نیست. */}
|
||||||
{patient !== null && (
|
{patient !== null && (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -512,9 +576,28 @@ export default function NewAppointmentModal({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Step>
|
</Step>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* هزینه ویزیت مرحلهٔ شمارهدار نیست: در حالت اختیاری یک کلپسِ بسته است و
|
{currentStep === 'confirm' && (
|
||||||
شماره دادن به آن، کاری اختیاری را اجباری نشان میداد. */}
|
<Step title={STEP_TITLES.confirm}>
|
||||||
|
{/* خلاصهٔ همان چیزی که ثبت میشود — تنها جایی که کاربر پیش از ثبت، انتخاب
|
||||||
|
سرویس و زمان و بیمار را با هم میبیند. */}
|
||||||
|
<dl style={{
|
||||||
|
display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '8px 14px', margin: '0 0 18px',
|
||||||
|
padding: '12px 14px', borderRadius: 'var(--r-sm)', background: 'var(--surface-2)',
|
||||||
|
fontSize: 13,
|
||||||
|
}}>
|
||||||
|
{summaryRows.map(row => (
|
||||||
|
<React.Fragment key={row.label}>
|
||||||
|
<dt style={{ color: 'var(--text-3)' }}>{row.label}</dt>
|
||||||
|
<dd style={{ margin: 0, color: 'var(--text)', fontWeight: 600, minWidth: 0, overflowWrap: 'anywhere' }}>
|
||||||
|
{row.value}
|
||||||
|
</dd>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
{/* هزینه ویزیت در حالت اختیاری یک کلپسِ بسته است. */}
|
||||||
<div className="field-block">
|
<div className="field-block">
|
||||||
{requireVisit ? (
|
{requireVisit ? (
|
||||||
<label>
|
<label>
|
||||||
@@ -572,6 +655,8 @@ export default function NewAppointmentModal({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</Step>
|
||||||
|
)}
|
||||||
|
|
||||||
{blockReason && (
|
{blockReason && (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -579,31 +664,78 @@ export default function NewAppointmentModal({
|
|||||||
fontSize: 12.5, color: 'var(--text-3)',
|
fontSize: 12.5, color: 'var(--text-3)',
|
||||||
}}>
|
}}>
|
||||||
<ExclamationCircleIcon style={{ width: 15, height: 15, flexShrink: 0 }} />
|
<ExclamationCircleIcon style={{ width: 15, height: 15, flexShrink: 0 }} />
|
||||||
برای ثبت نوبت: {blockReason}
|
{isLastStep ? 'برای ثبت نوبت' : 'برای مرحلهٔ بعد'}: {blockReason}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** مرحلهٔ شمارهدار فرم — ترتیب تصمیمها را دیدنی میکند، نه فقط ترتیب فیلدها. */
|
/** بدنهٔ یک مرحله — عنوانش در `Stepper` هم هست، اینجا سرِ همان بخش است. */
|
||||||
function Step({ n, title, children }: { n: number | null; title: string; children: ReactNode }) {
|
function Step({ title, children }: { title: string; children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<section style={{ marginBottom: 18 }}>
|
<section style={{ marginBottom: 18 }}>
|
||||||
<h3 style={{
|
<h3 style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 8, margin: '0 0 10px',
|
margin: '0 0 10px', fontSize: 13.5, fontWeight: 700, color: 'var(--text)',
|
||||||
fontSize: 13.5, fontWeight: 700, color: 'var(--text)',
|
|
||||||
}}>
|
}}>
|
||||||
{/* حالت اسلاتی فقط یک مرحله دارد؛ شمارهٔ «۱» تنها، نویز است نه راهنما. */}
|
|
||||||
{n !== null && (
|
|
||||||
<span style={{
|
|
||||||
display: 'grid', placeItems: 'center', width: 20, height: 20, borderRadius: 999,
|
|
||||||
background: 'var(--primary)', color: 'var(--on-primary)', fontSize: 11.5, flexShrink: 0,
|
|
||||||
}}>{n}</span>
|
|
||||||
)}
|
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
{children}
|
{children}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* نوار مراحل — «کجای کارم و چقدر مانده».
|
||||||
|
*
|
||||||
|
* فرم قبلاً همهٔ مراحل را با هم نشان میداد و مودال از ارتفاع صفحه بلندتر میشد؛
|
||||||
|
* حالا هر بار یک مرحله دیده میشود و این نوار تنها چیزی است که کلِ مسیر را میگوید.
|
||||||
|
*/
|
||||||
|
function Stepper({ keys, current }: { keys: StepKey[]; current: StepKey }) {
|
||||||
|
const activeIdx = keys.indexOf(current);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ol
|
||||||
|
aria-label="مراحل ثبت نوبت"
|
||||||
|
style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 8, listStyle: 'none',
|
||||||
|
margin: '0 0 18px', padding: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{keys.map((key, idx) => {
|
||||||
|
const done = idx < activeIdx;
|
||||||
|
const active = idx === activeIdx;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={key}
|
||||||
|
aria-current={active ? 'step' : undefined}
|
||||||
|
style={{ display: 'flex', alignItems: 'center', gap: 8, flex: idx === keys.length - 1 ? '0 0 auto' : 1 }}
|
||||||
|
>
|
||||||
|
<span style={{
|
||||||
|
display: 'grid', placeItems: 'center', width: 22, height: 22, borderRadius: 999,
|
||||||
|
flexShrink: 0, fontSize: 11.5, fontWeight: 700,
|
||||||
|
background: active || done ? 'var(--primary)' : 'var(--surface-3)',
|
||||||
|
color: active || done ? 'var(--on-primary)' : 'var(--text-3)',
|
||||||
|
}}>
|
||||||
|
{idx + 1}
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 12.5, whiteSpace: 'nowrap',
|
||||||
|
fontWeight: active ? 700 : 500,
|
||||||
|
color: active ? 'var(--text)' : 'var(--text-3)',
|
||||||
|
}}>
|
||||||
|
{STEP_TITLES[key]}
|
||||||
|
</span>
|
||||||
|
{idx < keys.length - 1 && (
|
||||||
|
<span style={{
|
||||||
|
flex: 1, height: 1, minWidth: 12,
|
||||||
|
background: done ? 'var(--primary)' : 'var(--border)',
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,6 +61,15 @@ async function pickServiceAndTime() {
|
|||||||
fireEvent.click(await screen.findByRole('button', { name: '12:00' }));
|
fireEvent.click(await screen.findByRole('button', { name: '12:00' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** فرم ویزارد شد: هر «مرحلهٔ بعد» یک گام جلو میبرد. */
|
||||||
|
const nextStep = () => fireEvent.click(screen.getByRole('button', { name: 'مرحلهٔ بعد' }));
|
||||||
|
|
||||||
|
/** خدمت و زمان را انتخاب میکند و به مرحلهٔ «بیمار» میرود. */
|
||||||
|
async function pickServiceAndTimeThenNext() {
|
||||||
|
await pickServiceAndTime();
|
||||||
|
nextStep();
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
get.mockReset();
|
get.mockReset();
|
||||||
post.mockReset();
|
post.mockReset();
|
||||||
@@ -90,12 +99,13 @@ describe('مودال ثبت نوبتِ منبع', () => {
|
|||||||
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
await pickServiceAndTime();
|
await pickServiceAndTimeThenNext();
|
||||||
|
|
||||||
fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } });
|
fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } });
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'جستجو' }));
|
fireEvent.click(screen.getByRole('button', { name: 'جستجو' }));
|
||||||
await screen.findByText('بیمار یافت شد');
|
await screen.findByText('بیمار یافت شد');
|
||||||
|
|
||||||
|
nextStep();
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
||||||
|
|
||||||
await waitFor(() => expect(post).toHaveBeenCalled());
|
await waitFor(() => expect(post).toHaveBeenCalled());
|
||||||
@@ -111,16 +121,14 @@ describe('مودال ثبت نوبتِ منبع', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('بدون انتخاب سرویس و زمان، ثبت غیرفعال میماند', async () => {
|
it('بدون انتخاب سرویس و زمان، از مرحلهٔ اول رد نمیشود', () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } });
|
// مرحلهٔ بیمار هنوز ساخته نشده، پس فیلد جستجو در DOM نیست.
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'جستجو' }));
|
expect(screen.queryByPlaceholderText('کد ملی ۱۰ رقمی')).toBeNull();
|
||||||
await screen.findByText('بیمار یافت شد');
|
expect(screen.getByRole('button', { name: 'مرحلهٔ بعد' })).toBeDisabled();
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: 'ثبت نوبت' })).toBeDisabled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('دلیلِ غیرفعال بودنِ ثبت را میگوید و با پیشرفتِ فرم عوض میشود', async () => {
|
it('دلیلِ غیرفعال بودنِ ثبت را میگوید و با پیشرفتِ فرم عوض میشود', async () => {
|
||||||
@@ -139,6 +147,7 @@ describe('مودال ثبت نوبتِ منبع', () => {
|
|||||||
expect(await screen.findByText(/ساعت شروع را انتخاب کنید/)).toBeInTheDocument();
|
expect(await screen.findByText(/ساعت شروع را انتخاب کنید/)).toBeInTheDocument();
|
||||||
|
|
||||||
fireEvent.click(await screen.findByRole('button', { name: '12:00' }));
|
fireEvent.click(await screen.findByRole('button', { name: '12:00' }));
|
||||||
|
nextStep();
|
||||||
expect(await screen.findByText(/ابتدا بیمار را جستجو کنید/)).toBeInTheDocument();
|
expect(await screen.findByText(/ابتدا بیمار را جستجو کنید/)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,11 +163,13 @@ describe('مودال ثبت نوبتِ منبع', () => {
|
|||||||
expect(screen.queryByText('تاریخ:')).not.toBeInTheDocument();
|
expect(screen.queryByText('تاریخ:')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('معیار جستجو یک seg با حالتِ اعلامشده است', () => {
|
it('معیار جستجو یک seg با حالتِ اعلامشده است', async () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await pickServiceAndTimeThenNext();
|
||||||
|
|
||||||
const national = screen.getByRole('button', { name: 'کد ملی' });
|
const national = screen.getByRole('button', { name: 'کد ملی' });
|
||||||
const mobile = screen.getByRole('button', { name: 'شماره موبایل' });
|
const mobile = screen.getByRole('button', { name: 'شماره موبایل' });
|
||||||
expect(national).toHaveAttribute('aria-pressed', 'true');
|
expect(national).toHaveAttribute('aria-pressed', 'true');
|
||||||
@@ -176,13 +187,15 @@ describe('مودال ثبت نوبتِ منبع', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await pickServiceAndTime();
|
await pickServiceAndTime();
|
||||||
|
// پرسنل در همان مرحلهٔ «خدمت و زمان» است.
|
||||||
|
await waitFor(() => expect(screen.getByLabelText(/پرسنل/)).toBeInTheDocument());
|
||||||
|
nextStep();
|
||||||
|
|
||||||
fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } });
|
fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } });
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'جستجو' }));
|
fireEvent.click(screen.getByRole('button', { name: 'جستجو' }));
|
||||||
await screen.findByText('بیمار یافت شد');
|
await screen.findByText('بیمار یافت شد');
|
||||||
|
|
||||||
await waitFor(() => expect(screen.getByLabelText(/پرسنل/)).toBeInTheDocument());
|
nextStep();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
||||||
await waitFor(() => expect(post).toHaveBeenCalled());
|
await waitFor(() => expect(post).toHaveBeenCalled());
|
||||||
expect(post.mock.calls[0][1]).toMatchObject({ staff_uuid: 'st-1' });
|
expect(post.mock.calls[0][1]).toMatchObject({ staff_uuid: 'st-1' });
|
||||||
@@ -208,12 +221,13 @@ describe('مودال ثبت نوبتِ منبع', () => {
|
|||||||
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
<NewAppointmentModal slot={slot} resource={resource} services={services} date="2026-08-04" onClose={() => {}} onSuccess={() => {}} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
await pickServiceAndTime();
|
await pickServiceAndTimeThenNext();
|
||||||
|
|
||||||
fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } });
|
fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } });
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'جستجو' }));
|
fireEvent.click(screen.getByRole('button', { name: 'جستجو' }));
|
||||||
await screen.findByText('بیمار یافت شد');
|
await screen.findByText('بیمار یافت شد');
|
||||||
|
|
||||||
|
nextStep();
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
||||||
await waitFor(() => expect(post).toHaveBeenCalled());
|
await waitFor(() => expect(post).toHaveBeenCalled());
|
||||||
expect(post.mock.calls[0][1]).not.toHaveProperty('staff_uuid');
|
expect(post.mock.calls[0][1]).not.toHaveProperty('staff_uuid');
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import Modal from './Modal';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* فرمهای بلند (ثبت نوبت) کلِ کارت را اسکرول میکردند، پس عنوان و دکمهٔ اصلی از دید
|
||||||
|
* خارج میشد. حالا هدر و فوتر بیرون از ناحیهٔ اسکرولاند و فقط بدنه اسکرول میشود.
|
||||||
|
*/
|
||||||
|
describe('Modal', () => {
|
||||||
|
const renderModal = (size?: 'sm' | 'md' | 'lg' | 'xl') =>
|
||||||
|
render(
|
||||||
|
<Modal open title="ثبت نوبت" size={size} onClose={vi.fn()} footer={<button>ثبت</button>}>
|
||||||
|
<p>محتوای بلند</p>
|
||||||
|
</Modal>,
|
||||||
|
);
|
||||||
|
|
||||||
|
it('هدر و فوتر بیرون از ناحیهٔ اسکرول میمانند', () => {
|
||||||
|
renderModal();
|
||||||
|
|
||||||
|
const body = document.querySelector('.modal-body');
|
||||||
|
expect(body).not.toBeNull();
|
||||||
|
expect(body!.textContent).toBe('محتوای بلند');
|
||||||
|
// نه عنوان و نه دکمه نباید داخل بدنه باشند، وگرنه با محتوا اسکرول میشوند.
|
||||||
|
expect(body!.querySelector('.modal-head')).toBeNull();
|
||||||
|
expect(body!.querySelector('.modal-foot')).toBeNull();
|
||||||
|
expect(screen.getByRole('button', { name: 'ثبت' }).closest('.modal-foot')).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('عرض از size میآید', () => {
|
||||||
|
const { unmount } = renderModal('sm');
|
||||||
|
expect((document.querySelector('.modal') as HTMLElement).style.maxWidth).toBe('420px');
|
||||||
|
unmount();
|
||||||
|
|
||||||
|
renderModal('lg');
|
||||||
|
expect((document.querySelector('.modal') as HTMLElement).style.maxWidth).toBe('720px');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('بسته، هیچ چیزی رندر نمیکند', () => {
|
||||||
|
render(<Modal open={false} title="ثبت نوبت" onClose={vi.fn()}><p>محتوا</p></Modal>);
|
||||||
|
|
||||||
|
expect(document.querySelector('.modal')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -16,6 +16,12 @@ const post = api.post as ReturnType<typeof vi.fn>;
|
|||||||
|
|
||||||
const slot = { start: 1_800_000_000, end: 1_800_001_800, start_time: '15:00', end_time: '15:30', doctor_uuid: 'doc1', doctor_name: 'دکتر تست' };
|
const slot = { start: 1_800_000_000, end: 1_800_001_800, start_time: '15:00', end_time: '15:30', doctor_uuid: 'doc1', doctor_name: 'دکتر تست' };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* فرم ویزارد شد: بیمار و «تأیید و ثبت» دو مرحلهٔ جدا هستند، پس ثبت یک کلیک جلوتر است.
|
||||||
|
* (نوبت اسلاتی مرحلهٔ «خدمت و زمان» ندارد؛ زمانش از خودِ اسلات میآید.)
|
||||||
|
*/
|
||||||
|
const goToConfirm = () => fireEvent.click(screen.getByRole('button', { name: 'مرحلهٔ بعد' }));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
get.mockReset();
|
get.mockReset();
|
||||||
post.mockReset();
|
post.mockReset();
|
||||||
@@ -28,8 +34,9 @@ describe('NewAppointmentModal — جستجوی موبایلمحور', () => {
|
|||||||
renderWithProviders(<NewAppointmentModal slot={slot} onClose={() => {}} onSuccess={() => {}} />);
|
renderWithProviders(<NewAppointmentModal slot={slot} onClose={() => {}} onSuccess={() => {}} />);
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'شماره موبایل' }));
|
fireEvent.click(screen.getByRole('button', { name: 'شماره موبایل' }));
|
||||||
fireEvent.change(screen.getByPlaceholderText('مثال: 09123456789'), { target: { value: '09121234567' } });
|
fireEvent.change(screen.getByPlaceholderText('مثال: 09123456789'), { target: { value: '09121234567' } });
|
||||||
// بدون جستجو، ثبت نوبت غیرفعال است
|
// بدون جستجو، رفتن به مرحلهٔ بعد ممکن نیست
|
||||||
expect(screen.getByRole('button', { name: 'ثبت نوبت' })).toBeDisabled();
|
expect(screen.getByRole('button', { name: 'مرحلهٔ بعد' })).toBeDisabled();
|
||||||
|
expect(screen.queryByRole('button', { name: 'ثبت نوبت' })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('converts Persian digits to English and searches by the normalized mobile', async () => {
|
it('converts Persian digits to English and searches by the normalized mobile', async () => {
|
||||||
@@ -70,6 +77,7 @@ describe('NewAppointmentModal — جستجوی موبایلمحور', () => {
|
|||||||
// فیلد کد ملی برای بیمارِ یافتشده نمایش داده نمیشود
|
// فیلد کد ملی برای بیمارِ یافتشده نمایش داده نمیشود
|
||||||
expect(screen.queryByPlaceholderText('کد ملی ۱۰ رقمی')).toBeNull();
|
expect(screen.queryByPlaceholderText('کد ملی ۱۰ رقمی')).toBeNull();
|
||||||
|
|
||||||
|
goToConfirm();
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
||||||
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/my/appointment', expect.objectContaining({
|
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/my/appointment', expect.objectContaining({
|
||||||
doctor_uuid: 'doc1',
|
doctor_uuid: 'doc1',
|
||||||
@@ -91,6 +99,7 @@ describe('NewAppointmentModal — جستجوی موبایلمحور', () => {
|
|||||||
fireEvent.change(screen.getByPlaceholderText('مثال: علی محمدی'), { target: { value: 'مریم خلیلی' } });
|
fireEvent.change(screen.getByPlaceholderText('مثال: علی محمدی'), { target: { value: 'مریم خلیلی' } });
|
||||||
fireEvent.change(nc, { target: { value: '1234567891' } });
|
fireEvent.change(nc, { target: { value: '1234567891' } });
|
||||||
|
|
||||||
|
goToConfirm();
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
||||||
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/my/appointment', expect.objectContaining({
|
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/my/appointment', expect.objectContaining({
|
||||||
patient_mobile: '09990001122',
|
patient_mobile: '09990001122',
|
||||||
@@ -113,6 +122,7 @@ describe('NewAppointmentModal — جستجو با کد ملی', () => {
|
|||||||
await waitFor(() => expect(get).toHaveBeenCalledWith('/api/v1/my/appointment/patient-lookup?national_code=0012345678'));
|
await waitFor(() => expect(get).toHaveBeenCalledWith('/api/v1/my/appointment/patient-lookup?national_code=0012345678'));
|
||||||
await screen.findByText('بیمار یافت شد');
|
await screen.findByText('بیمار یافت شد');
|
||||||
|
|
||||||
|
goToConfirm();
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
||||||
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/my/appointment', expect.objectContaining({
|
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/my/appointment', expect.objectContaining({
|
||||||
patient_mobile: '09121112233',
|
patient_mobile: '09121112233',
|
||||||
@@ -135,6 +145,7 @@ describe('NewAppointmentModal — جستجو با کد ملی', () => {
|
|||||||
fireEvent.change(screen.getByPlaceholderText('مثال: علی محمدی'), { target: { value: 'مریم خلیلی' } });
|
fireEvent.change(screen.getByPlaceholderText('مثال: علی محمدی'), { target: { value: 'مریم خلیلی' } });
|
||||||
fireEvent.change(mob, { target: { value: '09990001122' } });
|
fireEvent.change(mob, { target: { value: '09990001122' } });
|
||||||
|
|
||||||
|
goToConfirm();
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
fireEvent.click(screen.getByRole('button', { name: 'ثبت نوبت' }));
|
||||||
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/my/appointment', expect.objectContaining({
|
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/my/appointment', expect.objectContaining({
|
||||||
patient_mobile: '09990001122',
|
patient_mobile: '09990001122',
|
||||||
@@ -145,6 +156,17 @@ describe('NewAppointmentModal — جستجو با کد ملی', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('NewAppointmentModal — هزینه ویزیت', () => {
|
describe('NewAppointmentModal — هزینه ویزیت', () => {
|
||||||
|
/** بیمارِ یافتشده را رد میکند تا به مرحلهٔ «تأیید و ثبت» برسیم. */
|
||||||
|
async function bookableWithoutPatient() {
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'شماره موبایل' }));
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('مثال: 09123456789'), { target: { value: '09121234567' } });
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'جستجو' }));
|
||||||
|
await screen.findByPlaceholderText('مثال: علی محمدی');
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('مثال: علی محمدی'), { target: { value: 'علی محمدی' } });
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('کد ملی ۱۰ رقمی'), { target: { value: '0012345678' } });
|
||||||
|
goToConfirm();
|
||||||
|
}
|
||||||
|
|
||||||
/** تعرفه را برای همان پزشکِ اسلات برمیگرداند؛ بقیهٔ GETها بیمارِ ناشناس. */
|
/** تعرفه را برای همان پزشکِ اسلات برمیگرداند؛ بقیهٔ GETها بیمارِ ناشناس. */
|
||||||
function mockPricing(rials: number, requireVisit = false) {
|
function mockPricing(rials: number, requireVisit = false) {
|
||||||
get.mockImplementation((url: string) =>
|
get.mockImplementation((url: string) =>
|
||||||
@@ -159,6 +181,9 @@ describe('NewAppointmentModal — هزینه ویزیت', () => {
|
|||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(get).toHaveBeenCalledWith('/api/v1/insurance-pricing?doctor_uuid=doc1'));
|
expect(get).toHaveBeenCalledWith('/api/v1/insurance-pricing?doctor_uuid=doc1'));
|
||||||
|
// هزینه در مرحلهٔ تأیید است؛ بدون بیمار هم میشود جلو رفت چون دکمه فقط با
|
||||||
|
// نبودِ بیمار قفل است — پس اول بیمار را رد میکنیم.
|
||||||
|
await bookableWithoutPatient();
|
||||||
// اختیاری → کلپس بسته است؛ باز کردن آن تا فیلد دیده شود
|
// اختیاری → کلپس بسته است؛ باز کردن آن تا فیلد دیده شود
|
||||||
fireEvent.click(screen.getByRole('button', { name: /هزینه ویزیت/ }));
|
fireEvent.click(screen.getByRole('button', { name: /هزینه ویزیت/ }));
|
||||||
// ۲٬۱۲۰٬۰۰۰ ریال = ۲۱۲٬۰۰۰ تومان
|
// ۲٬۱۲۰٬۰۰۰ ریال = ۲۱۲٬۰۰۰ تومان
|
||||||
@@ -169,6 +194,7 @@ describe('NewAppointmentModal — هزینه ویزیت', () => {
|
|||||||
mockPricing(0);
|
mockPricing(0);
|
||||||
renderWithProviders(<NewAppointmentModal slot={slot} onClose={() => {}} onSuccess={() => {}} />);
|
renderWithProviders(<NewAppointmentModal slot={slot} onClose={() => {}} onSuccess={() => {}} />);
|
||||||
|
|
||||||
|
await bookableWithoutPatient();
|
||||||
// برچسب اختیاری همیشه در سرِ کلپس هست، ولی فیلد در ابتدا پنهان است
|
// برچسب اختیاری همیشه در سرِ کلپس هست، ولی فیلد در ابتدا پنهان است
|
||||||
expect(screen.getByText('(اختیاری)')).toBeInTheDocument();
|
expect(screen.getByText('(اختیاری)')).toBeInTheDocument();
|
||||||
expect(screen.queryByPlaceholderText('0')).toBeNull();
|
expect(screen.queryByPlaceholderText('0')).toBeNull();
|
||||||
@@ -182,6 +208,7 @@ describe('NewAppointmentModal — هزینه ویزیت', () => {
|
|||||||
mockPricing(0, true);
|
mockPricing(0, true);
|
||||||
renderWithProviders(<NewAppointmentModal slot={slot} onClose={() => {}} onSuccess={() => {}} />);
|
renderWithProviders(<NewAppointmentModal slot={slot} onClose={() => {}} onSuccess={() => {}} />);
|
||||||
|
|
||||||
|
await bookableWithoutPatient();
|
||||||
// الزامی → بدون کلیک، فیلد و خطا نمایش داده میشوند
|
// الزامی → بدون کلیک، فیلد و خطا نمایش داده میشوند
|
||||||
await screen.findByText('هزینه ویزیت الزامی است');
|
await screen.findByText('هزینه ویزیت الزامی است');
|
||||||
expect(screen.getByPlaceholderText('0')).toBeInTheDocument();
|
expect(screen.getByPlaceholderText('0')).toBeInTheDocument();
|
||||||
|
|||||||
Reference in New Issue
Block a user