684 lines
23 KiB
TypeScript
684 lines
23 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';
|
|
import { SWRConfig } from 'swr';
|
|
import type React from 'react';
|
|
|
|
const {
|
|
mockListCabinetItems,
|
|
mockGetCabinetSummary,
|
|
mockCreateCabinetItem,
|
|
mockAdjustCabinetItemQuantity,
|
|
mockDeleteCabinetItem,
|
|
} = vi.hoisted(() => ({
|
|
mockListCabinetItems: vi.fn(),
|
|
mockGetCabinetSummary: vi.fn(),
|
|
mockCreateCabinetItem: vi.fn(),
|
|
mockAdjustCabinetItemQuantity: vi.fn(),
|
|
mockDeleteCabinetItem: vi.fn(),
|
|
}));
|
|
|
|
const { mockListMedicines } = vi.hoisted(() => ({ mockListMedicines: vi.fn() }));
|
|
|
|
vi.mock('@/services/cabinet', () => ({
|
|
listCabinetItems: mockListCabinetItems,
|
|
getCabinetSummary: mockGetCabinetSummary,
|
|
createCabinetItem: mockCreateCabinetItem,
|
|
adjustCabinetItemQuantity: mockAdjustCabinetItemQuantity,
|
|
deleteCabinetItem: mockDeleteCabinetItem,
|
|
}));
|
|
|
|
vi.mock('@/services/medicines', () => ({ listMedicines: mockListMedicines }));
|
|
|
|
vi.mock('next/link', () => ({
|
|
default: (props: { href?: string; children: React.ReactNode }) => (
|
|
<a href={props.href}>{props.children}</a>
|
|
),
|
|
}));
|
|
|
|
import { CabinetTab } from '../../../../src/app/(dashboard)/medicines/CabinetTab';
|
|
|
|
const emptySummary = { data: [] };
|
|
const emptyItems = { data: [], pagination: { cursor: null, hasMore: false } };
|
|
const emptyMeds = { data: [], pagination: { cursor: null, hasMore: false } };
|
|
|
|
const cabinetItem = {
|
|
_id: 'ci-1',
|
|
medicineId: 'med-1',
|
|
medicineName: 'Metformin',
|
|
quantity: 10,
|
|
unit: 'tablet',
|
|
status: 'active',
|
|
expirationDate: null,
|
|
unitPrice: null,
|
|
totalPrice: null,
|
|
storeId: null,
|
|
notes: null,
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockGetCabinetSummary.mockResolvedValue(emptySummary);
|
|
mockListCabinetItems.mockResolvedValue(emptyItems);
|
|
mockListMedicines.mockResolvedValue(emptyMeds);
|
|
});
|
|
|
|
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
<SWRConfig value={{ provider: () => new Map(), dedupingInterval: 0 }}>{children}</SWRConfig>
|
|
);
|
|
|
|
describe('CabinetTab', () => {
|
|
it('shows empty state when no items', async () => {
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => expect(screen.getByText(/cabinet is empty/i)).toBeInTheDocument());
|
|
});
|
|
|
|
it('shows error when list fails', async () => {
|
|
mockGetCabinetSummary.mockRejectedValue(new Error('Server error'));
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => expect(screen.getByText('Server error')).toBeInTheDocument());
|
|
});
|
|
|
|
it('dismisses error on Dismiss click', async () => {
|
|
mockGetCabinetSummary.mockRejectedValue(new Error('Server error'));
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('Server error'));
|
|
await userEvent.click(screen.getByText('Dismiss'));
|
|
expect(screen.queryByText('Server error')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('toggles Add to Cabinet form', async () => {
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('Add to Cabinet'));
|
|
await userEvent.click(screen.getByText('Add to Cabinet'));
|
|
expect(screen.getByPlaceholderText('Search medicines...')).toBeInTheDocument();
|
|
|
|
await userEvent.click(screen.getAllByText('Cancel')[0]);
|
|
expect(screen.queryByPlaceholderText('Search medicines...')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('switches between Summary and All Items views', async () => {
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('All Items'));
|
|
await userEvent.click(screen.getByText('All Items'));
|
|
|
|
expect(mockListCabinetItems).toHaveBeenCalled();
|
|
});
|
|
|
|
it('renders cabinet items in detail view', async () => {
|
|
mockListCabinetItems.mockResolvedValue({
|
|
data: [cabinetItem],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('All Items'));
|
|
await userEvent.click(screen.getByText('All Items'));
|
|
|
|
await waitFor(() => expect(screen.getByText('Metformin')).toBeInTheDocument());
|
|
});
|
|
|
|
it('adjusts item quantity', async () => {
|
|
mockListCabinetItems.mockResolvedValue({
|
|
data: [cabinetItem],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
mockAdjustCabinetItemQuantity.mockResolvedValue({ ...cabinetItem, quantity: 11 });
|
|
mockGetCabinetSummary.mockResolvedValue(emptySummary);
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('All Items'));
|
|
await userEvent.click(screen.getByText('All Items'));
|
|
|
|
await waitFor(() => screen.getByTitle('Add 1'));
|
|
await userEvent.click(screen.getByTitle('Add 1'));
|
|
|
|
expect(mockAdjustCabinetItemQuantity).toHaveBeenCalledWith('hh1', 'ci-1', { delta: 1 });
|
|
});
|
|
|
|
it('shows summary view with medicine data', async () => {
|
|
mockGetCabinetSummary.mockResolvedValue({
|
|
data: [
|
|
{
|
|
medicineId: 'med-1',
|
|
medicineName: 'Metformin',
|
|
medicineStrength: 500,
|
|
medicineStrengthUnit: 'mg',
|
|
medicineForm: 'tablet',
|
|
totalQuantity: 30,
|
|
unit: 'tablet',
|
|
itemCount: 2,
|
|
earliestExpiry: null,
|
|
},
|
|
],
|
|
});
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => expect(screen.getByText('Metformin')).toBeInTheDocument());
|
|
expect(screen.getByText(/2 items/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('expands medicine in summary view', async () => {
|
|
mockGetCabinetSummary.mockResolvedValue({
|
|
data: [
|
|
{
|
|
medicineId: 'med-1',
|
|
medicineName: 'Metformin',
|
|
medicineStrength: 500,
|
|
medicineStrengthUnit: 'mg',
|
|
medicineForm: 'tablet',
|
|
totalQuantity: 30,
|
|
unit: 'tablet',
|
|
itemCount: 1,
|
|
earliestExpiry: null,
|
|
},
|
|
],
|
|
});
|
|
mockListCabinetItems.mockResolvedValue({
|
|
data: [cabinetItem],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('Metformin'));
|
|
await userEvent.click(screen.getByText('Metformin').closest('button')!);
|
|
|
|
await waitFor(() =>
|
|
expect(mockListCabinetItems).toHaveBeenCalledWith(
|
|
'hh1',
|
|
expect.objectContaining({ medicineId: 'med-1' }),
|
|
),
|
|
);
|
|
});
|
|
|
|
it('submits AddToCabinetForm with medicine selection validation', async () => {
|
|
mockCreateCabinetItem.mockResolvedValue(cabinetItem);
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('Add to Cabinet'));
|
|
await userEvent.click(screen.getByText('Add to Cabinet'));
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('Search medicines...'));
|
|
|
|
// Submit without selecting a medicine - should show error
|
|
fireEvent.submit(screen.getByPlaceholderText('Search medicines...').closest('form')!);
|
|
|
|
await waitFor(() => expect(screen.getByText('Please select a medicine')).toBeInTheDocument());
|
|
});
|
|
|
|
it('submits AddToCabinetForm successfully', async () => {
|
|
mockListMedicines.mockResolvedValue({
|
|
data: [
|
|
{
|
|
_id: 'med-1',
|
|
name: 'Metformin',
|
|
form: 'tablet',
|
|
strength: 500,
|
|
strengthUnit: 'mg',
|
|
},
|
|
],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
mockCreateCabinetItem.mockResolvedValue(cabinetItem);
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('Add to Cabinet'));
|
|
await userEvent.click(screen.getByText('Add to Cabinet'));
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('Search medicines...'));
|
|
|
|
// Select a medicine (first combobox is the medicine select)
|
|
fireEvent.change(screen.getAllByRole('combobox')[0]!, {
|
|
target: { value: 'med-1' },
|
|
});
|
|
|
|
fireEvent.submit(screen.getByPlaceholderText('Search medicines...').closest('form')!);
|
|
|
|
await waitFor(() =>
|
|
expect(mockCreateCabinetItem).toHaveBeenCalledWith(
|
|
'hh1',
|
|
expect.objectContaining({ medicineId: 'med-1' }),
|
|
),
|
|
);
|
|
});
|
|
|
|
it('submits AddToCabinetForm with expiration date and notes', async () => {
|
|
mockListMedicines.mockResolvedValue({
|
|
data: [
|
|
{
|
|
_id: 'med-1',
|
|
name: 'Metformin',
|
|
form: 'tablet',
|
|
strength: 500,
|
|
strengthUnit: 'mg',
|
|
},
|
|
],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
mockCreateCabinetItem.mockResolvedValue(cabinetItem);
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('Add to Cabinet'));
|
|
await userEvent.click(screen.getByText('Add to Cabinet'));
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('Search medicines...'));
|
|
|
|
// Select a medicine
|
|
fireEvent.change(screen.getAllByRole('combobox')[0]!, {
|
|
target: { value: 'med-1' },
|
|
});
|
|
|
|
// Set expiration date (type="date" input, no label association)
|
|
const dateInput = document.querySelector('input[type="date"]') as HTMLElement;
|
|
if (dateInput) fireEvent.change(dateInput, { target: { value: '2027-12-31' } });
|
|
|
|
// Set notes
|
|
fireEvent.change(screen.getByPlaceholderText('Any notes about this item'), {
|
|
target: { value: 'Store in fridge' },
|
|
});
|
|
|
|
fireEvent.submit(screen.getByPlaceholderText('Search medicines...').closest('form')!);
|
|
|
|
await waitFor(() =>
|
|
expect(mockCreateCabinetItem).toHaveBeenCalledWith(
|
|
'hh1',
|
|
expect.objectContaining({
|
|
medicineId: 'med-1',
|
|
notes: 'Store in fridge',
|
|
}),
|
|
),
|
|
);
|
|
});
|
|
|
|
it('changes quantity and unit in AddToCabinetForm', async () => {
|
|
mockListMedicines.mockResolvedValue({
|
|
data: [
|
|
{ _id: 'med-1', name: 'Metformin', form: 'tablet', strength: 500, strengthUnit: 'mg' },
|
|
],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('Add to Cabinet'));
|
|
await userEvent.click(screen.getByText('Add to Cabinet'));
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('Search medicines...'));
|
|
|
|
// Search for medicine
|
|
fireEvent.change(screen.getByPlaceholderText('Search medicines...'), {
|
|
target: { value: 'Met' },
|
|
});
|
|
|
|
// Change quantity
|
|
fireEvent.change(screen.getByPlaceholderText('30'), { target: { value: '60' } });
|
|
|
|
// Change unit
|
|
fireEvent.change(screen.getAllByRole('combobox')[1]!, { target: { value: 'capsule' } });
|
|
|
|
expect(screen.getByPlaceholderText('Search medicines...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('switches from detail view back to summary view', async () => {
|
|
mockGetCabinetSummary.mockResolvedValue({
|
|
data: [
|
|
{
|
|
medicineId: 'med-1',
|
|
medicineName: 'Metformin',
|
|
medicineStrength: 500,
|
|
medicineStrengthUnit: 'mg',
|
|
medicineForm: 'tablet',
|
|
totalQuantity: 10,
|
|
unit: 'tablet',
|
|
itemCount: 1,
|
|
earliestExpiry: null,
|
|
},
|
|
],
|
|
});
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('All Items'));
|
|
await userEvent.click(screen.getByText('All Items'));
|
|
// Now in detail view
|
|
await waitFor(() => screen.getByText('Summary'));
|
|
await userEvent.click(screen.getByText('Summary'));
|
|
|
|
// Back in summary view — Metformin should show
|
|
await waitFor(() => expect(screen.getByText('Metformin')).toBeInTheDocument());
|
|
});
|
|
|
|
it('shows expiry date for cabinet items', async () => {
|
|
mockListCabinetItems.mockResolvedValue({
|
|
data: [
|
|
{
|
|
...cabinetItem,
|
|
expirationDate: '2099-12-31T00:00:00.000Z',
|
|
},
|
|
],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('All Items'));
|
|
await userEvent.click(screen.getByText('All Items'));
|
|
|
|
await waitFor(() => screen.getByText('Metformin'));
|
|
expect(screen.getByText(/days\)/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('filters cabinet items by status in detail view', async () => {
|
|
mockListCabinetItems.mockResolvedValue({
|
|
data: [cabinetItem],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('All Items'));
|
|
await userEvent.click(screen.getByText('All Items'));
|
|
|
|
await waitFor(() => screen.getByDisplayValue('All Statuses'));
|
|
fireEvent.change(screen.getByDisplayValue('All Statuses'), { target: { value: 'active' } });
|
|
|
|
await waitFor(() => expect(mockListCabinetItems).toHaveBeenCalledTimes(2));
|
|
});
|
|
|
|
it('takes 1 from cabinet item', async () => {
|
|
mockListCabinetItems.mockResolvedValue({
|
|
data: [cabinetItem],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
mockAdjustCabinetItemQuantity.mockResolvedValue({ ...cabinetItem, quantity: 9 });
|
|
mockGetCabinetSummary.mockResolvedValue(emptySummary);
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('All Items'));
|
|
await userEvent.click(screen.getByText('All Items'));
|
|
|
|
await waitFor(() => screen.getByTitle('Take 1'));
|
|
await userEvent.click(screen.getByTitle('Take 1'));
|
|
|
|
expect(mockAdjustCabinetItemQuantity).toHaveBeenCalledWith('hh1', 'ci-1', { delta: -1 });
|
|
});
|
|
|
|
it('collapses expanded medicine when it returns 0 items', async () => {
|
|
mockGetCabinetSummary.mockResolvedValue({
|
|
data: [
|
|
{
|
|
medicineId: 'med-1',
|
|
medicineName: 'Metformin',
|
|
medicineStrength: 500,
|
|
medicineStrengthUnit: 'mg',
|
|
medicineForm: 'tablet',
|
|
totalQuantity: 0,
|
|
unit: 'tablet',
|
|
itemCount: 0,
|
|
earliestExpiry: null,
|
|
},
|
|
],
|
|
});
|
|
mockListCabinetItems.mockResolvedValue({
|
|
data: [],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('Metformin'));
|
|
await userEvent.click(screen.getByText('Metformin').closest('button')!);
|
|
|
|
await waitFor(() =>
|
|
expect(mockListCabinetItems).toHaveBeenCalledWith(
|
|
'hh1',
|
|
expect.objectContaining({ medicineId: 'med-1' }),
|
|
),
|
|
);
|
|
});
|
|
|
|
it('shows error when createCabinetItem fails', async () => {
|
|
mockCreateCabinetItem.mockRejectedValue(new Error('Create failed'));
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('Add to Cabinet'));
|
|
await userEvent.click(screen.getByText('Add to Cabinet'));
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('Search medicines...'));
|
|
fireEvent.submit(screen.getByPlaceholderText('Search medicines...').closest('form')!);
|
|
|
|
// Should show 'Please select a medicine' validation first (since no medicine selected)
|
|
await waitFor(() => expect(screen.getByText('Please select a medicine')).toBeInTheDocument());
|
|
});
|
|
|
|
it('deletes cabinet item after confirmation', async () => {
|
|
mockListCabinetItems.mockResolvedValue({
|
|
data: [cabinetItem],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
mockDeleteCabinetItem.mockResolvedValue({});
|
|
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('All Items'));
|
|
await userEvent.click(screen.getByText('All Items'));
|
|
|
|
await waitFor(() => screen.getByTitle('Delete'));
|
|
await userEvent.click(screen.getByTitle('Delete'));
|
|
|
|
expect(mockDeleteCabinetItem).toHaveBeenCalledWith('hh1', 'ci-1');
|
|
});
|
|
|
|
it('shows error when adjust fails', async () => {
|
|
mockListCabinetItems.mockResolvedValue({
|
|
data: [cabinetItem],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
mockAdjustCabinetItemQuantity.mockRejectedValue(new Error('Adjust failed'));
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('All Items'));
|
|
await userEvent.click(screen.getByText('All Items'));
|
|
|
|
await waitFor(() => screen.getByTitle('Add 1'));
|
|
await userEvent.click(screen.getByTitle('Add 1'));
|
|
|
|
await waitFor(() => expect(screen.getByText('Adjust failed')).toBeInTheDocument());
|
|
});
|
|
|
|
it('shows error when delete fails', async () => {
|
|
mockListCabinetItems.mockResolvedValue({
|
|
data: [cabinetItem],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
mockDeleteCabinetItem.mockRejectedValue(new Error('Delete failed'));
|
|
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('All Items'));
|
|
await userEvent.click(screen.getByText('All Items'));
|
|
|
|
await waitFor(() => screen.getByTitle('Delete'));
|
|
await userEvent.click(screen.getByTitle('Delete'));
|
|
|
|
await waitFor(() => expect(screen.getByText('Delete failed')).toBeInTheDocument());
|
|
});
|
|
|
|
it('cancels AddToCabinetForm with internal Cancel button', async () => {
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('Add to Cabinet'));
|
|
await userEvent.click(screen.getByText('Add to Cabinet'));
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('Search medicines...'));
|
|
// Click the Cancel button inside the form (not the toggle button)
|
|
const cancelButtons = screen.getAllByText('Cancel');
|
|
await userEvent.click(cancelButtons[cancelButtons.length - 1]!);
|
|
|
|
expect(screen.queryByPlaceholderText('Search medicines...')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('collapses expanded medicine when clicked again', async () => {
|
|
const summary = {
|
|
medicineId: 'med-1',
|
|
medicineName: 'Metformin',
|
|
medicineStrength: 500,
|
|
medicineStrengthUnit: 'mg',
|
|
medicineForm: 'tablet',
|
|
totalQuantity: 10,
|
|
unit: 'tablet',
|
|
itemCount: 1,
|
|
earliestExpiry: null,
|
|
};
|
|
mockGetCabinetSummary.mockResolvedValue({ data: [summary] });
|
|
mockListCabinetItems.mockResolvedValue({
|
|
data: [cabinetItem],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('Metformin'));
|
|
// First click expands — handleExpand calls listCabinetItems
|
|
await userEvent.click(screen.getByText('Metformin').closest('button')!);
|
|
await waitFor(() => expect(mockListCabinetItems).toHaveBeenCalledTimes(1));
|
|
const callsAfterExpand = mockListCabinetItems.mock.calls.length;
|
|
|
|
// Second click collapses — no additional listCabinetItems calls
|
|
await userEvent.click(screen.getByText('Metformin').closest('button')!);
|
|
await waitFor(() => expect(screen.queryByTitle('Take 1')).not.toBeInTheDocument());
|
|
|
|
expect(mockListCabinetItems.mock.calls.length).toBe(callsAfterExpand);
|
|
});
|
|
|
|
it('shows empty items when expand fails', async () => {
|
|
mockGetCabinetSummary.mockResolvedValue({
|
|
data: [
|
|
{
|
|
medicineId: 'med-1',
|
|
medicineName: 'Metformin',
|
|
medicineStrength: 500,
|
|
medicineStrengthUnit: 'mg',
|
|
medicineForm: 'tablet',
|
|
totalQuantity: 10,
|
|
unit: 'tablet',
|
|
itemCount: 1,
|
|
earliestExpiry: null,
|
|
},
|
|
],
|
|
});
|
|
mockListCabinetItems.mockRejectedValue(new Error('Expand failed'));
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('Metformin'));
|
|
await userEvent.click(screen.getByText('Metformin').closest('button')!);
|
|
|
|
await waitFor(() => expect(mockListCabinetItems).toHaveBeenCalledTimes(1));
|
|
// No items should be shown (empty after error)
|
|
expect(screen.queryByTitle('Take 1')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('shows create error when medicine is selected and create fails', async () => {
|
|
mockListMedicines.mockResolvedValue({
|
|
data: [
|
|
{ _id: 'med-1', name: 'Metformin', form: 'tablet', strength: 500, strengthUnit: 'mg' },
|
|
],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
mockCreateCabinetItem.mockRejectedValue(new Error('Create failed'));
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('Add to Cabinet'));
|
|
await userEvent.click(screen.getByText('Add to Cabinet'));
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('Search medicines...'));
|
|
fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } });
|
|
fireEvent.submit(screen.getByPlaceholderText('Search medicines...').closest('form')!);
|
|
|
|
await waitFor(() => expect(screen.getByText('Create failed')).toBeInTheDocument());
|
|
});
|
|
|
|
it('shows item notes in detail view', async () => {
|
|
mockListCabinetItems.mockResolvedValue({
|
|
data: [{ ...cabinetItem, notes: 'Store in fridge' }],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('All Items'));
|
|
await userEvent.click(screen.getByText('All Items'));
|
|
|
|
await waitFor(() => expect(screen.getByText('Store in fridge')).toBeInTheDocument());
|
|
});
|
|
|
|
it('waits for medicines to load then selects medicine in form', async () => {
|
|
mockListMedicines.mockResolvedValue({
|
|
data: [
|
|
{ _id: 'med-1', name: 'Metformin', form: 'tablet', strength: 500, strengthUnit: 'mg' },
|
|
],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
await userEvent.click(screen.getByText('Add to Cabinet'));
|
|
|
|
// Wait for medicine options to load (covers medicines.map callback)
|
|
await waitFor(() =>
|
|
expect(
|
|
(screen.getAllByRole('combobox')[0] as HTMLSelectElement).options.length,
|
|
).toBeGreaterThan(1),
|
|
);
|
|
|
|
// Select a valid medicine (true branch: medForm is set → defaultUnitForForm called)
|
|
fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } });
|
|
|
|
// Select empty value (false branch: selectedMed undefined → DosageUnit.TABLET fallback)
|
|
fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: '' } });
|
|
|
|
expect(screen.getByPlaceholderText('Search medicines...')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows fallback error when non-Error is thrown during create', async () => {
|
|
mockListMedicines.mockResolvedValue({
|
|
data: [
|
|
{ _id: 'med-1', name: 'Metformin', form: 'tablet', strength: 500, strengthUnit: 'mg' },
|
|
],
|
|
pagination: { cursor: null, hasMore: false },
|
|
});
|
|
mockCreateCabinetItem.mockRejectedValue('unexpected');
|
|
|
|
render(<CabinetTab householdId="hh1" />, { wrapper });
|
|
|
|
await waitFor(() => screen.getByText('Add to Cabinet'));
|
|
await userEvent.click(screen.getByText('Add to Cabinet'));
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('Search medicines...'));
|
|
fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } });
|
|
fireEvent.submit(screen.getByPlaceholderText('Search medicines...').closest('form')!);
|
|
|
|
await waitFor(() => expect(screen.getByText('Failed to add item')).toBeInTheDocument());
|
|
});
|
|
});
|