Files
clinicpro/assets/admin/components/AppointmentActions.test.tsx
T
hamed 684cf1f783 feat: implement useUrlState hook for managing URL-based state in admin pages
- Refactor multiple admin pages (BlogsPage, ClinicsPage, DoctorsPage, etc.) to utilize the new useUrlState hook for managing pagination, search, and filter states via URL.
- Ensure that the state persists in the URL, allowing users to return to the same state when navigating back from detail pages.
- Update relevant components to handle state changes appropriately and maintain clean URLs by removing default values.
- Add SlotPicker component for selecting appointment slots based on availability.
- Create tests for useUrlState to validate its functionality and ensure correct behavior when interacting with the URL.
- Update API documentation to reflect changes in appointment creation and slot selection processes.
2026-07-29 21:04:40 +03:30

161 lines
8.0 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 AppointmentActionsMenu from './AppointmentActions';
import type { Appointment } from '../types';
const get = api.get as ReturnType<typeof vi.fn>;
const patch = api.patch as ReturnType<typeof vi.fn>;
const post = api.post as ReturnType<typeof vi.fn>;
const navigate = vi.fn();
vi.mock('react-router-dom', async () => ({
...(await vi.importActual<typeof import('react-router-dom')>('react-router-dom')),
useNavigate: () => navigate,
}));
const appt: Appointment = {
uuid: 'ap1', patient_name: 'مریم خلیلی', patient_mobile: '09136549874',
doctor_uuid: 'd1', doctor_name: 'دکتر احمدی',
slot_start: 1735639200, slot_end: 1735641900, // 45 min
appointment_date: '2024-12-31', appointment_time: '09:00', end_time: '09:45',
status: 'confirmed', version: 3, created_at: '',
service_section: { uuid: 's1', name: 'زیبایی' },
service_item: { uuid: 'i1', name: 'لیزر توتال' },
staff: { uuid: 'st1', full_name: 'دکتر حمیدی' },
};
beforeEach(() => {
get.mockReset(); patch.mockReset();
get.mockImplementation((url: string) => {
if (url.startsWith('/api/v1/patients?search=')) return Promise.resolve({ success: true, data: [{ uuid: 'rec1' }] });
if (url === '/api/v1/patient/rec1/wallet') return Promise.resolve({ success: true, data: { balance_rials: 500000, recent_transactions: [] } });
return Promise.resolve({ success: true, data: [] });
});
patch.mockResolvedValue({ success: true, data: {} });
post.mockReset();
navigate.mockReset();
});
function openMenu() {
renderWithProviders(<AppointmentActionsMenu appointment={appt} queryKey={['appts']} />);
fireEvent.click(screen.getByRole('button', { name: 'عملیات' }));
}
describe('AppointmentActionsMenu (عملیات نوبت)', () => {
it('lists all six actions from the Figma menu', () => {
openMenu();
for (const label of ['ویرایش', 'ثبت سرویس', 'مشاهده', 'جا به جایی نوبت', 'انتقال به لیست رزرو', 'جایگزینی نوبت']) {
expect(screen.getByText(label)).toBeInTheDocument();
}
});
it('«ثبت سرویس» پروندهٔ موجود بیمار را باز می‌کند', async () => {
openMenu();
fireEvent.click(screen.getByText('ثبت سرویس'));
await waitFor(() => expect(navigate).toHaveBeenCalledWith('/admin/patients/rec1/session/new'));
// مسیر جستجو باید همان endpoint واقعی باشد، وگرنه ۴۰۴ می‌گیرد و «خطا در یافتن پرونده» می‌دهد.
expect(get).toHaveBeenCalledWith(expect.stringContaining('/api/v1/patients?search='));
expect(post).not.toHaveBeenCalled();
});
it('«ثبت سرویس» برای بیمارِ بدون پرونده، اول پرونده می‌سازد', async () => {
get.mockImplementation((url: string) => {
if (url.startsWith('/api/v1/patients?search=')) return Promise.resolve({ success: true, data: [] });
return Promise.resolve({ success: true, data: [] });
});
post.mockResolvedValue({ success: true, data: { uuid: 'rec-new' } });
openMenu();
fireEvent.click(screen.getByText('ثبت سرویس'));
await waitFor(() => expect(post).toHaveBeenCalledWith('/api/v1/patient', expect.objectContaining({
mobile: '09136549874',
name: 'مریم خلیلی',
})));
expect(navigate).toHaveBeenCalledWith('/admin/patients/rec-new/session/new');
});
it('info modal shows appointment details and patient wallet balance', async () => {
openMenu();
fireEvent.click(screen.getByText('مشاهده'));
expect(await screen.findByText('ساعت شروع:')).toBeInTheDocument();
expect(screen.getByText('۴۵ دقیقه')).toBeInTheDocument();
expect(screen.getByText('لیزر توتال')).toBeInTheDocument();
expect(screen.getByText('دکتر حمیدی')).toBeInTheDocument();
// wallet resolved through record search → balance shown (rial → toman)
expect(await screen.findByText(/تومان/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'مشاهده پرونده' })).toBeInTheDocument();
});
it('move modal patches new slot times', async () => {
openMenu();
fireEvent.click(screen.getByText('جا به جایی نوبت'));
expect(await screen.findByText('اعمال تغییرات')).toBeInTheDocument();
fireEvent.change(screen.getByLabelText('ساعت شروع'), { target: { value: '15:00' } });
fireEvent.change(screen.getByLabelText('ساعت پایان'), { target: { value: '16:00' } });
fireEvent.click(screen.getByText('اعمال تغییرات'));
await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/appointment/ap1', expect.objectContaining({
slot_start: Math.floor(new Date('2024-12-31T15:00').getTime() / 1000),
slot_end: Math.floor(new Date('2024-12-31T16:00').getTime() / 1000),
version: 3,
})));
});
it('transfer modal flips is_reserve with a day-level slot', async () => {
openMenu();
fireEvent.click(screen.getByText('انتقال به لیست رزرو'));
expect(await screen.findByText(/به لیست نوبت های رزرو شده منتقل می شود/)).toBeInTheDocument();
fireEvent.click(screen.getByText('انتقال و حذف از لیست'));
const day = Math.floor(new Date('2024-12-31T00:00').getTime() / 1000);
await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/appointment/ap1', expect.objectContaining({
is_reserve: true, slot_start: day, slot_end: day, version: 3,
})));
});
it('replace modal swaps the patient and keeps the slot locked', async () => {
openMenu();
fireEvent.click(screen.getByText('جایگزینی نوبت'));
expect(await screen.findByPlaceholderText('نام و نام خانوادگی')).toBeInTheDocument();
// the original slot is shown read-only
expect(screen.getByDisplayValue('2024-12-31')).toBeDisabled();
expect(screen.getByDisplayValue('09:00')).toBeDisabled();
// prefilled from the appointment's current specs (react-select single value)
expect(screen.getByText('قطعی شده')).toBeInTheDocument();
fireEvent.change(screen.getByPlaceholderText('نام و نام خانوادگی'), { target: { value: 'ساغر صابری' } });
fireEvent.change(screen.getByPlaceholderText('شماره تماس'), { target: { value: '09356619438' } });
fireEvent.click(screen.getByText('ثبت نوبت'));
await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/appointment/ap1', expect.objectContaining({
patient_name: 'ساغر صابری', patient_mobile: '09356619438',
service_section_uuid: 's1', service_item_uuid: 'i1', staff_uuid: 'st1',
version: 3,
})));
});
it('replace modal picks an existing patient from the record search', async () => {
get.mockImplementation((url: string) => {
if (url.startsWith('/api/v1/patients?search=')) return Promise.resolve({ success: true, data: [
{ uuid: 'rec9', user_name: 'پریسا همتی', user_mobile: '09120009999' },
] });
return Promise.resolve({ success: true, data: [] });
});
openMenu();
fireEvent.click(screen.getByText('جایگزینی نوبت'));
fireEvent.change(await screen.findByPlaceholderText('جستجوی نام، شماره تماس، شماره پرونده...'), { target: { value: 'پریسا' } });
fireEvent.click(await screen.findByText('پریسا همتی'));
fireEvent.click(screen.getByText('ثبت نوبت'));
await waitFor(() => expect(patch).toHaveBeenCalledWith('/api/v1/appointment/ap1', expect.objectContaining({
patient_name: 'پریسا همتی', patient_mobile: '09120009999',
})));
});
});