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 {}, })); vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } })); import { Routes, Route } from 'react-router-dom'; import { api } from '../lib/api'; import BranchWorkingHoursPage from './BranchWorkingHoursPage'; const get = api.get as ReturnType; const put = api.put as ReturnType; const branch = { id: '1', uuid: 'b1', type: 'clinic', clinic_id: 3, clinic_name: 'کلینیک ما', name: 'شعبهٔ مرکزی', map: { latitude: null, longitude: null }, address: 'خیابان اول', telephone: '03511111111', active: true, timezone: 'Asia/Tehran', city: null, province: null, working_hours_defined: true, rooms_count: 0, }; function emptyDays(): Record { return Object.fromEntries(Array.from({ length: 7 }, (_, d) => [String(d), []])); } function mockApi(days: Record) { get.mockImplementation((path: string) => { if (path === '/api/v1/branches') return Promise.resolve({ success: true, data: [branch] }); if (path.endsWith('/working-hours')) { return Promise.resolve({ success: true, data: { branch_uuid: 'b1', timezone: 'Asia/Tehran', defined: true, days }, }); } return Promise.resolve({ success: true, data: null }); }); put.mockResolvedValue({ success: true, data: { branch_uuid: 'b1', timezone: 'Asia/Tehran', defined: true, days } }); } function renderPage() { return renderWithProviders( } /> , { route: '/admin/branches/b1/working-hours' }, ); } describe('BranchWorkingHoursPage', () => { beforeEach(() => { vi.clearAllMocks(); }); it('renders all seven days and marks the empty ones closed', async () => { mockApi(emptyDays()); renderPage(); await waitFor(() => expect(screen.getByText('شنبه')).toBeInTheDocument()); expect(screen.getByText('جمعه')).toBeInTheDocument(); expect(screen.getAllByText('بسته')).toHaveLength(7); }); it('shows stored ranges as times, converting minutes from midnight', async () => { const days = emptyDays(); days['0'] = [{ sequence: 0, start_minute: 540, end_minute: 780, start_time: '09:00', end_time: '13:00', active: true }]; mockApi(days); renderPage(); await waitFor(() => expect(screen.getByDisplayValue('09:00')).toBeInTheDocument()); expect(screen.getByDisplayValue('13:00')).toBeInTheDocument(); }); /** * `` سقفش ۲۳:۵۹ است، پس ۱۴۴۰ با پرچم «تا پایان روز» نمایش داده * می‌شود و همان ۱۴۴۰ برمی‌گردد — وگرنه اولین ذخیره بازهٔ شبانه‌روزی را خراب می‌کرد. */ it('keeps an all-day range at 1440 through a round trip', async () => { const days = emptyDays(); days['3'] = [{ sequence: 0, start_minute: 0, end_minute: 1440, start_time: '00:00', end_time: '24:00', active: true }]; mockApi(days); renderPage(); await waitFor(() => expect(screen.getByText('۲۴:۰۰')).toBeInTheDocument()); expect((screen.getByLabelText('تا پایان روز') as HTMLInputElement).checked).toBe(true); fireEvent.click(screen.getByText('ذخیرهٔ هفته')); await waitFor(() => expect(put).toHaveBeenCalled()); expect(put.mock.calls[0][1].days['3']).toEqual([{ start_minute: 0, end_minute: 1440 }]); }); it('turns a normal range into an all-day one when the flag is checked', async () => { const days = emptyDays(); days['6'] = [{ sequence: 0, start_minute: 540, end_minute: 660, start_time: '09:00', end_time: '11:00', active: true }]; mockApi(days); renderPage(); await waitFor(() => expect(screen.getByDisplayValue('11:00')).toBeInTheDocument()); fireEvent.click(screen.getByLabelText('تا پایان روز')); fireEvent.click(screen.getByText('ذخیرهٔ هفته')); await waitFor(() => expect(put).toHaveBeenCalled()); expect(put.mock.calls[0][1].days['6']).toEqual([{ start_minute: 540, end_minute: 1440 }]); }); it('sends minutes, not time strings, on save', async () => { const days = emptyDays(); days['1'] = [{ sequence: 0, start_minute: 600, end_minute: 720, start_time: '10:00', end_time: '12:00', active: true }]; mockApi(days); renderPage(); await waitFor(() => expect(screen.getByDisplayValue('10:00')).toBeInTheDocument()); fireEvent.click(screen.getByText('ذخیرهٔ هفته')); await waitFor(() => expect(put).toHaveBeenCalled()); const [path, body] = put.mock.calls[0]; expect(path).toBe('/api/v1/branch/b1/working-hours'); expect(body.days['1']).toEqual([{ start_minute: 600, end_minute: 720 }]); // هر هفت روز فرستاده می‌شود، چون PUT جایگزینی کامل است نه merge تفاضلی. expect(Object.keys(body.days)).toHaveLength(7); }); it('blocks a save whose end is not after its start, without calling the API', async () => { const days = emptyDays(); days['2'] = [{ sequence: 0, start_minute: 600, end_minute: 720, start_time: '10:00', end_time: '12:00', active: true }]; mockApi(days); renderPage(); await waitFor(() => expect(screen.getByDisplayValue('12:00')).toBeInTheDocument()); fireEvent.change(screen.getByDisplayValue('12:00'), { target: { value: '09:00' } }); fireEvent.click(screen.getByText('ذخیرهٔ هفته')); await waitFor(() => expect(screen.getByText(/پایان بازه باید بعد از شروع/)).toBeInTheDocument()); expect(put).not.toHaveBeenCalled(); }); it('copies one day onto the whole week', async () => { const days = emptyDays(); days['0'] = [{ sequence: 0, start_minute: 480, end_minute: 600, start_time: '08:00', end_time: '10:00', active: true }]; mockApi(days); renderPage(); await waitFor(() => expect(screen.getByDisplayValue('08:00')).toBeInTheDocument()); fireEvent.click(screen.getByText('اعمال روی همهٔ روزها')); expect(screen.getAllByDisplayValue('08:00')).toHaveLength(7); expect(screen.queryByText('بسته')).not.toBeInTheDocument(); }); it('removes a range so the day becomes closed', async () => { const days = emptyDays(); days['4'] = [{ sequence: 0, start_minute: 540, end_minute: 660, start_time: '09:00', end_time: '11:00', active: true }]; mockApi(days); renderPage(); await waitFor(() => expect(screen.getByDisplayValue('11:00')).toBeInTheDocument()); fireEvent.click(screen.getByLabelText('حذف بازه')); expect(screen.getAllByText('بسته')).toHaveLength(7); }); });