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'; import type React from 'react'; const { mockListCabinetItems, mockGetCabinetSummary, mockCreateCabinetItem, mockAdjustCabinetItemQuantity, mockDeleteCabinetItem, } = vi.hoisted(() => ({ mockListCabinetItems: vi.fn(), mockGetCabinetSummary: vi.fn(), mockCreateCabinetItem: vi.fn(), mockAdjustCabinetItemQuantity: vi.fn(), mockDeleteCabinetItem: vi.fn(), })); const { mockListMedicines } = vi.hoisted(() => ({ mockListMedicines: vi.fn() })); vi.mock('@/services/cabinet', () => ({ listCabinetItems: mockListCabinetItems, getCabinetSummary: mockGetCabinetSummary, createCabinetItem: mockCreateCabinetItem, adjustCabinetItemQuantity: mockAdjustCabinetItemQuantity, deleteCabinetItem: mockDeleteCabinetItem, })); vi.mock('@/services/medicines', () => ({ listMedicines: mockListMedicines })); vi.mock('next/link', () => ({ default: (props: { href?: string; children: React.ReactNode }) => ( {props.children} ), })); import { CabinetTab } from '../../../../src/app/(dashboard)/medicines/CabinetTab'; const emptySummary = { data: [] }; const emptyItems = { data: [], pagination: { cursor: null, hasMore: false } }; const emptyMeds = { data: [], pagination: { cursor: null, hasMore: false } }; const cabinetItem = { _id: 'ci-1', medicineId: 'med-1', medicineName: 'Metformin', quantity: 10, unit: 'tablet', status: 'active', expirationDate: null, unitPrice: null, totalPrice: null, storeId: null, notes: null, createdAt: '2026-01-01T00:00:00.000Z', }; beforeEach(() => { vi.clearAllMocks(); mockGetCabinetSummary.mockResolvedValue(emptySummary); mockListCabinetItems.mockResolvedValue(emptyItems); mockListMedicines.mockResolvedValue(emptyMeds); }); const wrapper = ({ children }: { children: React.ReactNode }) => ( new Map(), dedupingInterval: 0 }}>{children} ); describe('CabinetTab', () => { it('shows empty state when no items', async () => { render(, { wrapper }); await waitFor(() => expect(screen.getByText(/cabinet is empty/i)).toBeInTheDocument()); }); it('shows error when list fails', async () => { mockGetCabinetSummary.mockRejectedValue(new Error('Server error')); render(, { wrapper }); await waitFor(() => expect(screen.getByText('Server error')).toBeInTheDocument()); }); it('dismisses error on Dismiss click', async () => { mockGetCabinetSummary.mockRejectedValue(new Error('Server error')); render(, { wrapper }); await waitFor(() => screen.getByText('Server error')); await userEvent.click(screen.getByText('Dismiss')); expect(screen.queryByText('Server error')).not.toBeInTheDocument(); }); it('toggles Add to Cabinet form', async () => { render(, { wrapper }); await waitFor(() => screen.getByText('Add to Cabinet')); await userEvent.click(screen.getByText('Add to Cabinet')); expect(screen.getByPlaceholderText('Search medicines...')).toBeInTheDocument(); await userEvent.click(screen.getAllByText('Cancel')[0]); expect(screen.queryByPlaceholderText('Search medicines...')).not.toBeInTheDocument(); }); it('switches between Summary and All Items views', async () => { render(, { wrapper }); await waitFor(() => screen.getByText('All Items')); await userEvent.click(screen.getByText('All Items')); expect(mockListCabinetItems).toHaveBeenCalled(); }); it('renders cabinet items in detail view', async () => { mockListCabinetItems.mockResolvedValue({ data: [cabinetItem], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => screen.getByText('All Items')); await userEvent.click(screen.getByText('All Items')); await waitFor(() => expect(screen.getByText('Metformin')).toBeInTheDocument()); }); it('adjusts item quantity', async () => { mockListCabinetItems.mockResolvedValue({ data: [cabinetItem], pagination: { cursor: null, hasMore: false }, }); mockAdjustCabinetItemQuantity.mockResolvedValue({ ...cabinetItem, quantity: 11 }); mockGetCabinetSummary.mockResolvedValue(emptySummary); render(, { wrapper }); await waitFor(() => screen.getByText('All Items')); await userEvent.click(screen.getByText('All Items')); await waitFor(() => screen.getByTitle('Add 1')); await userEvent.click(screen.getByTitle('Add 1')); expect(mockAdjustCabinetItemQuantity).toHaveBeenCalledWith('hh1', 'ci-1', { delta: 1 }); }); it('shows summary view with medicine data', async () => { mockGetCabinetSummary.mockResolvedValue({ data: [ { medicineId: 'med-1', medicineName: 'Metformin', medicineStrength: 500, medicineStrengthUnit: 'mg', medicineForm: 'tablet', totalQuantity: 30, unit: 'tablet', itemCount: 2, earliestExpiry: null, }, ], }); render(, { wrapper }); await waitFor(() => expect(screen.getByText('Metformin')).toBeInTheDocument()); expect(screen.getByText(/2 items/)).toBeInTheDocument(); }); it('expands medicine in summary view', async () => { mockGetCabinetSummary.mockResolvedValue({ data: [ { medicineId: 'med-1', medicineName: 'Metformin', medicineStrength: 500, medicineStrengthUnit: 'mg', medicineForm: 'tablet', totalQuantity: 30, unit: 'tablet', itemCount: 1, earliestExpiry: null, }, ], }); mockListCabinetItems.mockResolvedValue({ data: [cabinetItem], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => screen.getByText('Metformin')); await userEvent.click(screen.getByText('Metformin').closest('button')!); await waitFor(() => expect(mockListCabinetItems).toHaveBeenCalledWith( 'hh1', expect.objectContaining({ medicineId: 'med-1' }), ), ); }); it('submits AddToCabinetForm with medicine selection validation', async () => { mockCreateCabinetItem.mockResolvedValue(cabinetItem); render(, { wrapper }); await waitFor(() => screen.getByText('Add to Cabinet')); await userEvent.click(screen.getByText('Add to Cabinet')); await waitFor(() => screen.getByPlaceholderText('Search medicines...')); // Submit without selecting a medicine - should show error fireEvent.submit(screen.getByPlaceholderText('Search medicines...').closest('form')!); await waitFor(() => expect(screen.getByText('Please select a medicine')).toBeInTheDocument()); }); it('submits AddToCabinetForm successfully', async () => { mockListMedicines.mockResolvedValue({ data: [ { _id: 'med-1', name: 'Metformin', form: 'tablet', strength: 500, strengthUnit: 'mg', }, ], pagination: { cursor: null, hasMore: false }, }); mockCreateCabinetItem.mockResolvedValue(cabinetItem); render(, { wrapper }); await waitFor(() => screen.getByText('Add to Cabinet')); await userEvent.click(screen.getByText('Add to Cabinet')); await waitFor(() => screen.getByPlaceholderText('Search medicines...')); // Select a medicine (first combobox is the medicine select) fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' }, }); fireEvent.submit(screen.getByPlaceholderText('Search medicines...').closest('form')!); await waitFor(() => expect(mockCreateCabinetItem).toHaveBeenCalledWith( 'hh1', expect.objectContaining({ medicineId: 'med-1' }), ), ); }); it('submits AddToCabinetForm with expiration date and notes', async () => { mockListMedicines.mockResolvedValue({ data: [ { _id: 'med-1', name: 'Metformin', form: 'tablet', strength: 500, strengthUnit: 'mg', }, ], pagination: { cursor: null, hasMore: false }, }); mockCreateCabinetItem.mockResolvedValue(cabinetItem); render(, { wrapper }); await waitFor(() => screen.getByText('Add to Cabinet')); await userEvent.click(screen.getByText('Add to Cabinet')); await waitFor(() => screen.getByPlaceholderText('Search medicines...')); // Select a medicine fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' }, }); // Set expiration date (type="date" input, no label association) const dateInput = document.querySelector('input[type="date"]') as HTMLElement; if (dateInput) fireEvent.change(dateInput, { target: { value: '2027-12-31' } }); // Set notes fireEvent.change(screen.getByPlaceholderText('Any notes about this item'), { target: { value: 'Store in fridge' }, }); fireEvent.submit(screen.getByPlaceholderText('Search medicines...').closest('form')!); await waitFor(() => expect(mockCreateCabinetItem).toHaveBeenCalledWith( 'hh1', expect.objectContaining({ medicineId: 'med-1', notes: 'Store in fridge', }), ), ); }); it('changes quantity and unit in AddToCabinetForm', async () => { mockListMedicines.mockResolvedValue({ data: [ { _id: 'med-1', name: 'Metformin', form: 'tablet', strength: 500, strengthUnit: 'mg' }, ], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => screen.getByText('Add to Cabinet')); await userEvent.click(screen.getByText('Add to Cabinet')); await waitFor(() => screen.getByPlaceholderText('Search medicines...')); // Search for medicine fireEvent.change(screen.getByPlaceholderText('Search medicines...'), { target: { value: 'Met' }, }); // Change quantity fireEvent.change(screen.getByPlaceholderText('30'), { target: { value: '60' } }); // Change unit fireEvent.change(screen.getAllByRole('combobox')[1]!, { target: { value: 'capsule' } }); expect(screen.getByPlaceholderText('Search medicines...')).toBeInTheDocument(); }); it('switches from detail view back to summary view', async () => { mockGetCabinetSummary.mockResolvedValue({ data: [ { medicineId: 'med-1', medicineName: 'Metformin', medicineStrength: 500, medicineStrengthUnit: 'mg', medicineForm: 'tablet', totalQuantity: 10, unit: 'tablet', itemCount: 1, earliestExpiry: null, }, ], }); render(, { wrapper }); await waitFor(() => screen.getByText('All Items')); await userEvent.click(screen.getByText('All Items')); // Now in detail view await waitFor(() => screen.getByText('Summary')); await userEvent.click(screen.getByText('Summary')); // Back in summary view — Metformin should show await waitFor(() => expect(screen.getByText('Metformin')).toBeInTheDocument()); }); it('shows expiry date for cabinet items', async () => { mockListCabinetItems.mockResolvedValue({ data: [ { ...cabinetItem, expirationDate: '2099-12-31T00:00:00.000Z', }, ], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => screen.getByText('All Items')); await userEvent.click(screen.getByText('All Items')); await waitFor(() => screen.getByText('Metformin')); expect(screen.getByText(/days\)/)).toBeInTheDocument(); }); it('filters cabinet items by status in detail view', async () => { mockListCabinetItems.mockResolvedValue({ data: [cabinetItem], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => screen.getByText('All Items')); await userEvent.click(screen.getByText('All Items')); await waitFor(() => screen.getByDisplayValue('All Statuses')); fireEvent.change(screen.getByDisplayValue('All Statuses'), { target: { value: 'active' } }); await waitFor(() => expect(mockListCabinetItems).toHaveBeenCalledTimes(2)); }); it('takes 1 from cabinet item', async () => { mockListCabinetItems.mockResolvedValue({ data: [cabinetItem], pagination: { cursor: null, hasMore: false }, }); mockAdjustCabinetItemQuantity.mockResolvedValue({ ...cabinetItem, quantity: 9 }); mockGetCabinetSummary.mockResolvedValue(emptySummary); render(, { wrapper }); await waitFor(() => screen.getByText('All Items')); await userEvent.click(screen.getByText('All Items')); await waitFor(() => screen.getByTitle('Take 1')); await userEvent.click(screen.getByTitle('Take 1')); expect(mockAdjustCabinetItemQuantity).toHaveBeenCalledWith('hh1', 'ci-1', { delta: -1 }); }); it('collapses expanded medicine when it returns 0 items', async () => { mockGetCabinetSummary.mockResolvedValue({ data: [ { medicineId: 'med-1', medicineName: 'Metformin', medicineStrength: 500, medicineStrengthUnit: 'mg', medicineForm: 'tablet', totalQuantity: 0, unit: 'tablet', itemCount: 0, earliestExpiry: null, }, ], }); mockListCabinetItems.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => screen.getByText('Metformin')); await userEvent.click(screen.getByText('Metformin').closest('button')!); await waitFor(() => expect(mockListCabinetItems).toHaveBeenCalledWith( 'hh1', expect.objectContaining({ medicineId: 'med-1' }), ), ); }); it('shows error when createCabinetItem fails', async () => { mockCreateCabinetItem.mockRejectedValue(new Error('Create failed')); render(, { wrapper }); await waitFor(() => screen.getByText('Add to Cabinet')); await userEvent.click(screen.getByText('Add to Cabinet')); await waitFor(() => screen.getByPlaceholderText('Search medicines...')); fireEvent.submit(screen.getByPlaceholderText('Search medicines...').closest('form')!); // Should show 'Please select a medicine' validation first (since no medicine selected) await waitFor(() => expect(screen.getByText('Please select a medicine')).toBeInTheDocument()); }); it('deletes cabinet item after confirmation', async () => { mockListCabinetItems.mockResolvedValue({ data: [cabinetItem], pagination: { cursor: null, hasMore: false }, }); mockDeleteCabinetItem.mockResolvedValue({}); vi.spyOn(window, 'confirm').mockReturnValue(true); render(, { wrapper }); await waitFor(() => screen.getByText('All Items')); await userEvent.click(screen.getByText('All Items')); await waitFor(() => screen.getByTitle('Delete')); await userEvent.click(screen.getByTitle('Delete')); expect(mockDeleteCabinetItem).toHaveBeenCalledWith('hh1', 'ci-1'); }); it('shows error when adjust fails', async () => { mockListCabinetItems.mockResolvedValue({ data: [cabinetItem], pagination: { cursor: null, hasMore: false }, }); mockAdjustCabinetItemQuantity.mockRejectedValue(new Error('Adjust failed')); render(, { wrapper }); await waitFor(() => screen.getByText('All Items')); await userEvent.click(screen.getByText('All Items')); await waitFor(() => screen.getByTitle('Add 1')); await userEvent.click(screen.getByTitle('Add 1')); await waitFor(() => expect(screen.getByText('Adjust failed')).toBeInTheDocument()); }); it('shows error when delete fails', async () => { mockListCabinetItems.mockResolvedValue({ data: [cabinetItem], pagination: { cursor: null, hasMore: false }, }); mockDeleteCabinetItem.mockRejectedValue(new Error('Delete failed')); vi.spyOn(window, 'confirm').mockReturnValue(true); render(, { wrapper }); await waitFor(() => screen.getByText('All Items')); await userEvent.click(screen.getByText('All Items')); await waitFor(() => screen.getByTitle('Delete')); await userEvent.click(screen.getByTitle('Delete')); await waitFor(() => expect(screen.getByText('Delete failed')).toBeInTheDocument()); }); it('cancels AddToCabinetForm with internal Cancel button', async () => { render(, { wrapper }); await waitFor(() => screen.getByText('Add to Cabinet')); await userEvent.click(screen.getByText('Add to Cabinet')); await waitFor(() => screen.getByPlaceholderText('Search medicines...')); // Click the Cancel button inside the form (not the toggle button) const cancelButtons = screen.getAllByText('Cancel'); await userEvent.click(cancelButtons[cancelButtons.length - 1]!); expect(screen.queryByPlaceholderText('Search medicines...')).not.toBeInTheDocument(); }); it('collapses expanded medicine when clicked again', async () => { const summary = { medicineId: 'med-1', medicineName: 'Metformin', medicineStrength: 500, medicineStrengthUnit: 'mg', medicineForm: 'tablet', totalQuantity: 10, unit: 'tablet', itemCount: 1, earliestExpiry: null, }; mockGetCabinetSummary.mockResolvedValue({ data: [summary] }); mockListCabinetItems.mockResolvedValue({ data: [cabinetItem], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => screen.getByText('Metformin')); // First click expands — handleExpand calls listCabinetItems await userEvent.click(screen.getByText('Metformin').closest('button')!); await waitFor(() => expect(mockListCabinetItems).toHaveBeenCalledTimes(1)); const callsAfterExpand = mockListCabinetItems.mock.calls.length; // Second click collapses — no additional listCabinetItems calls await userEvent.click(screen.getByText('Metformin').closest('button')!); await waitFor(() => expect(screen.queryByTitle('Take 1')).not.toBeInTheDocument()); expect(mockListCabinetItems.mock.calls.length).toBe(callsAfterExpand); }); it('shows empty items when expand fails', async () => { mockGetCabinetSummary.mockResolvedValue({ data: [ { medicineId: 'med-1', medicineName: 'Metformin', medicineStrength: 500, medicineStrengthUnit: 'mg', medicineForm: 'tablet', totalQuantity: 10, unit: 'tablet', itemCount: 1, earliestExpiry: null, }, ], }); mockListCabinetItems.mockRejectedValue(new Error('Expand failed')); render(, { wrapper }); await waitFor(() => screen.getByText('Metformin')); await userEvent.click(screen.getByText('Metformin').closest('button')!); await waitFor(() => expect(mockListCabinetItems).toHaveBeenCalledTimes(1)); // No items should be shown (empty after error) expect(screen.queryByTitle('Take 1')).not.toBeInTheDocument(); }); it('shows create error when medicine is selected and create fails', async () => { mockListMedicines.mockResolvedValue({ data: [ { _id: 'med-1', name: 'Metformin', form: 'tablet', strength: 500, strengthUnit: 'mg' }, ], pagination: { cursor: null, hasMore: false }, }); mockCreateCabinetItem.mockRejectedValue(new Error('Create failed')); render(, { wrapper }); await waitFor(() => screen.getByText('Add to Cabinet')); await userEvent.click(screen.getByText('Add to Cabinet')); await waitFor(() => screen.getByPlaceholderText('Search medicines...')); fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } }); fireEvent.submit(screen.getByPlaceholderText('Search medicines...').closest('form')!); await waitFor(() => expect(screen.getByText('Create failed')).toBeInTheDocument()); }); it('shows item notes in detail view', async () => { mockListCabinetItems.mockResolvedValue({ data: [{ ...cabinetItem, notes: 'Store in fridge' }], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => screen.getByText('All Items')); await userEvent.click(screen.getByText('All Items')); await waitFor(() => expect(screen.getByText('Store in fridge')).toBeInTheDocument()); }); it('waits for medicines to load then selects medicine in form', async () => { mockListMedicines.mockResolvedValue({ data: [ { _id: 'med-1', name: 'Metformin', form: 'tablet', strength: 500, strengthUnit: 'mg' }, ], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await userEvent.click(screen.getByText('Add to Cabinet')); // Wait for medicine options to load (covers medicines.map callback) await waitFor(() => expect( (screen.getAllByRole('combobox')[0] as HTMLSelectElement).options.length, ).toBeGreaterThan(1), ); // Select a valid medicine (true branch: medForm is set → defaultUnitForForm called) fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } }); // Select empty value (false branch: selectedMed undefined → DosageUnit.TABLET fallback) fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: '' } }); expect(screen.getByPlaceholderText('Search medicines...')).toBeInTheDocument(); }); it('shows fallback error when non-Error is thrown during create', async () => { mockListMedicines.mockResolvedValue({ data: [ { _id: 'med-1', name: 'Metformin', form: 'tablet', strength: 500, strengthUnit: 'mg' }, ], pagination: { cursor: null, hasMore: false }, }); mockCreateCabinetItem.mockRejectedValue('unexpected'); render(, { wrapper }); await waitFor(() => screen.getByText('Add to Cabinet')); await userEvent.click(screen.getByText('Add to Cabinet')); await waitFor(() => screen.getByPlaceholderText('Search medicines...')); fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } }); fireEvent.submit(screen.getByPlaceholderText('Search medicines...').closest('form')!); await waitFor(() => expect(screen.getByText('Failed to add item')).toBeInTheDocument()); }); });