Resource tabs now sit under the selected doctor and list only the resources that doctor supervises, so moving between a doctor's own appointments and the devices under them is one row of tabs rather than a flat list of everything. The booking modal reads the doctor from the resource's supervisor instead of asking again. The doctor↔resource relation is defined once, on the resource, and repeating the question here would have made a second source of truth. A resource whose supervisor was removed is blocked with a message pointing at the fix rather than a silently disabled button. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
130 lines
6.3 KiB
TypeScript
130 lines
6.3 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('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
|
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 ResourceBookingModal from './ResourceBookingModal';
|
|
|
|
const get = api.get as ReturnType<typeof vi.fn>;
|
|
const post = api.post as ReturnType<typeof vi.fn>;
|
|
|
|
const resource = {
|
|
uuid: 'r-laser',
|
|
name: 'لیزر CO2',
|
|
address_uuid: 'addr-1',
|
|
supervisor: { uuid: 'd-1', name: 'دکتر مرادی' },
|
|
} as never;
|
|
|
|
/** یکی از این دو وقت مالِ همین منبع است و دیگری مالِ دستگاه دیگری. */
|
|
const slots = [
|
|
{ start: 1783859400, end: 1783861200, assignment: { device: [{ uuid: 'r-laser', name: 'لیزر CO2' }] } },
|
|
{ start: 1783863000, end: 1783864800, assignment: { device: [{ uuid: 'r-other', name: 'لیزر دیگر' }] } },
|
|
];
|
|
|
|
beforeEach(() => {
|
|
get.mockReset();
|
|
post.mockReset();
|
|
get.mockImplementation((url: string) => {
|
|
if (url.includes('/services')) {
|
|
return Promise.resolve({ success: true, data: [
|
|
{ service_uuid: 's-1', service_name: 'لیزر صورت', active: true, effective_duration_minutes: 30 },
|
|
{ service_uuid: 's-2', service_name: 'سرویس غیرفعال', active: false, effective_duration_minutes: 20 },
|
|
] });
|
|
}
|
|
if (url.includes('/my/clinic-doctors')) {
|
|
return Promise.resolve({ success: true, data: { data: [{ uuid: 'd-1', name: 'دکتر مرادی' }] } });
|
|
}
|
|
return Promise.resolve({ success: true, data: [] });
|
|
});
|
|
post.mockImplementation((url: string) => {
|
|
if (url.includes('appointment-availability')) {
|
|
return Promise.resolve({ success: true, data: { plan: { total_minutes: 30, segments: [] }, slots, reason: null } });
|
|
}
|
|
if (url.includes('appointment-hold')) {
|
|
return Promise.resolve({ success: true, data: { hold_uuid: 'h-1', assignment: {} } });
|
|
}
|
|
return Promise.resolve({ success: true, data: { uuid: 'appt-1' } });
|
|
});
|
|
});
|
|
|
|
describe('ResourceBookingModal', () => {
|
|
/** فقط سرویسهایی که خودِ منبع ارائه میدهد — سرور هم بقیه را با ۴۲۲ رد میکند. */
|
|
it('فقط سرویسهای فعالِ همین منبع را میآورد', async () => {
|
|
renderWithProviders(
|
|
<ResourceBookingModal resource={resource} onClose={vi.fn()} onBooked={vi.fn()} />,
|
|
);
|
|
|
|
expect(await screen.findByText('ثبت نوبت — لیزر CO2')).toBeInTheDocument();
|
|
const called = get.mock.calls.map((c: any[]) => c[0]);
|
|
expect(called.some((u: string) => u.includes('/api/v1/resource/r-laser/services'))).toBe(true);
|
|
});
|
|
|
|
/**
|
|
* هستهٔ منبعمحور بودن: موتور خدمتمحور جواب میدهد، پس وقتی که این منبع در آن
|
|
* پیشنهاد نشده باید حذف شود — نشان دادنش یعنی اپراتور چیزی بگیرد که ۴۰۹ میشود.
|
|
*/
|
|
it('فقط وقتهایی را نشان میدهد که موتور همین منبع را در آنها داده', async () => {
|
|
const user = userEvent.setup();
|
|
renderWithProviders(
|
|
<ResourceBookingModal resource={resource} onClose={vi.fn()} onBooked={vi.fn()} />,
|
|
);
|
|
|
|
await screen.findByText('ثبت نوبت — لیزر CO2');
|
|
await user.click(await screen.findByText('سرویس را انتخاب کنید'));
|
|
await user.click(await screen.findByText(/لیزر صورت/));
|
|
|
|
await waitFor(() => expect(screen.getByText(/زمانهای خالی این منبع/)).toBeInTheDocument());
|
|
|
|
// وقتِ منبعِ دیگر (۱۷۸۳۸۶۳۰۰۰) نباید دکمهای داشته باشد.
|
|
const buttons = screen.getAllByRole('button').map((b) => b.textContent ?? '');
|
|
const timeButtons = buttons.filter((t) => /\d{2}:\d{2}/.test(t));
|
|
expect(timeButtons).toHaveLength(1);
|
|
});
|
|
|
|
/**
|
|
* پزشک از ناظرِ خودِ منبع میآید، نه از یک انتخابگر دیگر — ارتباط پزشک↔منبع یک جا
|
|
* تعریف شده و تکرارش اینجا یعنی دو منبعِ حقیقت.
|
|
*/
|
|
it('پزشک را از ناظرِ منبع میگیرد و انتخابگر پزشک ندارد', async () => {
|
|
const user = userEvent.setup();
|
|
renderWithProviders(
|
|
<ResourceBookingModal resource={resource} onClose={vi.fn()} onBooked={vi.fn()} />,
|
|
);
|
|
|
|
await screen.findByText('ثبت نوبت — لیزر CO2');
|
|
expect(screen.getByText(/پزشک ناظر: دکتر مرادی/)).toBeInTheDocument();
|
|
expect(screen.queryByText('پزشک انجامدهنده')).toBeNull();
|
|
|
|
// بدون سرویس و زمان هنوز غیرفعال است.
|
|
expect(screen.getByRole('button', { name: 'ثبت نوبت' })).toBeDisabled();
|
|
|
|
await user.click(await screen.findByText('سرویس را انتخاب کنید'));
|
|
await user.click(await screen.findByText(/لیزر صورت/));
|
|
await waitFor(() => expect(screen.getByText(/زمانهای خالی این منبع/)).toBeInTheDocument());
|
|
|
|
const timeBtn = screen.getAllByRole('button').find((b) => /\d{2}:\d{2}/.test(b.textContent ?? ''));
|
|
await user.click(timeBtn!);
|
|
|
|
// سرویس + زمان + ناظر ⇒ حالا قابل ثبت است.
|
|
await waitFor(() => expect(screen.getByRole('button', { name: 'ثبت نوبت' })).toBeEnabled());
|
|
});
|
|
|
|
/** منبعی که ناظرش حذف شده نباید بیصدا غیرقابلثبت بماند. */
|
|
it('منبع بدون ناظر را با پیام روشن مسدود میکند', async () => {
|
|
const orphan = { uuid: 'r-x', name: 'لیزر یتیم', address_uuid: 'addr-1', supervisor: null } as never;
|
|
renderWithProviders(
|
|
<ResourceBookingModal resource={orphan} onClose={vi.fn()} onBooked={vi.fn()} />,
|
|
);
|
|
|
|
expect(await screen.findByText(/این منبع پزشک ناظر ندارد/)).toBeInTheDocument();
|
|
expect(screen.getByRole('button', { name: 'ثبت نوبت' })).toBeDisabled();
|
|
});
|
|
});
|