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 { mockListCabinetEvents, mockGetSpendingSummary } = vi.hoisted(() => ({ mockListCabinetEvents: vi.fn(), mockGetSpendingSummary: vi.fn(), })); const { mockListMedicines } = vi.hoisted(() => ({ mockListMedicines: vi.fn() })); vi.mock('@/services/cabinet-events', () => ({ listCabinetEvents: mockListCabinetEvents, getSpendingSummary: mockGetSpendingSummary, })); vi.mock('@/services/medicines', () => ({ listMedicines: mockListMedicines })); import { ActivityTab } from '../../../../src/app/(dashboard)/medicines/ActivityTab'; const emptyMeds = { data: [], pagination: { cursor: null, hasMore: false } }; const emptyEvents = { data: [], pagination: { cursor: null, hasMore: false } }; const emptySummary = { totalSpent: 0, currency: null, byMedicine: [], byPeriod: [], }; beforeEach(() => { vi.clearAllMocks(); mockListMedicines.mockResolvedValue(emptyMeds); mockListCabinetEvents.mockResolvedValue(emptyEvents); mockGetSpendingSummary.mockResolvedValue(emptySummary); }); describe('ActivityTab', () => { it('renders Spending Summary and Cabinet Activity sections', async () => { render(); await waitFor(() => expect(screen.getByText('Spending Summary')).toBeInTheDocument()); expect(screen.getByText('Cabinet Activity')).toBeInTheDocument(); }); it('fetches spending summary on mount', async () => { render(); await waitFor(() => expect(mockGetSpendingSummary).toHaveBeenCalledWith('hh1', expect.any(Object)), ); }); it('fetches events on mount', async () => { render(); await waitFor(() => expect(mockListCabinetEvents).toHaveBeenCalledWith('hh1', expect.any(Object)), ); }); it('shows empty state when no events', async () => { render(); await waitFor(() => expect(screen.getByText('No events found for the selected filters.')).toBeInTheDocument(), ); }); it('shows error when events fail to load', async () => { mockListCabinetEvents.mockRejectedValue(new Error('Events error')); render(); await waitFor(() => expect(screen.getByText('Events error')).toBeInTheDocument()); }); it('shows error when spending summary fails', async () => { mockGetSpendingSummary.mockRejectedValue(new Error('Spending error')); render(); await waitFor(() => expect(screen.getByText('Spending error')).toBeInTheDocument()); }); it('renders event entries', async () => { mockListCabinetEvents.mockResolvedValue({ data: [ { _id: 'ev-1', eventType: 'purchased', medicineId: 'med-1', medicineName: 'Metformin', quantity: 10, quantityBefore: 0, quantityAfter: 10, unit: 'tablet', createdAt: '2026-01-01T00:00:00.000Z', }, ], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => expect(screen.getByText('Metformin')).toBeInTheDocument()); }); it('shows spending summary with data', async () => { mockGetSpendingSummary.mockResolvedValue({ totalSpent: 125.5, currency: 'USD', byMedicine: [ { medicineId: 'med-1', medicineName: 'Metformin', totalSpent: 125.5, purchaseCount: 2, avgUnitPrice: 0.69, }, ], byPeriod: [{ period: '2026-01', totalSpent: 125.5 }], }); render(); await waitFor(() => expect(screen.getAllByText(/125.50/).length).toBeGreaterThan(0)); expect(screen.getByText('Metformin')).toBeInTheDocument(); expect(screen.getByText('By medicine')).toBeInTheDocument(); }); it('shows no purchase data message when total is zero', async () => { render(); await waitFor(() => expect(screen.getByText('No purchase data found for this period.')).toBeInTheDocument(), ); }); it('filters events by event type', async () => { render(); await waitFor(() => expect(mockListCabinetEvents).toHaveBeenCalledTimes(1)); fireEvent.change(screen.getByDisplayValue('All event types'), { target: { value: 'purchased' }, }); await waitFor(() => expect(mockListCabinetEvents).toHaveBeenCalledTimes(2)); }); it('filters events by medicine', async () => { mockListMedicines.mockResolvedValue({ data: [{ _id: 'med-1', name: 'Metformin' }], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => expect(screen.getAllByDisplayValue('All medicines').length).toBeGreaterThan(1), ); const allMedSelects = screen.getAllByDisplayValue('All medicines'); // The last select is the cabinet events medicine filter fireEvent.change(allMedSelects[allMedSelects.length - 1]!, { target: { value: 'med-1' } }); await waitFor(() => expect(mockListCabinetEvents).toHaveBeenCalledTimes(2)); }); it('shows clear filters button when filter applied and clears them', async () => { render(); await waitFor(() => expect(mockListCabinetEvents).toHaveBeenCalled()); fireEvent.change(screen.getByDisplayValue('All event types'), { target: { value: 'purchased' }, }); await waitFor(() => screen.getByText('Clear filters')); await userEvent.click(screen.getByText('Clear filters')); expect(screen.queryByText('Clear filters')).not.toBeInTheDocument(); }); it('dismisses cabinet activity error', async () => { mockListCabinetEvents.mockRejectedValue(new Error('Events error')); render(); await waitFor(() => screen.getByText('Events error')); await userEvent.click(screen.getByText('Dismiss')); expect(screen.queryByText('Events error')).not.toBeInTheDocument(); }); it('changes spending summary period and medicine filters', async () => { mockListMedicines.mockResolvedValue({ data: [{ _id: 'med-1', name: 'Metformin' }], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => expect(mockGetSpendingSummary).toHaveBeenCalledTimes(1)); // Change period fireEvent.change(screen.getByDisplayValue('This month'), { target: { value: 'quarter' } }); await waitFor(() => expect(mockGetSpendingSummary).toHaveBeenCalledTimes(2)); // Change medicine filter in spending summary (first "All medicines" select) const medSelects = screen.getAllByDisplayValue('All medicines'); fireEvent.change(medSelects[0]!, { target: { value: 'med-1' } }); await waitFor(() => expect(mockGetSpendingSummary).toHaveBeenCalledTimes(3)); }); it('changes start and end date filters', async () => { render(); await waitFor(() => expect(mockListCabinetEvents).toHaveBeenCalledTimes(1)); const dateInputs = document.querySelectorAll('input[type="date"]'); fireEvent.change(dateInputs[0]!, { target: { value: '2026-01-01' } }); fireEvent.change(dateInputs[1]!, { target: { value: '2026-03-31' } }); await waitFor(() => expect(mockListCabinetEvents).toHaveBeenCalledTimes(3)); }); it('loads more events when Load more is clicked', async () => { mockListCabinetEvents.mockResolvedValue({ data: [ { _id: 'ev-1', eventType: 'purchased', medicineId: 'med-1', medicineName: 'Metformin', quantity: 10, quantityBefore: 0, quantityAfter: 10, unit: 'tablet', createdAt: '2026-01-01T00:00:00.000Z', }, ], pagination: { cursor: 'cursor-1', hasMore: true }, }); render(); await waitFor(() => screen.getByText('Load more')); await userEvent.click(screen.getByRole('button', { name: 'Load more' })); expect(mockListCabinetEvents).toHaveBeenCalledTimes(2); }); it('renders event with negative quantity (red dot)', async () => { mockListCabinetEvents.mockResolvedValue({ data: [ { _id: 'ev-1', eventType: 'taken', medicineId: 'med-1', medicineName: 'Metformin', quantity: -1, quantityBefore: 10, quantityAfter: 9, unit: 'tablet', createdAt: '2026-01-01T00:00:00.000Z', }, ], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => expect(screen.getByText('Metformin')).toBeInTheDocument()); }); it('renders event with reason, storeName, totalPrice, and notes', async () => { mockListCabinetEvents.mockResolvedValue({ data: [ { _id: 'ev-1', eventType: 'purchased', medicineId: 'med-1', medicineName: 'Metformin', quantity: 30, quantityBefore: 0, quantityAfter: 30, unit: 'tablet', reason: 'Monthly refill', storeName: 'Pharmacy', totalPrice: 25.5, currency: 'USD', notes: 'On sale', createdAt: '2026-01-01T00:00:00.000Z', }, ], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => expect(screen.getByText('Metformin')).toBeInTheDocument()); expect(screen.getByText(/Monthly refill/)).toBeInTheDocument(); expect(screen.getByText(/Pharmacy/)).toBeInTheDocument(); expect(screen.getByText(/25.50/)).toBeInTheDocument(); expect(screen.getByText(/On sale/)).toBeInTheDocument(); }); });