import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { SWRConfig } from 'swr'; const { mockListRegimens, mockCreateRegimen, mockUpdateRegimen, mockDeleteRegimen, mockGetBurnRates, } = vi.hoisted(() => ({ mockListRegimens: vi.fn(), mockCreateRegimen: vi.fn(), mockUpdateRegimen: vi.fn(), mockDeleteRegimen: vi.fn(), mockGetBurnRates: vi.fn(), })); const { mockListMedicines } = vi.hoisted(() => ({ mockListMedicines: vi.fn() })); vi.mock('@/services/regimens', () => ({ listRegimens: mockListRegimens, createRegimen: mockCreateRegimen, updateRegimen: mockUpdateRegimen, deleteRegimen: mockDeleteRegimen, getBurnRates: mockGetBurnRates, })); vi.mock('@/services/medicines', () => ({ listMedicines: mockListMedicines })); import { RegimensTab } from '../../../../src/app/(dashboard)/medicines/RegimensTab'; const emptyMeds = { data: [], pagination: { cursor: null, hasMore: false } }; const emptyRegimens = { data: [], pagination: { cursor: null, hasMore: false } }; const regimen = { _id: 'reg-1', name: 'Morning Routine', isActive: true, medications: [ { medicineId: 'med-1', medicineName: 'Metformin', medicineStrength: 500, medicineStrengthUnit: 'mg', medicineForm: 'tablet', dosage: 1, dosageUnit: 'tablet', frequency: 'once_daily', }, ], createdAt: '2026-01-01T00:00:00.000Z', }; beforeEach(() => { vi.clearAllMocks(); mockListRegimens.mockResolvedValue(emptyRegimens); mockListMedicines.mockResolvedValue(emptyMeds); mockGetBurnRates.mockResolvedValue({ data: [] }); }); const wrapper = ({ children }: { children: React.ReactNode }) => ( new Map(), dedupingInterval: 0 }}>{children} ); describe('RegimensTab', () => { it('shows empty state when no regimens', async () => { render(, { wrapper }); await waitFor(() => expect(screen.getByText(/No regimens yet/)).toBeInTheDocument()); }); it('renders regimen list', async () => { mockListRegimens.mockResolvedValue({ data: [regimen], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => expect(screen.getByText('Morning Routine')).toBeInTheDocument()); }); it('shows error when list fails', async () => { mockListRegimens.mockRejectedValue(new Error('Load failed')); render(, { wrapper }); await waitFor(() => expect(screen.getByText('Load failed')).toBeInTheDocument()); }); it('dismisses error', async () => { mockListRegimens.mockRejectedValue(new Error('Load failed')); render(, { wrapper }); await waitFor(() => screen.getByText('Load failed')); await userEvent.click(screen.getByText('Dismiss')); expect(screen.queryByText('Load failed')).not.toBeInTheDocument(); }); it('toggles new regimen form', async () => { render(, { wrapper }); await waitFor(() => screen.getByText('New Regimen')); await userEvent.click(screen.getByText('New Regimen')); expect(screen.getByText(/New Regimen/)).toBeInTheDocument(); expect(screen.getByPlaceholderText('e.g. Morning routine')).toBeInTheDocument(); await userEvent.click(screen.getAllByText('Cancel')[0]); expect(screen.queryByPlaceholderText('e.g. Morning routine')).not.toBeInTheDocument(); }); it('creates a regimen when form is submitted', async () => { mockCreateRegimen.mockResolvedValue(regimen); render(, { wrapper }); await waitFor(() => screen.getByText('New Regimen')); await userEvent.click(screen.getAllByText('New Regimen')[0]); await waitFor(() => screen.getByPlaceholderText('e.g. Morning routine')); fireEvent.change(screen.getByPlaceholderText('e.g. Morning routine'), { target: { value: 'Evening Routine' }, }); fireEvent.submit(screen.getByPlaceholderText('e.g. Morning routine').closest('form')!); await waitFor(() => expect(mockCreateRegimen).toHaveBeenCalledWith( 'hh1', expect.objectContaining({ name: 'Evening Routine' }), ), ); }); it('deletes regimen after confirmation', async () => { mockListRegimens.mockResolvedValue({ data: [regimen], pagination: { cursor: null, hasMore: false }, }); mockDeleteRegimen.mockResolvedValue({}); vi.spyOn(window, 'confirm').mockReturnValue(true); render(, { wrapper }); await waitFor(() => screen.getByText('Morning Routine')); await userEvent.click(screen.getByTitle('Delete')); expect(mockDeleteRegimen).toHaveBeenCalledWith('hh1', 'reg-1'); }); it('shows burn rate section when toggled', async () => { mockGetBurnRates.mockResolvedValue({ data: [] }); render(, { wrapper }); await waitFor(() => screen.getByText('Burn rate')); await userEvent.click(screen.getByText('Burn rate')); await waitFor(() => expect(screen.getByText('Burn Rate & Spending Projections')).toBeInTheDocument(), ); expect(mockGetBurnRates).toHaveBeenCalledWith('hh1'); }); it('opens edit form for regimen', async () => { mockListRegimens.mockResolvedValue({ data: [regimen], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => screen.getByText('Morning Routine')); await userEvent.click(screen.getByTitle('Edit')); expect(screen.getByDisplayValue('Morning Routine')).toBeInTheDocument(); }); it('saves edited regimen', async () => { mockListRegimens.mockResolvedValue({ data: [regimen], pagination: { cursor: null, hasMore: false }, }); mockUpdateRegimen.mockResolvedValue({ ...regimen, name: 'Evening Routine' }); render(, { wrapper }); await waitFor(() => screen.getByText('Morning Routine')); await userEvent.click(screen.getByTitle('Edit')); await waitFor(() => screen.getByDisplayValue('Morning Routine')); fireEvent.change(screen.getByDisplayValue('Morning Routine'), { target: { value: 'Evening Routine' }, }); fireEvent.submit(screen.getByDisplayValue('Evening Routine').closest('form')!); await waitFor(() => expect(mockUpdateRegimen).toHaveBeenCalledWith( 'hh1', 'reg-1', expect.objectContaining({ name: 'Evening Routine' }), ), ); }); it('shows validation error when submitting regimen form with no medications', async () => { render(, { wrapper }); await waitFor(() => screen.getByText('New Regimen')); await userEvent.click(screen.getAllByText('New Regimen')[0]); await waitFor(() => screen.getByPlaceholderText('e.g. Morning routine')); fireEvent.change(screen.getByPlaceholderText('e.g. Morning routine'), { target: { value: 'My Regimen' }, }); // Remove the default medication await userEvent.click(screen.getByTitle('Remove medication')); // Submit with no medications fireEvent.submit(screen.getByPlaceholderText('e.g. Morning routine').closest('form')!); await waitFor(() => expect(screen.getByText('At least one medication is required.')).toBeInTheDocument(), ); }); it('shows error when delete fails', async () => { mockListRegimens.mockResolvedValue({ data: [regimen], pagination: { cursor: null, hasMore: false }, }); mockDeleteRegimen.mockRejectedValue(new Error('Delete failed')); vi.spyOn(window, 'confirm').mockReturnValue(true); render(, { wrapper }); await waitFor(() => screen.getByText('Morning Routine')); await userEvent.click(screen.getByTitle('Delete')); await waitFor(() => expect(screen.getByText('Delete failed')).toBeInTheDocument()); }); it('shows burn rate table with data', async () => { mockGetBurnRates.mockResolvedValue({ data: [ { medicineId: 'med-1', medicineName: 'Metformin', dailyConsumption: 2.0, totalInCabinet: 60, daysUntilEmpty: 30, earliestExpiry: null, avgUnitPrice: null, projectedDailyCost: null, projectedMonthlyCost: null, projectedYearlyCost: null, currency: null, }, ], }); render(, { wrapper }); await waitFor(() => screen.getByText('Burn rate')); await userEvent.click(screen.getByText('Burn rate')); await waitFor(() => expect(screen.getByText('Metformin')).toBeInTheDocument()); expect(screen.getByText('2.00')).toBeInTheDocument(); }); it('cancels edit form and hides it', async () => { mockListRegimens.mockResolvedValue({ data: [regimen], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => screen.getByText('Morning Routine')); await userEvent.click(screen.getByTitle('Edit')); await waitFor(() => screen.getByDisplayValue('Morning Routine')); await userEvent.click(screen.getByRole('button', { name: 'Cancel' })); expect(screen.queryByDisplayValue('Morning Routine')).not.toBeInTheDocument(); }); it('filters regimens by active status', async () => { mockListRegimens.mockResolvedValue({ data: [regimen], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => screen.getByText('All regimens')); fireEvent.change(screen.getByDisplayValue('All regimens'), { target: { value: 'active' } }); await waitFor(() => expect(mockListRegimens).toHaveBeenCalledTimes(2)); }); it('adds a medication in the regimen form', async () => { render(, { wrapper }); await waitFor(() => screen.getByText('New Regimen')); await userEvent.click(screen.getAllByText('New Regimen')[0]); await waitFor(() => screen.getByText('+ Add medication')); await userEvent.click(screen.getByText('+ Add medication')); // Now 2 medications — verify the button still shows expect(screen.getByText('+ Add medication')).toBeInTheDocument(); }); it('toggles isActive checkbox in regimen form', async () => { render(, { wrapper }); await waitFor(() => screen.getByText('New Regimen')); await userEvent.click(screen.getAllByText('New Regimen')[0]); await waitFor(() => screen.getByLabelText('Active')); expect(screen.getByLabelText('Active')).toBeChecked(); fireEvent.click(screen.getByLabelText('Active')); expect(screen.getByLabelText('Active')).not.toBeChecked(); }); it('changes medicine, dosage, and unit in medication row', async () => { mockListMedicines.mockResolvedValue({ data: [ { _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' }, ], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => screen.getByText('New Regimen')); await userEvent.click(screen.getAllByText('New Regimen')[0]); await waitFor(() => screen.getByText('Select medicine...')); // Select a medicine in the medication row const medicineSelect = screen .getAllByRole('combobox') .find( (s) => (s as HTMLSelectElement).options[0]?.text === 'Select medicine...', ) as HTMLSelectElement; expect(medicineSelect).toBeDefined(); fireEvent.change(medicineSelect!, { target: { value: 'med-1' } }); // Change dosage const dosageInput = document.querySelector('input[min="0.01"]') as HTMLElement; if (dosageInput) fireEvent.change(dosageInput, { target: { value: '2' } }); // Change dosage unit const unitSelect = screen .getAllByRole('combobox') .find((s) => (s as HTMLSelectElement).value === 'tablet') as HTMLSelectElement; if (unitSelect) fireEvent.change(unitSelect!, { target: { value: 'capsule' } }); expect(screen.getByPlaceholderText('e.g. Morning routine')).toBeInTheDocument(); }); it('changes instructions field in medication row', async () => { render(, { wrapper }); await waitFor(() => screen.getByText('New Regimen')); await userEvent.click(screen.getAllByText('New Regimen')[0]); await waitFor(() => screen.getByPlaceholderText('e.g. Take with food')); fireEvent.change(screen.getByPlaceholderText('e.g. Take with food'), { target: { value: 'With meals' }, }); expect(screen.getByDisplayValue('With meals')).toBeInTheDocument(); }); it('changes frequency to custom and sets times per day', async () => { render(, { wrapper }); await waitFor(() => screen.getByText('New Regimen')); await userEvent.click(screen.getAllByText('New Regimen')[0]); await waitFor(() => screen.getByPlaceholderText('e.g. Morning routine')); // The frequency select has 'daily' as its first option value const frequencySelect = screen .getAllByRole('combobox') .find((s) => (s as HTMLSelectElement).options[0]?.value === 'daily') as HTMLSelectElement; expect(frequencySelect).toBeDefined(); fireEvent.change(frequencySelect!, { target: { value: 'custom' } }); // customFrequencyPerDay input should appear await waitFor(() => expect(document.querySelector('input[min="1"][step="1"]')).not.toBeNull()); const timesPerDayInput = document.querySelector('input[min="1"][step="1"]') as HTMLElement; fireEvent.change(timesPerDayInput, { target: { value: '3' } }); // Change back to non-custom (covers the undefined branch) fireEvent.change(frequencySelect!, { target: { value: 'twice_daily' } }); expect(screen.getByPlaceholderText('e.g. Morning routine')).toBeInTheDocument(); }); it('changes time of day in medication row', async () => { render(, { wrapper }); await waitFor(() => screen.getByText('New Regimen')); await userEvent.click(screen.getAllByText('New Regimen')[0]); await waitFor(() => screen.getByPlaceholderText('e.g. Morning routine')); // The time-of-day select has 'Any time' as its first option text const timeOfDaySelect = screen .getAllByRole('combobox') .find((s) => (s as HTMLSelectElement).options[0]?.text === 'Any time') as HTMLSelectElement; expect(timeOfDaySelect).toBeDefined(); fireEvent.change(timeOfDaySelect!, { target: { value: 'morning' } }); expect(screen.getByPlaceholderText('e.g. Morning routine')).toBeInTheDocument(); }); it('shows error when burn rate fetch fails', async () => { mockGetBurnRates.mockRejectedValue(new Error('Burn rate failed')); render(, { wrapper }); await waitFor(() => screen.getByText('Burn rate')); await userEvent.click(screen.getByText('Burn rate')); await waitFor(() => expect(screen.getByText('Burn rate failed')).toBeInTheDocument()); }); it('shows error when update fails', async () => { mockListRegimens.mockResolvedValue({ data: [regimen], pagination: { cursor: null, hasMore: false }, }); mockUpdateRegimen.mockRejectedValue(new Error('Update failed')); render(, { wrapper }); await waitFor(() => screen.getByText('Morning Routine')); await userEvent.click(screen.getByTitle('Deactivate')); await waitFor(() => expect(screen.getByText('Update failed')).toBeInTheDocument()); }); it('toggles active/inactive status', async () => { mockListRegimens.mockResolvedValue({ data: [regimen], pagination: { cursor: null, hasMore: false }, }); mockUpdateRegimen.mockResolvedValue({ ...regimen, isActive: false }); render(, { wrapper }); await waitFor(() => screen.getByText('Morning Routine')); await userEvent.click(screen.getByTitle('Deactivate')); expect(mockUpdateRegimen).toHaveBeenCalledWith('hh1', 'reg-1', { isActive: false }); }); it('cancels new regimen form using internal Cancel button', async () => { render(, { wrapper }); await waitFor(() => screen.getByText('New Regimen')); await userEvent.click(screen.getAllByText('New Regimen')[0]); await waitFor(() => screen.getByPlaceholderText('e.g. Morning routine')); // The Cancel button inside the RegimenForm (not the toggle button) const cancelButtons = screen.getAllByRole('button', { name: 'Cancel' }); await userEvent.click(cancelButtons[cancelButtons.length - 1]!); expect(screen.queryByPlaceholderText('e.g. Morning routine')).not.toBeInTheDocument(); }); it('filters regimens by inactive status', async () => { mockListRegimens.mockResolvedValue(emptyRegimens); render(, { wrapper }); await waitFor(() => expect(mockListRegimens).toHaveBeenCalledTimes(1)); const filterSelect = screen.getByDisplayValue('All regimens'); fireEvent.change(filterSelect, { target: { value: 'inactive' } }); await waitFor(() => expect(mockListRegimens).toHaveBeenCalledWith( 'hh1', expect.objectContaining({ isActive: false }), ), ); }); it('shows filtered empty state when filter is active and no results', async () => { mockListRegimens.mockResolvedValue(emptyRegimens); render(, { wrapper }); await waitFor(() => screen.getByDisplayValue('All regimens')); fireEvent.change(screen.getByDisplayValue('All regimens'), { target: { value: 'active' } }); await waitFor(() => expect(screen.getByText('No active regimens found.')).toBeInTheDocument()); }); it('shows null when form is open and regimens list is empty', async () => { mockListRegimens.mockResolvedValue(emptyRegimens); render(, { wrapper }); await waitFor(() => screen.getByText('New Regimen')); await userEvent.click(screen.getAllByText('New Regimen')[0]); // Form is open with no regimens — empty state shows null (nothing visible in that spot) await waitFor(() => screen.getByPlaceholderText('e.g. Morning routine')); expect(screen.queryByText('No regimens yet.')).not.toBeInTheDocument(); }); it('shows plural medications label when regimen has multiple meds', async () => { const multiMedRegimen = { ...regimen, medications: [ { ...regimen.medications[0] }, { ...regimen.medications[0], medicineId: 'med-2', medicineName: 'Aspirin' }, ], }; mockListRegimens.mockResolvedValue({ data: [multiMedRegimen], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => expect(screen.getByText(/2 medications/)).toBeInTheDocument()); }); it('shows fallback error when non-Error thrown on burn rates', async () => { mockGetBurnRates.mockRejectedValue('burn failed'); mockListRegimens.mockResolvedValue(emptyRegimens); render(, { wrapper }); await waitFor(() => screen.getByText('Burn rate')); await userEvent.click(screen.getByText('Burn rate')); await waitFor(() => expect(screen.getByText('Failed to load data')).toBeInTheDocument()); }); it('shows fallback error when non-Error thrown on create regimen', async () => { mockCreateRegimen.mockRejectedValue('save failed'); render(, { wrapper }); await waitFor(() => screen.getByText('New Regimen')); await userEvent.click(screen.getAllByText('New Regimen')[0]); await waitFor(() => screen.getByPlaceholderText('e.g. Morning routine')); await userEvent.type(screen.getByPlaceholderText('e.g. Morning routine'), 'Test'); fireEvent.submit(screen.getByPlaceholderText('e.g. Morning routine').closest('form')!); await waitFor(() => expect(screen.getByText('Failed to save regimen')).toBeInTheDocument()); }); it('initializes edit form with existing medications', async () => { mockListRegimens.mockResolvedValue({ data: [regimen], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => screen.getByText('Morning Routine')); await userEvent.click(screen.getByTitle('Edit')); await waitFor(() => screen.getByDisplayValue('Morning Routine')); // The medication row should be pre-filled — Remove button should be present expect(screen.getByTitle('Remove medication')).toBeInTheDocument(); }); });