import { describe, it, expect, beforeEach, vi } from 'vitest'; import { screen, fireEvent, waitFor } 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 TreatmentCasesPage from './TreatmentCasesPage'; const get = api.get as ReturnType; function caseRow(over: Record = {}) { return { uuid: 'case-1', status: 'active', total_sessions: 3, completed_sessions: 1, opened_at: 1_786_000_000, closed_at: null, service: { uuid: 'svc-1', name: 'لیزر توتال' }, supervisor: { uuid: 'doc-1', name: 'پزشک مدیسا' }, patient: { record_uuid: 'rec-1', name: 'محمد رسولی', mobile: '09120001111', record_number: '۱۲' }, areas: [{ uuid: 'ca-1', name: 'دست', category_uuid: 'cat-1' }], assigned_staff: [], performed_by: [], ...over, }; } /** فقط اندپوینت فهرست را جواب می‌دهد؛ بقیه خالی. */ function mockList(rows: unknown[]) { get.mockImplementation((url: string) => { if (url.startsWith('/api/v1/treatment-cases')) return Promise.resolve({ success: true, data: rows }); return Promise.resolve({ success: true, data: [] }); }); } beforeEach(() => { get.mockReset(); }); describe('صفحهٔ پرونده‌های درمان', () => { it('نام بیمار سرتیتر کارت است، نه نام سرویس', async () => { mockList([caseRow()]); renderWithProviders(); await screen.findByText('محمد رسولی'); fireEvent.click(screen.getByLabelText('نمایش کارتی')); const name = await screen.findByText('محمد رسولی'); expect(name.tagName).toBe('STRONG'); expect(screen.getByText('لیزر توتال')).toBeInTheDocument(); }); /** جستجو باید به سرور برود، نه اینکه فهرست را در مرورگر فیلتر کند. */ it('عبارت جستجو را به‌صورت پارامتر q می‌فرستد', async () => { mockList([caseRow()]); renderWithProviders(); await screen.findByText('محمد رسولی'); fireEvent.change(screen.getByLabelText('جستجوی پرونده'), { target: { value: 'رسولی' } }); await waitFor( () => expect(get.mock.calls.some( (c: unknown[]) => typeof c[0] === 'string' && c[0].includes('q=%D8%B1%D8%B3%D9%88%D9%84%DB%8C'), )).toBe(true), { timeout: 2000 }, ); }); it('فیلتر وضعیت به‌صورت status می‌رود', async () => { mockList([caseRow()]); renderWithProviders(); await screen.findByText('محمد رسولی'); fireEvent.click(screen.getByRole('button', { name: 'رها شده' })); await waitFor(() => expect(get.mock.calls.some( (c: unknown[]) => typeof c[0] === 'string' && c[0].includes('status=abandoned'), )).toBe(true)); }); /** نتیجهٔ خالیِ جستجو با «هنوز پرونده‌ای ساخته نشده» یکی نیست. */ it('خالیِ جستجو پیام خودش را دارد', async () => { mockList([]); renderWithProviders(); await screen.findByText(/پرونده‌ای یافت نشد/); fireEvent.change(screen.getByLabelText('جستجوی پرونده'), { target: { value: 'هیچ' } }); expect(await screen.findByText(/برای «هیچ» پرونده‌ای پیدا نشد/, {}, { timeout: 2000 })).toBeInTheDocument(); }); it('خطا را از فهرست خالی جدا می‌کند', async () => { get.mockRejectedValue(new Error('boom')); renderWithProviders(); await waitFor(() => expect(screen.getByText(/خواندن پرونده‌ها ناموفق بود/)).toBeInTheDocument()); expect(screen.queryByText(/پرونده‌ای یافت نشد/)).not.toBeInTheDocument(); }); it('بازهٔ تاریخ به‌صورت from و to می‌رود', async () => { mockList([caseRow()]); renderWithProviders(); await screen.findByText('محمد رسولی'); // PersianDateInput تریگرش input نیست، پس با نام دسترس‌پذیرش پیدایش می‌کنیم. fireEvent.click(screen.getByLabelText('شروع از تاریخ')); expect(screen.getByLabelText('شروع تا تاریخ')).toBeInTheDocument(); }); it('تاریخ شروع ساعت هم دارد', async () => { mockList([caseRow({ opened_at: 1_786_000_000 })]); renderWithProviders(); await screen.findByText('محمد رسولی'); fireEvent.click(screen.getByLabelText('نمایش کارتی')); // formatDateTime ساعت را با «:» می‌آورد؛ formatDate نمی‌آورد. const start = await screen.findByText(/^شروع:/); expect(start.textContent).toMatch(/:/); }); /** «چه کسی انجامش داد» سؤالِ اولِ مدیر است وقتی پرونده را در فهرست می‌بیند. */ it('نام پرسنل انجام‌دهنده را روی کارت می‌آورد', async () => { mockList([caseRow({ performed_by: [{ uuid: 'st-1', name: 'پرسنل۱' }] })]); renderWithProviders(); await screen.findByText('محمد رسولی'); fireEvent.click(screen.getByLabelText('نمایش کارتی')); expect(await screen.findByText(/انجام‌دهنده: پرسنل۱/)).toBeInTheDocument(); }); /** تا وقتی جلسه‌ای انجام نشده، برنامه را نشان می‌دهیم نه هیچ. */ it('اگر هنوز انجام نشده، پرسنلِ اختصاص‌یافته را نشان می‌دهد', async () => { mockList([caseRow({ assigned_staff: [{ uuid: 'st-2', name: 'پرسنل۲' }] })]); renderWithProviders(); await screen.findByText('محمد رسولی'); fireEvent.click(screen.getByLabelText('نمایش کارتی')); expect(await screen.findByText(/پرسنل: پرسنل۲/)).toBeInTheDocument(); }); /** نمای پیش‌فرض جدول است — همان الگوی صفحهٔ پرونده‌ها، با سوییچ به کارتی. */ it('پیش‌فرض جدولی است و به کارتی سوییچ می‌شود', async () => { mockList([caseRow()]); renderWithProviders(); await screen.findByText('محمد رسولی'); expect(screen.getByRole('columnheader', { name: 'پرسنل' })).toBeInTheDocument(); fireEvent.click(screen.getByLabelText('نمایش کارتی')); expect(screen.queryByRole('columnheader', { name: 'پرسنل' })).not.toBeInTheDocument(); fireEvent.click(screen.getByLabelText('نمایش جدولی')); expect(screen.getByRole('columnheader', { name: 'پرسنل' })).toBeInTheDocument(); }); it('ستون پرسنل در جدول پر می‌شود', async () => { mockList([caseRow({ performed_by: [{ uuid: 'st-1', name: 'پرسنل۱' }] })]); renderWithProviders(); const cell = await screen.findByRole('cell', { name: 'پرسنل۱' }); expect(cell).toBeInTheDocument(); }); it('دکمهٔ ویرایش مودال را باز می‌کند', async () => { mockList([caseRow()]); renderWithProviders(); fireEvent.click(await screen.findByRole('button', { name: /ویرایش/ })); expect(await screen.findByText('ویرایش پروندهٔ درمان')).toBeInTheDocument(); }); });