MeshiTrack/packages/web/tests/app/(dashboard)/medicines/ActivityTab.test.tsx

307 lines
9.9 KiB
TypeScript
Raw Normal View History

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 }));
2026-05-19 11:06:03 +09:00
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" />);
2026-04-26 18:44:59 +09:00
await waitFor(() =>
expect(mockGetSpendingSummary).toHaveBeenCalledWith('hh1', expect.any(Object)),
);
});
it('fetches events on mount', async () => {
render(<ActivityTab householdId="hh1" />);
2026-04-26 18:44:59 +09:00
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({
2026-04-26 18:44:59 +09:00
totalSpent: 125.5,
currency: 'USD',
byMedicine: [
{
medicineId: 'med-1',
medicineName: 'Metformin',
2026-04-26 18:44:59 +09:00
totalSpent: 125.5,
purchaseCount: 2,
avgUnitPrice: 0.69,
},
],
2026-04-26 18:44:59 +09:00
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));
2026-04-26 18:44:59 +09:00
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" />);
2026-04-26 18:44:59 +09:00
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());
2026-04-26 18:44:59 +09:00
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();
});
});