Files
clinicpro/assets/admin/pages/ClinicAppointmentSettingsPage.test.tsx
T
hamedandClaude Opus 5 9ba4c8d948 feat(settings): manage resource schedules beside the doctors'
A resource carries its own working hours and holidays in the resource-first
model, so it belongs on the same settings page as a doctor's schedule rather
than on a page of its own. The page now has a scope switch — doctors or
resources — with the per-item tab bar below it, and both scopes reuse the
panels that already existed: ScheduleSection for a doctor, the working-hours
and exceptions panels for a resource. The selection lives in the query
string, so back and refresh return to the same tab.

The screenshot of the finished tab caught two real defects, both fixed here:

Dates in the resource panels and the holidays page read as year 57932.
formatDate already multiplies seconds by 1000, and five call sites passed
`x * 1000` on top of it. This predates the tab — the code was inherited from
the old calendar page — but it was invisible until a two-week preview was put
on screen.

The working-hours panel still told the user their hours were intersected with
the branch's. Branches are gone; the shift is the only source now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 15:49:01 +03:30

104 lines
4.7 KiB
TypeScript
Raw 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/ResourceExceptionsPanel', () => ({
default: ({ resourceUuid }: { resourceUuid?: string }) => <div>تعطیلات {resourceUuid}</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.getByText('تعطیلات r-1')).toBeInTheDocument();
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');
});
});