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();
});
});