my/clinic-doctors now reports has_schedule per doctor, and the appointments page builds tabs from it. A doctor with no working days had a tab that could only ever show an empty timeline. The flag is resolved with one query for the whole list rather than one per doctor. Clinic owners now read this authenticated endpoint too instead of the public clinic doctor-list, which is where the flag lives; admin keeps the public list and, with no flag present, hides nobody. Also repairs fallout from making the resource supervisor mandatory: four test classes build resources through their own helpers and were failing with 422. The supervisorFor helper moved to ApiTestCase so all domains share one, rather than copying it per suite. Full backend suite is green again (1306 tests) — the previous commit only ran tests/Resource and missed this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
187 lines
11 KiB
TypeScript
187 lines
11 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||
import { screen } 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 { useAuthStore } from '../stores/authStore';
|
||
import AppointmentsPage from './AppointmentsPage';
|
||
|
||
const get = api.get as ReturnType<typeof vi.fn>;
|
||
|
||
beforeEach(() => {
|
||
get.mockReset();
|
||
useAuthStore.setState({ primaryRole: 'doctor', dbUuid: 'doc1', doctorUuid: 'doc1' } as any);
|
||
get.mockImplementation((url: string) => {
|
||
if (url.includes('today-stats')) return Promise.resolve({ success: true, data: { total: 7, completed: 3, waiting: 2, cancelled: 1 } });
|
||
if (url.includes('appointment-slots')) return Promise.resolve({ success: true, data: { sessions: [], empty_reason: 'holiday' } });
|
||
if (url.includes('/my/appointments')) return Promise.resolve({ success: true, data: [], meta: { totalRecords: 0, totalPages: 0, currentPage: 1 } });
|
||
return Promise.resolve({ success: true, data: [] });
|
||
});
|
||
});
|
||
|
||
describe('AppointmentsPage — طرح نوبتها', () => {
|
||
it('renders the title, the stats bar and the افزودن نوبت action', async () => {
|
||
renderWithProviders(<AppointmentsPage />);
|
||
expect(screen.getByText('نوبت ها')).toBeInTheDocument();
|
||
expect(screen.getByText('کل نوبت های امروز')).toBeInTheDocument();
|
||
expect(await screen.findByText('۷')).toBeInTheDocument(); // total from stats
|
||
expect(screen.getByText('افزودن نوبت')).toBeInTheDocument();
|
||
});
|
||
|
||
it('timeline view shows the holiday message when the doctor has no slots', async () => {
|
||
renderWithProviders(<AppointmentsPage />);
|
||
// نمای پیشفرض زمانبندی است و پزشک (نقش doctor) از قبل انتخاب شده
|
||
expect(await screen.findByText('این روز تعطیل است')).toBeInTheDocument();
|
||
});
|
||
|
||
it('honors the ?date= query param (returns to the same day after edit)', async () => {
|
||
renderWithProviders(<AppointmentsPage />, { route: '/admin/appointments?date=2024-06-01' });
|
||
await screen.findByText('این روز تعطیل است');
|
||
const usedDate = get.mock.calls.some((c: any[]) => typeof c[0] === 'string' && c[0].includes('date=2024-06-01'));
|
||
expect(usedDate).toBe(true);
|
||
});
|
||
|
||
it('independent doctor profile does NOT show doctor tabs', async () => {
|
||
renderWithProviders(<AppointmentsPage />);
|
||
await screen.findByText('این روز تعطیل است');
|
||
expect(screen.queryByText('همه')).toBeNull();
|
||
// برچسب فیلتر سرویس دیده میشود (مطابق طرح)
|
||
expect(screen.getByText('سرویس مورد نظر را انتخاب کنید...')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe('AppointmentsPage — پروفایل کلینیک چندپزشکه', () => {
|
||
beforeEach(() => {
|
||
get.mockReset();
|
||
useAuthStore.setState({ primaryRole: 'clinic', dbUuid: 'clinic1' } as any);
|
||
get.mockImplementation((url: string) => {
|
||
if (url.includes('today-stats')) return Promise.resolve({ success: true, data: { total: 5, completed: 1, waiting: 3, cancelled: 1 } });
|
||
// کلینیک هم از اندپوینت احرازشده میخواند؛ `has_schedule` مبنای ساختن تب است.
|
||
if (url.includes('/my/clinic-doctors') || url.includes('/clinic/doctor-list/')) {
|
||
return Promise.resolve({ success: true, data: { data: [
|
||
{ uuid: 'd1', name: 'دکتر محمدی', has_schedule: true },
|
||
{ uuid: 'd2', name: 'دکتر رضایی', has_schedule: true },
|
||
{ uuid: 'd3', name: 'دکتر بیبرنامه', has_schedule: false },
|
||
] } });
|
||
}
|
||
if (url.includes('appointment-slots')) return Promise.resolve({ success: true, data: { sessions: [], empty_reason: 'holiday' } });
|
||
if (url.includes('/my/appointments')) return Promise.resolve({ success: true, data: [], meta: { totalRecords: 0, totalPages: 0, currentPage: 1 } });
|
||
// منابع زیر نظر پزشکاند؛ تب هر منبع فقط زیر ناظرِ خودش دیده میشود.
|
||
if (url.includes('/api/v1/resources')) return Promise.resolve({ success: true, data: [
|
||
{ uuid: 'r-1', name: 'اتاق ۱', type_name: 'اتاق درمان', supervisor: { uuid: 'd1', name: 'دکتر محمدی' } },
|
||
{ uuid: 'r-2', name: 'لیزر CO2', type_name: 'دستگاه لیزر', supervisor: { uuid: 'd1', name: 'دکتر محمدی' } },
|
||
{ uuid: 'r-3', name: 'لیزر دکتر رضایی', type_name: 'دستگاه لیزر', supervisor: { uuid: 'd2', name: 'دکتر رضایی' } },
|
||
] });
|
||
return Promise.resolve({ success: true, data: [] });
|
||
});
|
||
});
|
||
|
||
it('shows the doctor tabs (multi-doctor management) without the «همه» tab', async () => {
|
||
renderWithProviders(<AppointmentsPage />);
|
||
expect(await screen.findByText('دکتر محمدی')).toBeInTheDocument();
|
||
expect(screen.getByText('دکتر رضایی')).toBeInTheDocument();
|
||
expect(screen.queryByText('همه')).toBeNull();
|
||
});
|
||
|
||
/**
|
||
* منابع مثل پزشکان تب خودشان را دارند: نوبتِ «لیزر CO2» به دستگاه تعلق دارد، نه به
|
||
* پزشکی که پشتش ایستاده.
|
||
*/
|
||
it('فقط منابعِ تحت نظارت پزشکِ انتخابشده تب میگیرند', async () => {
|
||
renderWithProviders(<AppointmentsPage />);
|
||
// پزشک اول (d1) خودکار انتخاب میشود، پس فقط منابع او دیده میشوند.
|
||
expect(await screen.findByText('لیزر CO2')).toBeInTheDocument();
|
||
expect(screen.getByText('اتاق ۱')).toBeInTheDocument();
|
||
expect(screen.queryByText('لیزر دکتر رضایی')).toBeNull();
|
||
});
|
||
|
||
/** تب فعال از URL خوانده میشود و فهرست با resource_uuid فیلتر میشود، نه doctor_uuid. */
|
||
it('تب منبع از URL خوانده میشود و فهرست را با resource_uuid میگیرد', async () => {
|
||
renderWithProviders(<AppointmentsPage />, { route: '/admin/appointments?resource=r-2' });
|
||
await screen.findByText('لیزر CO2');
|
||
|
||
const calls = get.mock.calls
|
||
.map((c: any[]) => c[0])
|
||
.filter((u: any) => typeof u === 'string' && u.includes('/my/appointments'));
|
||
|
||
expect(calls.some((u: string) => u.includes('resource_uuid=r-2'))).toBe(true);
|
||
expect(calls.some((u: string) => u.includes('doctor_uuid='))).toBe(false);
|
||
});
|
||
|
||
/** پزشکی که در تنظیمات نوبتدهی روز کاری تعریف نکرده تب نمیگیرد. */
|
||
it('پزشک بدون برنامهٔ کاری تب نمیگیرد', async () => {
|
||
renderWithProviders(<AppointmentsPage />);
|
||
expect(await screen.findByText('دکتر محمدی')).toBeInTheDocument();
|
||
expect(screen.getByText('دکتر رضایی')).toBeInTheDocument();
|
||
expect(screen.queryByText('دکتر بیبرنامه')).toBeNull();
|
||
});
|
||
|
||
it('auto-selects the first doctor so the timeline loads its slots', async () => {
|
||
renderWithProviders(<AppointmentsPage />);
|
||
await screen.findByText('دکتر محمدی');
|
||
// اسلاتها برای اولین دکتر (d1) درخواست میشوند
|
||
await screen.findByText('این روز تعطیل است');
|
||
const calledSlotsForD1 = get.mock.calls.some((c: any[]) => typeof c[0] === 'string' && c[0].includes('appointment-slots') && c[0].includes('doctor_uuid=d1'));
|
||
expect(calledSlotsForD1).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('AppointmentsPage — منشی', () => {
|
||
/**
|
||
* منشی در هر دو ساختار باید تایملاین ببیند. لیست پزشکانِ مجاز از اندپوینت
|
||
* احرازشدهٔ /my/clinic-doctors میآید (نه لیست عمومی کلینیک).
|
||
*/
|
||
function mockSecretary(doctors: { uuid: string; name: string }[], scope: string | null) {
|
||
useAuthStore.setState({
|
||
primaryRole: 'secretary',
|
||
dbUuid: scope === 'clinic' ? 'clinic1' : 'doc1',
|
||
context: scope ? { type: scope, scope } : null,
|
||
} as any);
|
||
get.mockImplementation((url?: string) => {
|
||
if (typeof url !== 'string') return Promise.resolve({ success: true, data: [] });
|
||
if (url.includes('today-stats')) return Promise.resolve({ success: true, data: { total: 11, completed: 2, waiting: 7, cancelled: 2 } });
|
||
if (url.includes('/my/clinic-doctors')) return Promise.resolve({ success: true, data: { data: doctors } });
|
||
if (url.includes('appointment-slots')) return Promise.resolve({ success: true, data: { sessions: [], empty_reason: 'holiday' } });
|
||
if (url.includes('/my/appointments')) return Promise.resolve({ success: true, data: [], meta: { totalRecords: 0, totalPages: 0, currentPage: 1 } });
|
||
return Promise.resolve({ success: true, data: [] });
|
||
});
|
||
}
|
||
|
||
beforeEach(() => get.mockReset());
|
||
|
||
it('منشیِ پزشک مستقل: همان پزشک خودکار انتخاب و تایملاین بارگذاری میشود', async () => {
|
||
mockSecretary([{ uuid: 'doc1', name: 'دکتر موسوی' }], null);
|
||
renderWithProviders(<AppointmentsPage />);
|
||
|
||
expect(await screen.findByText('این روز تعطیل است')).toBeInTheDocument();
|
||
expect(screen.queryByText('برای نمایش زمانبندی، ابتدا یک پزشک انتخاب کنید')).toBeNull();
|
||
const calledSlots = get.mock.calls.some((c: any[]) => typeof c[0] === 'string' && c[0].includes('appointment-slots') && c[0].includes('doctor_uuid=doc1'));
|
||
expect(calledSlots).toBe(true);
|
||
});
|
||
|
||
it('منشیِ کلینیک: تب پزشکانِ تخصیصیافته و انتخاب خودکار اولی', async () => {
|
||
mockSecretary([{ uuid: 'd1', name: 'دکتر محمدی' }, { uuid: 'd2', name: 'دکتر رضایی' }], 'clinic');
|
||
renderWithProviders(<AppointmentsPage />);
|
||
|
||
expect(await screen.findByText('دکتر محمدی')).toBeInTheDocument();
|
||
expect(screen.getByText('دکتر رضایی')).toBeInTheDocument();
|
||
await screen.findByText('این روز تعطیل است');
|
||
const calledSlotsForD1 = get.mock.calls.some((c: any[]) => typeof c[0] === 'string' && c[0].includes('appointment-slots') && c[0].includes('doctor_uuid=d1'));
|
||
expect(calledSlotsForD1).toBe(true);
|
||
});
|
||
|
||
it('منشی هرگز از لیست عمومی کلینیک نمیخواند', async () => {
|
||
mockSecretary([{ uuid: 'doc1', name: 'دکتر موسوی' }], null);
|
||
renderWithProviders(<AppointmentsPage />);
|
||
await screen.findByText('این روز تعطیل است');
|
||
|
||
const usedPublicList = get.mock.calls.some((c: any[]) => typeof c[0] === 'string' && c[0].includes('/clinic/doctor-list/'));
|
||
expect(usedPublicList).toBe(false);
|
||
});
|
||
});
|