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 { mockUseApi } = vi.hoisted(() => ({ mockUseApi: vi.fn() })); const { mockRecordPrice, mockGetPriceHistory, mockCompareStores } = vi.hoisted(() => ({ mockRecordPrice: vi.fn(), mockGetPriceHistory: vi.fn(), mockCompareStores: vi.fn(), })); const { mockListMedicines, mockListMedicineProducts } = vi.hoisted(() => ({ mockListMedicines: vi.fn(), mockListMedicineProducts: vi.fn(), })); const { mockListStores } = vi.hoisted(() => ({ mockListStores: vi.fn() })); vi.mock('@/lib/useApi', () => ({ useApi: mockUseApi })); vi.mock('@/services/medicine-prices', () => ({ recordPrice: mockRecordPrice, getPriceHistory: mockGetPriceHistory, compareStores: mockCompareStores, })); vi.mock('@/services/medicines', () => ({ listMedicines: mockListMedicines, listMedicineProducts: mockListMedicineProducts, })); vi.mock('@/services/stores', () => ({ listStores: mockListStores })); vi.mock('next/link', () => ({ default: (props: { href?: string; children: React.ReactNode }) => props.children, })); import MedicinePricesPage from '../../../../src/app/(dashboard)/medicine-prices/page'; const emptyResponse = { data: [], pagination: { cursor: null, hasMore: false } }; beforeEach(() => { vi.clearAllMocks(); mockListMedicines.mockResolvedValue(emptyResponse); mockListStores.mockResolvedValue(emptyResponse); mockGetPriceHistory.mockResolvedValue(emptyResponse); mockCompareStores.mockResolvedValue({ data: [] }); }); describe('MedicinePricesPage', () => { it('shows loading skeleton when session loading', () => { mockUseApi.mockReturnValue({ householdId: null, isLoading: true }); render(); expect(screen.getByText('Medicine Prices')).toBeInTheDocument(); expect(screen.queryByText('Record Price')).not.toBeInTheDocument(); }); it('shows household prompt when no householdId', () => { mockUseApi.mockReturnValue({ householdId: null, isLoading: false }); render(); expect(screen.getByText(/create or join a household/)).toBeInTheDocument(); }); it('renders Medicine Prices heading when householdId exists', () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); render(); expect(screen.getByText('Medicine Prices')).toBeInTheDocument(); expect(screen.getByText('Record Price')).toBeInTheDocument(); }); it('shows empty prompt to select a medicine', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); render(); await waitFor(() => expect(screen.getByText('Select a medicine to view price history.')).toBeInTheDocument(), ); }); it('loads price history when medicine selected', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListMedicines.mockResolvedValue({ data: [ { _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' }, ], pagination: { cursor: null, hasMore: false }, }); mockGetPriceHistory.mockResolvedValue({ data: [ { _id: 'pr-1', medicineId: 'med-1', medicineName: 'Metformin', storeId: 'st-1', storeName: 'Walgreens', price: 15.99, pricePerUnit: 0.18, currency: 'USD', quantity: 90, unit: 'tablet', date: '2026-01-01T00:00:00.000Z', createdAt: '2026-01-01T00:00:00.000Z', }, ], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => screen.getByText('Select a medicine')); fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } }); await waitFor(() => expect(mockGetPriceHistory).toHaveBeenCalledWith('hh1', 'med-1', expect.any(Object)), ); // Wait for price records to render await waitFor(() => expect(screen.getByText('Walgreens')).toBeInTheDocument()); }); it('shows validation error in form when required fields missing', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); render(); await userEvent.click(screen.getByText('Record Price')); await waitFor(() => screen.getByRole('button', { name: 'Record Price', hidden: false })); // The submit button inside the form also has text 'Record Price' const submitBtn = screen .getAllByRole('button', { name: 'Record Price' }) .find((b) => b.getAttribute('type') === 'submit'); if (submitBtn) { fireEvent.submit(submitBtn.closest('form')!); await waitFor(() => expect( screen.getByText('Please select a medicine, a product, and a store.'), ).toBeInTheDocument(), ); } }); it('shows error when price history fails', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListMedicines.mockResolvedValue({ data: [ { _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' }, ], pagination: { cursor: null, hasMore: false }, }); mockGetPriceHistory.mockRejectedValue(new Error('History load failed')); render(); await waitFor(() => screen.getByText('Select a medicine')); fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } }); await waitFor(() => expect(screen.getByText('History load failed')).toBeInTheDocument()); }); it('records a price successfully', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListMedicines.mockResolvedValue({ data: [ { _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' }, ], pagination: { cursor: null, hasMore: false }, }); mockListStores.mockResolvedValue({ data: [ { _id: 'st-1', name: 'Walgreens', tags: [], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', }, ], pagination: { cursor: null, hasMore: false }, }); mockListMedicineProducts.mockResolvedValue({ data: [{ _id: 'prod-1', brand: 'Generic', packageSize: 90, packageUnit: 'tablet' }], pagination: { cursor: null, hasMore: false }, }); mockRecordPrice.mockResolvedValue({}); render(); await waitFor(() => screen.getByText('Record Price')); await userEvent.click(screen.getByText('Record Price')); await waitFor(() => screen.getByText('Record Price', { selector: 'h2' })); // Select store fireEvent.change(screen.getByDisplayValue('Select store'), { target: { value: 'st-1' } }); // Select medicine fireEvent.change(screen.getByDisplayValue('Select medicine'), { target: { value: 'med-1' } }); // Wait for products to load and select product await waitFor(() => screen.getByText(/Generic.*90/)); fireEvent.change(screen.getByDisplayValue('Select product'), { target: { value: 'prod-1' } }); // Submit form fireEvent.submit( screen.getByRole('button', { name: 'Record Price', hidden: true }).closest('form')!, ); await waitFor(() => expect(mockRecordPrice).toHaveBeenCalledWith( 'hh1', expect.objectContaining({ medicineId: 'med-1' }), ), ); // Form should close after success await waitFor(() => expect(screen.queryByText('Record Price', { selector: 'h2' })).not.toBeInTheDocument(), ); }); it('shows Load more button in price history', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListMedicines.mockResolvedValue({ data: [ { _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' }, ], pagination: { cursor: null, hasMore: false }, }); mockGetPriceHistory.mockResolvedValue({ data: [ { _id: 'pr-1', medicineId: 'med-1', medicineName: 'Metformin', storeId: 'st-1', storeName: 'Walgreens', price: 15.99, pricePerUnit: 0.18, currency: 'USD', quantity: 90, unit: 'tablet', date: '2026-01-01T00:00:00.000Z', createdAt: '2026-01-01T00:00:00.000Z', }, ], pagination: { cursor: 'cur1', hasMore: true }, }); render(); await waitFor(() => screen.getByText('Select a medicine')); fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } }); await waitFor(() => expect(screen.getByText('Load more')).toBeInTheDocument()); await userEvent.click(screen.getByText('Load more')); expect(mockGetPriceHistory).toHaveBeenCalledTimes(2); }); it('shows store filter when medicine selected and changes it', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListMedicines.mockResolvedValue({ data: [ { _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' }, ], pagination: { cursor: null, hasMore: false }, }); mockListStores.mockResolvedValue({ data: [ { _id: 'st-1', name: 'Walgreens', tags: [], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', }, ], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => screen.getByText('Select a medicine')); fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } }); await waitFor(() => screen.getByDisplayValue('All stores')); fireEvent.change(screen.getByDisplayValue('All stores'), { target: { value: 'st-1' } }); await waitFor(() => expect(mockGetPriceHistory).toHaveBeenCalledTimes(2)); }); it('dismisses price history error', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListMedicines.mockResolvedValue({ data: [ { _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' }, ], pagination: { cursor: null, hasMore: false }, }); mockGetPriceHistory.mockRejectedValue(new Error('History load failed')); render(); await waitFor(() => screen.getByText('Select a medicine')); fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } }); await waitFor(() => screen.getByText('History load failed')); await userEvent.click(screen.getByText('Dismiss')); expect(screen.queryByText('History load failed')).not.toBeInTheDocument(); }); it('shows store comparison table', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListMedicines.mockResolvedValue({ data: [ { _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' }, ], pagination: { cursor: null, hasMore: false }, }); mockCompareStores.mockResolvedValue({ data: [ { storeId: 'st-1', storeName: 'Walgreens', latestPrice: 12.99, latestPricePerUnit: 0.14, currency: 'USD', date: '2026-01-01T00:00:00.000Z', isInsurancePrice: false, }, { storeId: 'st-2', storeName: 'CVS', latestPrice: 14.99, latestPricePerUnit: 0.17, currency: 'USD', date: '2026-01-01T00:00:00.000Z', isInsurancePrice: false, }, ], }); render(); await waitFor(() => screen.getByText('Select a medicine')); fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } }); await waitFor(() => expect(screen.getByText('Store comparison')).toBeInTheDocument()); expect(screen.getByText('Walgreens')).toBeInTheDocument(); // First row (cheapest) should be highlighted green expect(screen.getByText('cheapest')).toBeInTheDocument(); }); it('changes price, currency, quantity, and unit fields in form', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); render(); await userEvent.click(screen.getByText('Record Price')); await waitFor(() => screen.getByPlaceholderText('9.99')); fireEvent.change(screen.getByPlaceholderText('9.99'), { target: { value: '15.99' } }); fireEvent.change(screen.getByPlaceholderText('USD'), { target: { value: 'EUR' } }); fireEvent.change(screen.getByPlaceholderText('90'), { target: { value: '30' } }); fireEvent.change(screen.getByDisplayValue('tablet'), { target: { value: 'capsule' } }); expect(screen.getByPlaceholderText('9.99')).toBeInTheDocument(); }); it('changes medicine search, notes and insurance price fields in form', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); render(); await userEvent.click(screen.getByText('Record Price')); await waitFor(() => screen.getByPlaceholderText('Search medicines...')); fireEvent.change(screen.getByPlaceholderText('Search medicines...'), { target: { value: 'Met' }, }); // Notes field (no placeholder, but maxLength 1000) const notesInput = document.querySelector('input[maxLength="1000"]') as HTMLElement; if (notesInput) { fireEvent.change(notesInput, { target: { value: 'Some notes' } }); } // Insurance price checkbox fireEvent.click(screen.getByLabelText('Insurance price')); expect(screen.getByLabelText('Insurance price')).toBeChecked(); }); it('toggles Record Price form', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); render(); await userEvent.click(screen.getByText('Record Price')); // Both the header toggle and the form show "Cancel" when open expect(screen.getAllByRole('button', { name: 'Cancel' }).length).toBeGreaterThan(0); await userEvent.click(screen.getAllByRole('button', { name: 'Cancel' })[0]); expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument(); }); });