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 type React from 'react'; const { mockListMedicines, mockCreateMedicine, mockDeleteMedicine } = vi.hoisted(() => ({ mockListMedicines: vi.fn(), mockCreateMedicine: vi.fn(), mockDeleteMedicine: vi.fn(), })); vi.mock('@/services/medicines', () => ({ listMedicines: mockListMedicines, createMedicine: mockCreateMedicine, deleteMedicine: mockDeleteMedicine, })); vi.mock('next/link', () => ({ default: (props: { href?: string; children: React.ReactNode }) => ( {props.children} ), })); import { LibraryTab } from '../../../../src/app/(dashboard)/medicines/LibraryTab'; const emptyResponse = { data: [], pagination: { cursor: null, hasMore: false } }; const medicine = { _id: 'med-1', name: 'Metformin', form: 'tablet', strength: 500, strengthUnit: 'mg', category: 'prescription', tags: [], }; beforeEach(() => vi.clearAllMocks()); describe('LibraryTab', () => { it('shows loading skeleton initially', () => { mockListMedicines.mockReturnValue(new Promise(() => {})); render(); expect(screen.queryByText('Metformin')).not.toBeInTheDocument(); }); it('renders medicine list after load', async () => { mockListMedicines.mockResolvedValue({ data: [medicine], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => expect(screen.getByText('Metformin')).toBeInTheDocument()); }); it('shows empty state when no medicines', async () => { mockListMedicines.mockResolvedValue(emptyResponse); render(); await waitFor(() => expect(screen.getByText(/No medicines yet/)).toBeInTheDocument()); }); it('shows error when list fails', async () => { mockListMedicines.mockRejectedValue(new Error('Failed to load')); render(); await waitFor(() => expect(screen.getByText('Failed to load')).toBeInTheDocument()); }); it('dismisses error on Dismiss click', async () => { mockListMedicines.mockRejectedValue(new Error('Failed to load')); render(); await waitFor(() => screen.getByText('Failed to load')); await userEvent.click(screen.getByText('Dismiss')); expect(screen.queryByText('Failed to load')).not.toBeInTheDocument(); }); it('toggles Add Medicine form', async () => { mockListMedicines.mockResolvedValue(emptyResponse); render(); await waitFor(() => screen.getByText('Add Medicine')); await userEvent.click(screen.getByText('Add Medicine')); expect(screen.getByPlaceholderText('e.g. Metformin')).toBeInTheDocument(); await userEvent.click(screen.getAllByText('Cancel')[0]); expect(screen.queryByPlaceholderText('e.g. Metformin')).not.toBeInTheDocument(); }); it('creates medicine and refreshes list', async () => { mockListMedicines.mockResolvedValue(emptyResponse); mockCreateMedicine.mockResolvedValue({ _id: 'med-2', name: 'Aspirin' }); render(); await userEvent.click(screen.getByText('Add Medicine')); await userEvent.type(screen.getByPlaceholderText('e.g. Metformin'), 'Aspirin'); await userEvent.type(screen.getByPlaceholderText('500'), '100'); await userEvent.click(screen.getByRole('button', { name: 'Create Medicine' })); await waitFor(() => expect(mockCreateMedicine).toHaveBeenCalledWith( 'hh1', expect.objectContaining({ name: 'Aspirin' }), ), ); }); it('deletes medicine after confirmation', async () => { mockListMedicines.mockResolvedValue({ data: [medicine], pagination: { cursor: null, hasMore: false }, }); mockDeleteMedicine.mockResolvedValue({}); vi.spyOn(window, 'confirm').mockReturnValue(true); render(); await waitFor(() => screen.getByText('Metformin')); await userEvent.click(screen.getByTitle('Delete')); expect(mockDeleteMedicine).toHaveBeenCalledWith('hh1', 'med-1'); }); it('changes form fields in add medicine form', async () => { mockListMedicines.mockResolvedValue(emptyResponse); render(); await waitFor(() => screen.getByText('Add Medicine')); await userEvent.click(screen.getByText('Add Medicine')); await waitFor(() => screen.getByPlaceholderText('e.g. Metformin')); // Change form type fireEvent.change(screen.getByDisplayValue('Tablet'), { target: { value: 'capsule' } }); // Change strength unit fireEvent.change(screen.getByDisplayValue('mg'), { target: { value: 'mcg' } }); // Change category fireEvent.change(screen.getByDisplayValue('OTC'), { target: { value: 'prescription' } }); // Change notes (covers truthy branch) fireEvent.change(screen.getByPlaceholderText('Any additional notes'), { target: { value: 'test notes' }, }); // Clear notes (covers falsy branch → undefined) fireEvent.change(screen.getByPlaceholderText('Any additional notes'), { target: { value: '' }, }); // Verify form is still visible expect(screen.getByPlaceholderText('e.g. Metformin')).toBeInTheDocument(); }); it('filters medicines by search, category, and form', async () => { mockListMedicines.mockResolvedValue({ data: [medicine], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => screen.getByText('Metformin')); // Search filter fireEvent.change(screen.getByPlaceholderText('Search medicines...'), { target: { value: 'met' }, }); // Category filter fireEvent.change(screen.getByDisplayValue('All Categories'), { target: { value: 'prescription' }, }); // Form filter fireEvent.change(screen.getByDisplayValue('All Forms'), { target: { value: 'tablet' } }); expect(screen.getByPlaceholderText('Search medicines...')).toBeInTheDocument(); }); it('shows fallback error when non-Error is thrown on delete', async () => { mockListMedicines.mockResolvedValue({ data: [medicine], pagination: { cursor: null, hasMore: false }, }); mockDeleteMedicine.mockRejectedValue('oops'); vi.spyOn(window, 'confirm').mockReturnValue(true); render(); await waitFor(() => screen.getByText('Metformin')); await userEvent.click(screen.getByTitle('Delete')); await waitFor(() => expect(screen.getByText('Failed to delete')).toBeInTheDocument()); }); it('cancels CreateMedicineForm with internal Cancel button', async () => { mockListMedicines.mockResolvedValue(emptyResponse); render(); await waitFor(() => screen.getByText('Add Medicine')); await userEvent.click(screen.getByText('Add Medicine')); await waitFor(() => screen.getByPlaceholderText('e.g. Metformin')); const cancelButtons = screen.getAllByText('Cancel'); await userEvent.click(cancelButtons[cancelButtons.length - 1]!); expect(screen.queryByPlaceholderText('e.g. Metformin')).not.toBeInTheDocument(); }); it('shows fallback error when non-Error is thrown on create medicine', async () => { mockListMedicines.mockResolvedValue(emptyResponse); mockCreateMedicine.mockRejectedValue('create failed'); render(); await waitFor(() => screen.getByText('Add Medicine')); await userEvent.click(screen.getByText('Add Medicine')); await waitFor(() => screen.getByPlaceholderText('e.g. Metformin')); await userEvent.type(screen.getByPlaceholderText('e.g. Metformin'), 'Aspirin'); await userEvent.type(screen.getByPlaceholderText('500'), '100'); await userEvent.click(screen.getByRole('button', { name: 'Create Medicine' })); await waitFor(() => expect(screen.getByText('Failed to create medicine')).toBeInTheDocument()); }); it('does not delete medicine if confirmation cancelled', async () => { mockListMedicines.mockResolvedValue({ data: [medicine], pagination: { cursor: null, hasMore: false }, }); vi.spyOn(window, 'confirm').mockReturnValue(false); render(); await waitFor(() => screen.getByText('Metformin')); await userEvent.click(screen.getByTitle('Delete')); expect(mockDeleteMedicine).not.toHaveBeenCalled(); }); });