import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; const { mockListFills, mockPreviewFill, mockExecuteFill, mockUndoFill } = vi.hoisted(() => ({ mockListFills: vi.fn(), mockPreviewFill: vi.fn(), mockExecuteFill: vi.fn(), mockUndoFill: vi.fn(), })); const { mockListRegimens } = vi.hoisted(() => ({ mockListRegimens: vi.fn() })); vi.mock('@/services/organizer', () => ({ listFills: mockListFills, previewFill: mockPreviewFill, executeFill: mockExecuteFill, undoFill: mockUndoFill, })); vi.mock('@/services/regimens', () => ({ listRegimens: mockListRegimens })); import { OrganizerTab } from '../../../../src/app/(dashboard)/medicines/OrganizerTab'; const emptyRegimens = { data: [], pagination: { cursor: null, hasMore: false } }; const emptyFills = { data: [], pagination: { cursor: null, hasMore: false } }; const activeRegimen = { _id: 'reg-1', name: 'Morning Routine', isActive: true, medications: [], createdAt: '2026-01-01T00:00:00.000Z', }; beforeEach(() => { vi.clearAllMocks(); mockListRegimens.mockResolvedValue(emptyRegimens); mockListFills.mockResolvedValue(emptyFills); }); describe('OrganizerTab', () => { it('shows fill organizer and fill history sections', async () => { render(); await waitFor(() => expect(screen.getByText('Fill Pill Organizer')).toBeInTheDocument()); expect(screen.getByText('Fill History')).toBeInTheDocument(); }); it('shows no active regimens message when none exist', async () => { render(); await waitFor(() => expect(screen.getByText(/No active regimens found/)).toBeInTheDocument()); }); it('shows fill form when active regimens exist', async () => { mockListRegimens.mockResolvedValue({ data: [activeRegimen], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => expect(screen.getByText('Preview fill')).toBeInTheDocument()); }); it('shows empty fill history', async () => { render(); await waitFor(() => expect(screen.getByText('No fills recorded yet.')).toBeInTheDocument()); }); it('renders fill history entries', async () => { mockListFills.mockResolvedValue({ data: [ { _id: 'fill-1', regimenId: 'reg-1', regimenName: 'Morning Routine', status: 'completed', numberOfDays: 7, items: [], createdAt: '2026-01-01T00:00:00.000Z', }, ], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => expect(screen.getByText('Morning Routine')).toBeInTheDocument()); }); it('previews fill and shows preview result', async () => { mockListRegimens.mockResolvedValue({ data: [activeRegimen], pagination: { cursor: null, hasMore: false }, }); mockPreviewFill.mockResolvedValue({ regimenId: 'reg-1', numberOfDays: 7, hasShortages: false, items: [], }); render(); await waitFor(() => screen.getByText('Preview fill')); fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, { target: { value: 'reg-1' }, }); fireEvent.change(screen.getByDisplayValue('7'), { target: { value: '14' } }); fireEvent.submit(screen.getByText('Preview fill').closest('form')!); await waitFor(() => expect(mockPreviewFill).toHaveBeenCalledWith( 'hh1', expect.objectContaining({ regimenId: 'reg-1' }), ), ); }); it('shows preview result and executes fill', async () => { mockListRegimens.mockResolvedValue({ data: [activeRegimen], pagination: { cursor: null, hasMore: false }, }); mockPreviewFill.mockResolvedValue({ regimenId: 'reg-1', regimenName: 'Morning Routine', numberOfDays: 7, hasShortages: false, items: [ { medicineId: 'med-1', medicineName: 'Metformin', quantityNeeded: 7, quantityAvailable: 30, isShort: false, shortage: 0, cabinetBreakdown: [], }, ], }); mockExecuteFill.mockResolvedValue({ _id: 'fill-new', status: 'completed' }); render(); await waitFor(() => screen.getByText('Preview fill')); fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, { target: { value: 'reg-1' }, }); fireEvent.submit(screen.getByText('Preview fill').closest('form')!); await waitFor(() => screen.getByText('Confirm fill')); expect(screen.getByText('Metformin')).toBeInTheDocument(); await userEvent.click(screen.getByText('Confirm fill')); await waitFor(() => expect(mockExecuteFill).toHaveBeenCalledWith( 'hh1', expect.objectContaining({ regimenId: 'reg-1' }), ), ); }); it('shows shortage warning in preview', async () => { mockListRegimens.mockResolvedValue({ data: [activeRegimen], pagination: { cursor: null, hasMore: false }, }); mockPreviewFill.mockResolvedValue({ regimenId: 'reg-1', regimenName: 'Morning Routine', numberOfDays: 7, hasShortages: true, items: [ { medicineId: 'med-1', medicineName: 'Metformin', quantityNeeded: 7, quantityAvailable: 3, isShort: true, shortage: 4, cabinetBreakdown: [], }, ], }); render(); await waitFor(() => screen.getByText('Preview fill')); fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, { target: { value: 'reg-1' }, }); fireEvent.submit(screen.getByText('Preview fill').closest('form')!); await waitFor(() => screen.getByText('Shortages detected')); expect(screen.getByText('Allow partial fill (fill what is available)')).toBeInTheDocument(); }); it('shows undo error when undo fails', async () => { mockListFills.mockResolvedValue({ data: [ { _id: 'fill-1', regimenId: 'reg-1', regimenName: 'Morning Routine', status: 'completed', numberOfDays: 7, items: [], createdAt: '2026-01-01T00:00:00.000Z', }, ], pagination: { cursor: null, hasMore: false }, }); mockUndoFill.mockRejectedValue(new Error('Undo failed')); vi.spyOn(window, 'confirm').mockReturnValue(true); render(); await waitFor(() => screen.getByText('Undo')); await userEvent.click(screen.getByText('Undo')); await waitFor(() => expect(screen.getByText('Undo failed')).toBeInTheDocument()); }); it('filters fill history by status', async () => { mockListFills.mockResolvedValue({ data: [ { _id: 'fill-1', regimenId: 'reg-1', regimenName: 'Morning Routine', status: 'completed', numberOfDays: 7, items: [], createdAt: '2026-01-01T00:00:00.000Z', }, ], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => screen.getByDisplayValue('All statuses')); fireEvent.change(screen.getByDisplayValue('All statuses'), { target: { value: 'completed' } }); await waitFor(() => expect(mockListFills).toHaveBeenCalledTimes(2)); }); it('shows Ready to fill when no shortages', async () => { mockListRegimens.mockResolvedValue({ data: [activeRegimen], pagination: { cursor: null, hasMore: false }, }); mockPreviewFill.mockResolvedValue({ regimenId: 'reg-1', regimenName: 'Morning Routine', numberOfDays: 7, hasShortages: false, items: [ { medicineId: 'med-1', medicineName: 'Metformin', quantityNeeded: 7, quantityAvailable: 30, isShort: false, shortage: 0, cabinetBreakdown: [], }, ], }); render(); await waitFor(() => screen.getByText('Preview fill')); fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, { target: { value: 'reg-1' }, }); fireEvent.submit(screen.getByText('Preview fill').closest('form')!); await waitFor(() => expect(screen.getByText('Ready to fill')).toBeInTheDocument()); }); it('renders fill items with short status', async () => { mockListFills.mockResolvedValue({ data: [ { _id: 'fill-1', regimenId: 'reg-1', regimenName: 'Morning Routine', status: 'partial', numberOfDays: 7, fillDate: '2026-01-01T00:00:00.000Z', items: [ { medicineId: 'med-1', medicineName: 'Metformin', quantityNeeded: 7, quantityTaken: 3, wasShort: true, shortage: 4, deductions: [], }, ], createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z', }, ], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => expect(screen.getByText(/Metformin.*short/)).toBeInTheDocument()); }); it('dismisses fill history error', async () => { mockListFills.mockRejectedValue(new Error('History error')); render(); await waitFor(() => screen.getByText('History error')); await userEvent.click(screen.getByText('Dismiss')); expect(screen.queryByText('History error')).not.toBeInTheDocument(); }); it('undoes a fill after confirmation', async () => { mockListFills.mockResolvedValue({ data: [ { _id: 'fill-1', regimenId: 'reg-1', regimenName: 'Morning Routine', status: 'completed', numberOfDays: 7, items: [], createdAt: '2026-01-01T00:00:00.000Z', }, ], pagination: { cursor: null, hasMore: false }, }); mockUndoFill.mockResolvedValue({}); vi.spyOn(window, 'confirm').mockReturnValue(true); render(); await waitFor(() => screen.getByText('Undo')); await userEvent.click(screen.getByText('Undo')); expect(mockUndoFill).toHaveBeenCalledWith('hh1', 'fill-1'); }); it('cancels from preview screen', async () => { mockListRegimens.mockResolvedValue({ data: [activeRegimen], pagination: { cursor: null, hasMore: false }, }); mockPreviewFill.mockResolvedValue({ regimenId: 'reg-1', regimenName: 'Morning Routine', numberOfDays: 7, hasShortages: false, items: [], }); render(); await waitFor(() => screen.getByText('Preview fill')); fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, { target: { value: 'reg-1' }, }); fireEvent.submit(screen.getByText('Preview fill').closest('form')!); await waitFor(() => screen.getByText('Confirm fill')); await userEvent.click(screen.getByRole('button', { name: 'Back' })); // Should return to the form await waitFor(() => expect(screen.getByText('Preview fill')).toBeInTheDocument()); }); it('shows cabinet breakdown in preview items', async () => { mockListRegimens.mockResolvedValue({ data: [activeRegimen], pagination: { cursor: null, hasMore: false }, }); mockPreviewFill.mockResolvedValue({ regimenId: 'reg-1', regimenName: 'Morning Routine', numberOfDays: 7, hasShortages: false, items: [ { medicineId: 'med-1', medicineName: 'Metformin', quantityNeeded: 7, quantityAvailable: 30, isShort: false, shortage: 0, cabinetBreakdown: [ { cabinetItemId: 'ci-1', quantityToTake: 7, expirationDate: '2027-06-01T00:00:00.000Z', }, { cabinetItemId: 'ci-2', quantityToTake: 3 }, ], }, ], }); render(); await waitFor(() => screen.getByText('Preview fill')); fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, { target: { value: 'reg-1' }, }); fireEvent.submit(screen.getByText('Preview fill').closest('form')!); await waitFor(() => expect(screen.getByText(/7 units/)).toBeInTheDocument()); }); it('shows fallback error when non-Error thrown during preview', async () => { mockListRegimens.mockResolvedValue({ data: [activeRegimen], pagination: { cursor: null, hasMore: false }, }); mockPreviewFill.mockRejectedValue('preview failed'); render(); await waitFor(() => screen.getByText('Preview fill')); fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, { target: { value: 'reg-1' }, }); fireEvent.submit(screen.getByText('Preview fill').closest('form')!); await waitFor(() => expect(screen.getByText('Failed to generate preview')).toBeInTheDocument()); }); it('shows fallback error when non-Error thrown during fill', async () => { mockListRegimens.mockResolvedValue({ data: [activeRegimen], pagination: { cursor: null, hasMore: false }, }); mockPreviewFill.mockResolvedValue({ regimenId: 'reg-1', regimenName: 'Morning Routine', numberOfDays: 7, hasShortages: false, items: [], }); mockExecuteFill.mockRejectedValue('fill failed'); render(); await waitFor(() => screen.getByText('Preview fill')); fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, { target: { value: 'reg-1' }, }); fireEvent.submit(screen.getByText('Preview fill').closest('form')!); await waitFor(() => screen.getByText('Confirm fill')); await userEvent.click(screen.getByText('Confirm fill')); await waitFor(() => expect(screen.getByText('Fill failed')).toBeInTheDocument()); }); it('toggles allow partial checkbox in shortage preview', async () => { mockListRegimens.mockResolvedValue({ data: [activeRegimen], pagination: { cursor: null, hasMore: false }, }); mockPreviewFill.mockResolvedValue({ regimenId: 'reg-1', regimenName: 'Morning Routine', numberOfDays: 7, hasShortages: true, items: [ { medicineId: 'med-1', medicineName: 'Metformin', quantityNeeded: 7, quantityAvailable: 3, isShort: true, shortage: 4, cabinetBreakdown: [], }, ], }); render(); await waitFor(() => screen.getByText('Preview fill')); fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, { target: { value: 'reg-1' }, }); fireEvent.submit(screen.getByText('Preview fill').closest('form')!); await waitFor(() => screen.getByText('Allow partial fill (fill what is available)')); fireEvent.click(screen.getByLabelText('Allow partial fill (fill what is available)')); expect(screen.getByLabelText('Allow partial fill (fill what is available)')).toBeChecked(); }); it('changes notes field in the organizer form', async () => { mockListRegimens.mockResolvedValue({ data: [activeRegimen], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => screen.getByPlaceholderText('Any notes for this fill')); fireEvent.change(screen.getByPlaceholderText('Any notes for this fill'), { target: { value: 'Fill for next week' }, }); expect(screen.getByDisplayValue('Fill for next week')).toBeInTheDocument(); }); });