Files
clinicpro/assets/admin/pages/ClinicAppointmentSettingsPage.test.tsx
hamed 348e1cf517 feat: enhance resource calendar validation and UI
- Implement real-time validation for overlapping shifts in the ResourceWorkingHoursPanel.
- Remove the copy shift functionality to simplify the UI and prevent confusion.
- Introduce ResourceExceptionsCard to manage resource exceptions, including leave and maintenance.
- Update ClinicAppointmentSettingsPage to utilize new components and improve tab navigation for resource management.
- Add comprehensive validation tests for resource calendar to ensure overlapping shifts are correctly handled.
- Update API documentation to reflect new validation error messages and rules.
2026-08-03 09:37:22 +03:30

155 lines
7.4 KiB
TypeScript
Raw Permalink 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, vi, beforeEach } from 'vitest';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '../test/utils';
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
vi.mock('../hooks/usePermissions', () => ({ usePermissions: () => ({ can: () => true }) }));
// ScheduleSection یک درختِ سنگین با کوئری‌های خودش است؛ اینجا فقط باید ثابت شود کدام
// scope رندر می‌شود، نه اینکه برنامهٔ پزشک درست کار می‌کند — آن تست خودش را دارد.
vi.mock('../components/schedule/ScheduleSection', () => ({
ScheduleSection: () => <div>برنامهٔ پزشک</div>,
}));
vi.mock('../components/FreeVisitPrice', () => ({ default: () => <div>قیمت ویزیت</div> }));
vi.mock('../components/resources/ResourceWorkingHoursPanel', () => ({
default: ({ resourceUuid }: { resourceUuid?: string }) => <div>ساعات کاری {resourceUuid}</div>,
DAY_LABELS: [],
}));
vi.mock('../components/resources/ResourceExceptionsCard', () => ({
default: ({ resourceUuid }: { resourceUuid?: string }) => <div>مرخصی {resourceUuid}</div>,
}));
vi.mock('../components/holidays/NationalHolidaysCard', () => ({
default: () => <div>تعطیلات رسمی سال</div>,
}));
let resources: Array<{ uuid: string; name: string; type_name: string }> = [];
vi.mock('../hooks/useResources', () => ({
useResources: () => ({ resources, loading: false }),
}));
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 ClinicAppointmentSettingsPage from './ClinicAppointmentSettingsPage';
const get = api.get as ReturnType<typeof vi.fn>;
const LASER = { uuid: 'r-1', name: 'لیزر دایود', type_name: 'دستگاه لیزر' };
const ROOM = { uuid: 'r-2', name: 'اتاق ۱', type_name: 'اتاق درمان' };
beforeEach(() => {
vi.clearAllMocks();
resources = [LASER, ROOM];
useAuthStore.setState({
primaryRole: 'clinic',
dbUuid: 'c-1',
context: { type: 'clinic', db_uuid: 'c-1' } as never,
availableContexts: [],
});
get.mockResolvedValue({ success: true, data: { data: [{ uuid: 'd-1', name: 'دکتر مرادی' }] } });
});
describe('ClinicAppointmentSettingsPage', () => {
it('پیش‌فرض روی پزشکان است', async () => {
renderWithProviders(<ClinicAppointmentSettingsPage />);
await waitFor(() => expect(screen.getByText('برنامهٔ پزشک')).toBeInTheDocument());
expect(screen.queryByText(/ساعات کاری r-/)).not.toBeInTheDocument();
});
/** منبع همان‌جایی مدیریت می‌شود که برنامهٔ پزشک — نه در یک صفحهٔ جدا. */
it('تب منابع، برنامهٔ کاری منبع را می‌آورد', async () => {
const user = userEvent.setup();
renderWithProviders(<ClinicAppointmentSettingsPage />);
await user.click(screen.getByRole('button', { name: 'منابع' }));
await waitFor(() => expect(screen.getByText('ساعات کاری r-1')).toBeInTheDocument());
expect(screen.queryByText('برنامهٔ پزشک')).not.toBeInTheDocument();
});
/**
* سه تب، نه سه بخشِ پشت‌سرهم: هر سه یک سؤال را جواب می‌دهند ولی هم‌زمان لازم
* نیستند و چیدنشان زیر هم صفحه را سه برابر بلند می‌کرد.
*/
it('شیفت، تعطیلات و مرخصی سه تب مجزا هستند', async () => {
const user = userEvent.setup();
renderWithProviders(<ClinicAppointmentSettingsPage />, {
route: '/admin/settings/appointment-settings?scope=resources',
});
// پیش‌فرض روی شیفت هفتگی است — کارِ اصلی صفحه.
await waitFor(() => expect(screen.getByText('ساعات کاری r-1')).toBeInTheDocument());
expect(screen.queryByText('تعطیلات رسمی سال')).not.toBeInTheDocument();
expect(screen.queryByText('مرخصی r-1')).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'تعطیلات رسمی' }));
expect(await screen.findByText('تعطیلات رسمی سال')).toBeInTheDocument();
expect(screen.queryByText('ساعات کاری r-1')).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'مرخصی و سرویس' }));
expect(await screen.findByText('مرخصی r-1')).toBeInTheDocument();
expect(screen.queryByText('تعطیلات رسمی سال')).not.toBeInTheDocument();
});
/** تب از URL خوانده می‌شود، پس رفرش و «بازگشت» همان نما را نگه می‌دارند. */
it('تب برنامهٔ کاری را از URL می‌خواند', async () => {
renderWithProviders(<ClinicAppointmentSettingsPage />, {
route: '/admin/settings/appointment-settings?scope=resources&calendarTab=exceptions',
});
expect(await screen.findByText('مرخصی r-1')).toBeInTheDocument();
expect(screen.queryByText('ساعات کاری r-1')).not.toBeInTheDocument();
});
/** پیش‌نمایش دو هفته در این صفحه کاربردی ندارد و فقط ارتفاع اضافه می‌کرد. */
it('پیش‌نمایش دو هفته را نشان نمی‌دهد', async () => {
const user = userEvent.setup();
renderWithProviders(<ClinicAppointmentSettingsPage />, {
route: '/admin/settings/appointment-settings?scope=resources',
});
await waitFor(() => expect(screen.getByText('ساعات کاری r-1')).toBeInTheDocument());
expect(screen.queryByText('پیش‌نمایش دو هفته')).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'مرخصی و سرویس' }));
await screen.findByText('مرخصی r-1');
expect(screen.queryByText('پیش‌نمایش دو هفته')).not.toBeInTheDocument();
});
it('بین منابع جابه‌جا می‌شود', async () => {
const user = userEvent.setup();
renderWithProviders(<ClinicAppointmentSettingsPage />);
await user.click(screen.getByRole('button', { name: 'منابع' }));
await user.click(await screen.findByRole('button', { name: 'اتاق ۱' }));
await waitFor(() => expect(screen.getByText('ساعات کاری r-2')).toBeInTheDocument());
});
/** scope در URL می‌نشیند، وگرنه «بازگشت» و رفرش کاربر را به تب پزشکان می‌پراند. */
it('scope را از URL می‌خواند', async () => {
renderWithProviders(<ClinicAppointmentSettingsPage />, {
route: '/admin/settings/appointment-settings?scope=resources',
});
await waitFor(() => expect(screen.getByText('ساعات کاری r-1')).toBeInTheDocument());
});
it('کلینیک بدون منبع، حالت خالی با راه‌حل می‌دهد', async () => {
resources = [];
const user = userEvent.setup();
renderWithProviders(<ClinicAppointmentSettingsPage />);
await user.click(screen.getByRole('button', { name: 'منابع' }));
expect(await screen.findByText('هنوز منبعی تعریف نشده است')).toBeInTheDocument();
expect(screen.getByText('تنظیمات ← منابع')).toHaveAttribute('href', '/admin/resources');
});
});