306 lines
9.9 KiB
TypeScript
306 lines
9.9 KiB
TypeScript
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(<ActivityTab householdId="hh1" />);
|
|
|
|
await waitFor(() => expect(screen.getByText('Spending Summary')).toBeInTheDocument());
|
|
expect(screen.getByText('Cabinet Activity')).toBeInTheDocument();
|
|
});
|
|
|
|
it('fetches spending summary on mount', async () => {
|
|
render(<ActivityTab householdId="hh1" />);
|
|
|
|
await waitFor(() =>
|
|
expect(mockGetSpendingSummary).toHaveBeenCalledWith('hh1', expect.any(Object)),
|
|
);
|
|
});
|
|
|
|
it('fetches events on mount', async () => {
|
|
render(<ActivityTab householdId="hh1" />);
|
|
|
|
await waitFor(() =>
|
|
expect(mockListCabinetEvents).toHaveBeenCalledWith('hh1', expect.any(Object)),
|
|
);
|
|
});
|
|
|
|
it('shows empty state when no events', async () => {
|
|
render(<ActivityTab householdId="hh1" />);
|
|
|
|
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(<ActivityTab householdId="hh1" />);
|
|
|
|
await waitFor(() => expect(screen.getByText('Events error')).toBeInTheDocument());
|
|
});
|
|
|
|
it('shows error when spending summary fails', async () => {
|
|
mockGetSpendingSummary.mockRejectedValue(new Error('Spending error'));
|
|
|
|
render(<ActivityTab householdId="hh1" />);
|
|
|
|
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(<ActivityTab householdId="hh1" />);
|
|
|
|
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(<ActivityTab householdId="hh1" />);
|
|
|
|
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(<ActivityTab householdId="hh1" />);
|
|
|
|
await waitFor(() =>
|
|
expect(screen.getByText('No purchase data found for this period.')).toBeInTheDocument(),
|
|
);
|
|
});
|
|
|
|
it('filters events by event type', async () => {
|
|
render(<ActivityTab householdId="hh1" />);
|
|
|
|
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(<ActivityTab householdId="hh1" />);
|
|
|
|
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(<ActivityTab householdId="hh1" />);
|
|
|
|
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(<ActivityTab householdId="hh1" />);
|
|
|
|
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(<ActivityTab householdId="hh1" />);
|
|
|
|
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(<ActivityTab householdId="hh1" />);
|
|
|
|
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(<ActivityTab householdId="hh1" />);
|
|
|
|
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(<ActivityTab householdId="hh1" />);
|
|
|
|
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(<ActivityTab householdId="hh1" />);
|
|
|
|
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();
|
|
});
|
|
});
|