- Added new icons and improved error messaging for better user feedback. - Refactored state management to include baseline comparison for dirty state detection. - Introduced functionality to copy shifts across all days and reset to baseline. - Updated UI layout for better responsiveness and usability. - Enhanced tests to cover new features and ensure proper functionality. refactor: update ClinicAppointmentSettingsPage to use PageHeader component - Replaced BackButton with PageHeader for a more consistent header layout. - Simplified the structure of the appointment settings page for better readability. test: improve ResourceDetailPage tests for shift management - Updated tests to reflect changes in shift display and error handling. - Added tests for new features including the reset functionality and copying shifts. style: add styles for weekly shift layout in ResourceWorkingHoursPanel - Introduced new CSS classes for better layout and responsiveness of the weekly shift display. - Ensured styles are consistent with the overall design system.
211 lines
9.7 KiB
TypeScript
211 lines
9.7 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { screen, waitFor } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
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 {},
|
|
}));
|
|
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
|
vi.mock('../hooks/usePermissions', () => ({ usePermissions: () => ({ can: () => true }) }));
|
|
|
|
import { Routes, Route } from 'react-router-dom';
|
|
import { api } from '../lib/api';
|
|
import ResourceDetailPage from './ResourceDetailPage';
|
|
|
|
const get = api.get as ReturnType<typeof vi.fn>;
|
|
|
|
const resource = {
|
|
uuid: 'r1',
|
|
name: 'لیزر دایود',
|
|
address_uuid: 'a1',
|
|
address_name: 'شعبهٔ مرکزی',
|
|
type_uuid: 't1',
|
|
type_code: 'device',
|
|
type_name: 'دستگاه لیزر',
|
|
capacity: 1,
|
|
setup_minutes: 5,
|
|
cleanup_minutes: 10,
|
|
attributes: {},
|
|
subject_kind: null,
|
|
subject_uuid: null,
|
|
skills: [{ skill_uuid: 's1', skill_name: 'کار با لیزر', level: 4 }],
|
|
categories: [{ uuid: 'c-hand', name: 'دست' }],
|
|
active: true,
|
|
created_at: 0,
|
|
updated_at: 0,
|
|
upcoming_appointments: 0,
|
|
};
|
|
|
|
const emptyDays = (): Record<string, unknown[]> =>
|
|
Object.fromEntries(Array.from({ length: 7 }, (_, d) => [String(d), [] as unknown[]]));
|
|
|
|
function mockApi(days: Record<string, unknown[]> = emptyDays(), availabilityDays: unknown[] = []) {
|
|
get.mockImplementation((path: string) => {
|
|
if (path === '/api/v1/resource/r1') return Promise.resolve({ success: true, data: resource });
|
|
if (path.endsWith('/calendar')) {
|
|
return Promise.resolve({
|
|
success: true,
|
|
data: { resource_uuid: 'r1', timezone: 'Asia/Tehran', defined: true, days },
|
|
});
|
|
}
|
|
if (path.includes('/availability')) {
|
|
return Promise.resolve({
|
|
success: true,
|
|
data: { resource_uuid: 'r1', timezone: 'Asia/Tehran', days: availabilityDays },
|
|
});
|
|
}
|
|
if (path.includes('/service-categories/tree')) {
|
|
return Promise.resolve({
|
|
success: true,
|
|
data: [{ uuid: 'c-hand', name: 'دست', sort_order: 0, active: true, children: [] },
|
|
{ uuid: 'c-foot', name: 'پا', sort_order: 1, active: true, children: [] }],
|
|
});
|
|
}
|
|
return Promise.resolve({ success: true, data: [] });
|
|
});
|
|
}
|
|
|
|
function renderPage(route = '/admin/resources/r1') {
|
|
return renderWithProviders(
|
|
<Routes>
|
|
<Route path="/admin/resources/:resourceUuid" element={<ResourceDetailPage />} />
|
|
</Routes>,
|
|
{ route },
|
|
);
|
|
}
|
|
|
|
describe('ResourceDetailPage', () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it('اطلاعات منبع را در تب پیشفرض نشان میدهد', async () => {
|
|
mockApi();
|
|
renderPage();
|
|
|
|
await waitFor(() => expect(screen.getByText('شعبهٔ مرکزی')).toBeInTheDocument());
|
|
expect(screen.getByText('ظرفیت همزمان')).toBeInTheDocument();
|
|
expect(screen.getByText('کار با لیزر · 4')).toBeInTheDocument();
|
|
});
|
|
|
|
it('تب از URL خوانده میشود تا بازگشت و رفرش همان نما را بدهد', async () => {
|
|
mockApi();
|
|
renderPage('/admin/resources/r1?tab=hours');
|
|
|
|
await waitFor(() => expect(screen.getByText('شنبه')).toBeInTheDocument());
|
|
expect(screen.getByText('جمعه')).toBeInTheDocument();
|
|
expect(screen.getAllByText('بدون شیفت — این روز بسته است')).toHaveLength(7);
|
|
});
|
|
|
|
it('شیفت ذخیرهشده را بهصورت ساعت نشان میدهد', async () => {
|
|
const days = emptyDays();
|
|
days['0'] = [{ sequence: 0, start_minute: 540, end_minute: 1020, start_time: '09:00', end_time: '17:00', active: true }];
|
|
mockApi(days);
|
|
renderPage('/admin/resources/r1?tab=hours');
|
|
|
|
await waitFor(() => expect(screen.getByDisplayValue('09:00')).toBeInTheDocument());
|
|
expect(screen.getByDisplayValue('17:00')).toBeInTheDocument();
|
|
});
|
|
|
|
/**
|
|
* دلیلِ خالی بودن روز باید فارسی نشان داده شود؛ نشان دادن کلید خام سرور
|
|
* («outside_branch_hours») به کاربر یعنی پیام بیمعنا.
|
|
*/
|
|
it('دلیل خالی بودن روز را فارسی میکند', async () => {
|
|
mockApi(emptyDays(), [
|
|
{ date: 1785529800, day_of_week: 0, intervals: [], total_minutes: 0, reasons: ['national_holiday'] },
|
|
{ date: 1785616200, day_of_week: 1, intervals: [], total_minutes: 0, reasons: ['no_shift'] },
|
|
{ date: 1785702600, day_of_week: 2, intervals: [], total_minutes: 0, reasons: ['exception'] },
|
|
]);
|
|
renderPage('/admin/resources/r1?tab=exceptions');
|
|
|
|
await waitFor(() => expect(screen.getByText('تعطیل رسمی')).toBeInTheDocument());
|
|
expect(screen.getByText('شیفتی تعریف نشده')).toBeInTheDocument();
|
|
expect(screen.getByText('مرخصی یا سرویس')).toBeInTheDocument();
|
|
expect(screen.queryByText('no_shift')).not.toBeInTheDocument();
|
|
});
|
|
|
|
/**
|
|
* وقتی هیچ روزی شیفت ندارد، ۱۴ سطرِ یکسان نویز است و با یک حالت خالی جمع میشود.
|
|
* شرط باید تنگ بماند: تست بالا ثابت میکند دلیلِ غیر از `no_shift` سطرها را نگه میدارد.
|
|
*/
|
|
it('پیشنمایشِ کاملاً تعریفنشده را در یک حالت خالی جمع میکند', async () => {
|
|
mockApi(emptyDays(), [
|
|
{ date: 1785529800, day_of_week: 0, intervals: [], total_minutes: 0, reasons: ['no_shift'] },
|
|
{ date: 1785616200, day_of_week: 1, intervals: [], total_minutes: 0, reasons: ['no_shift'] },
|
|
]);
|
|
renderPage('/admin/resources/r1?tab=exceptions');
|
|
|
|
await waitFor(() =>
|
|
expect(screen.getByText(/در دو هفتهٔ آینده هیچ ساعتی باز نیست/)).toBeInTheDocument());
|
|
expect(screen.queryByText('شیفتی تعریف نشده')).not.toBeInTheDocument();
|
|
});
|
|
|
|
/** نوار ذخیره فقط وقتی ظاهر میشود که چیزی تغییر کرده باشد — خودش نشانهٔ dirty است. */
|
|
it('نوار ذخیره تا وقتی تغییری نباشد نمیآید', async () => {
|
|
const user = userEvent.setup();
|
|
mockApi();
|
|
renderPage('/admin/resources/r1?tab=hours');
|
|
|
|
await waitFor(() => expect(screen.getByText('شنبه')).toBeInTheDocument());
|
|
expect(screen.queryByRole('button', { name: 'ذخیرهٔ شیفتها' })).not.toBeInTheDocument();
|
|
|
|
await user.click(screen.getAllByRole('button', { name: 'شیفت' })[0]);
|
|
|
|
expect(await screen.findByText('تغییرات شیفت ذخیره نشده است')).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: 'ذخیرهٔ شیفتها' })).toBeInTheDocument();
|
|
});
|
|
|
|
/** «بازگرداندن» باید به نسخهٔ سرور برگردد، نه صرفاً آخرین ویرایش را لغو کند. */
|
|
it('بازگرداندن، draft را به نسخهٔ سرور برمیگرداند', async () => {
|
|
const user = userEvent.setup();
|
|
mockApi();
|
|
renderPage('/admin/resources/r1?tab=hours');
|
|
|
|
await waitFor(() => expect(screen.getByText('شنبه')).toBeInTheDocument());
|
|
await user.click(screen.getAllByRole('button', { name: 'شیفت' })[0]);
|
|
await user.click(await screen.findByRole('button', { name: 'بازگرداندن' }));
|
|
|
|
await waitFor(() =>
|
|
expect(screen.queryByText('تغییرات شیفت ذخیره نشده است')).not.toBeInTheDocument());
|
|
expect(screen.getAllByText('بدون شیفت — این روز بسته است')).toHaveLength(7);
|
|
});
|
|
|
|
/** پرتکرارترین حالت یک ساعتِ یکسان برای کل هفته است؛ بدون کپی، ۱۵ تعامل لازم بود. */
|
|
it('شیفت یک روز را به همهٔ روزهای هفته کپی میکند', async () => {
|
|
const days = emptyDays();
|
|
days['0'] = [{ sequence: 0, start_minute: 540, end_minute: 1020, start_time: '09:00', end_time: '17:00', active: true }];
|
|
const user = userEvent.setup();
|
|
mockApi(days);
|
|
renderPage('/admin/resources/r1?tab=hours');
|
|
|
|
await waitFor(() => expect(screen.getByDisplayValue('09:00')).toBeInTheDocument());
|
|
await user.click(screen.getByRole('button', { name: 'کپی شیفتهای شنبه به همهٔ روزهای هفته' }));
|
|
|
|
await waitFor(() => expect(screen.getAllByDisplayValue('09:00')).toHaveLength(7));
|
|
expect(screen.getAllByDisplayValue('17:00')).toHaveLength(7);
|
|
expect(screen.queryByText('بدون شیفت — این روز بسته است')).not.toBeInTheDocument();
|
|
});
|
|
|
|
/** پیشنمایش نباید «وقت قابل رزرو» خوانده شود — نوبتها هنوز کسر نشدهاند. */
|
|
it('پیشنمایش را خام معرفی میکند', async () => {
|
|
mockApi();
|
|
renderPage('/admin/resources/r1?tab=exceptions');
|
|
|
|
await waitFor(() => expect(screen.getByText(/نوبتهای ثبتشده هنوز از آن کسر نشدهاند/)).toBeInTheDocument());
|
|
});
|
|
|
|
it('تب دستهبندی فقط انتخاب میدهد، نه ساخت', async () => {
|
|
mockApi();
|
|
const user = userEvent.setup();
|
|
renderPage();
|
|
|
|
await waitFor(() => expect(screen.getByText('شعبهٔ مرکزی')).toBeInTheDocument());
|
|
await user.click(screen.getByRole('button', { name: 'دستهبندیها' }));
|
|
|
|
await waitFor(() => expect(screen.getByText(/فقط انتخاب میشود/)).toBeInTheDocument());
|
|
expect(screen.queryByRole('button', { name: /افزودن دستهبندی جدید/ })).not.toBeInTheDocument();
|
|
expect(screen.getByText('تنظیمات ← دستهبندیها')).toHaveAttribute('href', '/admin/service-categories');
|
|
});
|
|
});
|