- 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.
155 lines
7.4 KiB
TypeScript
155 lines
7.4 KiB
TypeScript
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');
|
||
});
|
||
});
|