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 { mockListFills, mockPreviewFill, mockExecuteFill, mockUndoFill } = vi.hoisted(() => ({
mockListFills: vi.fn(),
mockPreviewFill: vi.fn(),
mockExecuteFill: vi.fn(),
mockUndoFill: vi.fn(),
}));
const { mockListRegimens } = vi.hoisted(() => ({ mockListRegimens: vi.fn() }));
vi.mock('@/services/organizer', () => ({
listFills: mockListFills,
previewFill: mockPreviewFill,
executeFill: mockExecuteFill,
undoFill: mockUndoFill,
}));
vi.mock('@/services/regimens', () => ({ listRegimens: mockListRegimens }));
import { OrganizerTab } from '../../../../src/app/(dashboard)/medicines/OrganizerTab';
const emptyRegimens = { data: [], pagination: { cursor: null, hasMore: false } };
const emptyFills = { data: [], pagination: { cursor: null, hasMore: false } };
const activeRegimen = {
_id: 'reg-1',
name: 'Morning Routine',
isActive: true,
medications: [],
createdAt: '2026-01-01T00:00:00.000Z',
};
beforeEach(() => {
vi.clearAllMocks();
mockListRegimens.mockResolvedValue(emptyRegimens);
mockListFills.mockResolvedValue(emptyFills);
});
describe('OrganizerTab', () => {
it('shows fill organizer and fill history sections', async () => {
render();
await waitFor(() => expect(screen.getByText('Fill Pill Organizer')).toBeInTheDocument());
expect(screen.getByText('Fill History')).toBeInTheDocument();
});
it('shows no active regimens message when none exist', async () => {
render();
await waitFor(() => expect(screen.getByText(/No active regimens found/)).toBeInTheDocument());
});
it('shows fill form when active regimens exist', async () => {
mockListRegimens.mockResolvedValue({
data: [activeRegimen],
pagination: { cursor: null, hasMore: false },
});
render();
await waitFor(() => expect(screen.getByText('Preview fill')).toBeInTheDocument());
});
it('shows empty fill history', async () => {
render();
await waitFor(() => expect(screen.getByText('No fills recorded yet.')).toBeInTheDocument());
});
it('renders fill history entries', async () => {
mockListFills.mockResolvedValue({
data: [
{
_id: 'fill-1',
regimenId: 'reg-1',
regimenName: 'Morning Routine',
status: 'completed',
numberOfDays: 7,
items: [],
createdAt: '2026-01-01T00:00:00.000Z',
},
],
pagination: { cursor: null, hasMore: false },
});
render();
await waitFor(() => expect(screen.getByText('Morning Routine')).toBeInTheDocument());
});
it('previews fill and shows preview result', async () => {
mockListRegimens.mockResolvedValue({
data: [activeRegimen],
pagination: { cursor: null, hasMore: false },
});
mockPreviewFill.mockResolvedValue({
regimenId: 'reg-1',
numberOfDays: 7,
hasShortages: false,
items: [],
});
render();
await waitFor(() => screen.getByText('Preview fill'));
fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, {
target: { value: 'reg-1' },
});
fireEvent.change(screen.getByDisplayValue('7'), { target: { value: '14' } });
fireEvent.submit(screen.getByText('Preview fill').closest('form')!);
await waitFor(() =>
expect(mockPreviewFill).toHaveBeenCalledWith(
'hh1',
expect.objectContaining({ regimenId: 'reg-1' }),
),
);
});
it('shows preview result and executes fill', async () => {
mockListRegimens.mockResolvedValue({
data: [activeRegimen],
pagination: { cursor: null, hasMore: false },
});
mockPreviewFill.mockResolvedValue({
regimenId: 'reg-1',
regimenName: 'Morning Routine',
numberOfDays: 7,
hasShortages: false,
items: [
{
medicineId: 'med-1',
medicineName: 'Metformin',
quantityNeeded: 7,
quantityAvailable: 30,
isShort: false,
shortage: 0,
cabinetBreakdown: [],
},
],
});
mockExecuteFill.mockResolvedValue({ _id: 'fill-new', status: 'completed' });
render();
await waitFor(() => screen.getByText('Preview fill'));
fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, {
target: { value: 'reg-1' },
});
fireEvent.submit(screen.getByText('Preview fill').closest('form')!);
await waitFor(() => screen.getByText('Confirm fill'));
expect(screen.getByText('Metformin')).toBeInTheDocument();
await userEvent.click(screen.getByText('Confirm fill'));
await waitFor(() =>
expect(mockExecuteFill).toHaveBeenCalledWith(
'hh1',
expect.objectContaining({ regimenId: 'reg-1' }),
),
);
});
it('shows shortage warning in preview', async () => {
mockListRegimens.mockResolvedValue({
data: [activeRegimen],
pagination: { cursor: null, hasMore: false },
});
mockPreviewFill.mockResolvedValue({
regimenId: 'reg-1',
regimenName: 'Morning Routine',
numberOfDays: 7,
hasShortages: true,
items: [
{
medicineId: 'med-1',
medicineName: 'Metformin',
quantityNeeded: 7,
quantityAvailable: 3,
isShort: true,
shortage: 4,
cabinetBreakdown: [],
},
],
});
render();
await waitFor(() => screen.getByText('Preview fill'));
fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, {
target: { value: 'reg-1' },
});
fireEvent.submit(screen.getByText('Preview fill').closest('form')!);
await waitFor(() => screen.getByText('Shortages detected'));
expect(screen.getByText('Allow partial fill (fill what is available)')).toBeInTheDocument();
});
it('shows undo error when undo fails', async () => {
mockListFills.mockResolvedValue({
data: [
{
_id: 'fill-1',
regimenId: 'reg-1',
regimenName: 'Morning Routine',
status: 'completed',
numberOfDays: 7,
items: [],
createdAt: '2026-01-01T00:00:00.000Z',
},
],
pagination: { cursor: null, hasMore: false },
});
mockUndoFill.mockRejectedValue(new Error('Undo failed'));
vi.spyOn(window, 'confirm').mockReturnValue(true);
render();
await waitFor(() => screen.getByText('Undo'));
await userEvent.click(screen.getByText('Undo'));
await waitFor(() => expect(screen.getByText('Undo failed')).toBeInTheDocument());
});
it('filters fill history by status', async () => {
mockListFills.mockResolvedValue({
data: [
{
_id: 'fill-1',
regimenId: 'reg-1',
regimenName: 'Morning Routine',
status: 'completed',
numberOfDays: 7,
items: [],
createdAt: '2026-01-01T00:00:00.000Z',
},
],
pagination: { cursor: null, hasMore: false },
});
render();
await waitFor(() => screen.getByDisplayValue('All statuses'));
fireEvent.change(screen.getByDisplayValue('All statuses'), { target: { value: 'completed' } });
await waitFor(() => expect(mockListFills).toHaveBeenCalledTimes(2));
});
it('shows Ready to fill when no shortages', async () => {
mockListRegimens.mockResolvedValue({
data: [activeRegimen],
pagination: { cursor: null, hasMore: false },
});
mockPreviewFill.mockResolvedValue({
regimenId: 'reg-1',
regimenName: 'Morning Routine',
numberOfDays: 7,
hasShortages: false,
items: [
{
medicineId: 'med-1',
medicineName: 'Metformin',
quantityNeeded: 7,
quantityAvailable: 30,
isShort: false,
shortage: 0,
cabinetBreakdown: [],
},
],
});
render();
await waitFor(() => screen.getByText('Preview fill'));
fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, {
target: { value: 'reg-1' },
});
fireEvent.submit(screen.getByText('Preview fill').closest('form')!);
await waitFor(() => expect(screen.getByText('Ready to fill')).toBeInTheDocument());
});
it('renders fill items with short status', async () => {
mockListFills.mockResolvedValue({
data: [
{
_id: 'fill-1',
regimenId: 'reg-1',
regimenName: 'Morning Routine',
status: 'partial',
numberOfDays: 7,
fillDate: '2026-01-01T00:00:00.000Z',
items: [
{
medicineId: 'med-1',
medicineName: 'Metformin',
quantityNeeded: 7,
quantityTaken: 3,
wasShort: true,
shortage: 4,
deductions: [],
},
],
createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z',
},
],
pagination: { cursor: null, hasMore: false },
});
render();
await waitFor(() => expect(screen.getByText(/Metformin.*short/)).toBeInTheDocument());
});
it('dismisses fill history error', async () => {
mockListFills.mockRejectedValue(new Error('History error'));
render();
await waitFor(() => screen.getByText('History error'));
await userEvent.click(screen.getByText('Dismiss'));
expect(screen.queryByText('History error')).not.toBeInTheDocument();
});
it('undoes a fill after confirmation', async () => {
mockListFills.mockResolvedValue({
data: [
{
_id: 'fill-1',
regimenId: 'reg-1',
regimenName: 'Morning Routine',
status: 'completed',
numberOfDays: 7,
items: [],
createdAt: '2026-01-01T00:00:00.000Z',
},
],
pagination: { cursor: null, hasMore: false },
});
mockUndoFill.mockResolvedValue({});
vi.spyOn(window, 'confirm').mockReturnValue(true);
render();
await waitFor(() => screen.getByText('Undo'));
await userEvent.click(screen.getByText('Undo'));
expect(mockUndoFill).toHaveBeenCalledWith('hh1', 'fill-1');
});
it('cancels from preview screen', async () => {
mockListRegimens.mockResolvedValue({
data: [activeRegimen],
pagination: { cursor: null, hasMore: false },
});
mockPreviewFill.mockResolvedValue({
regimenId: 'reg-1',
regimenName: 'Morning Routine',
numberOfDays: 7,
hasShortages: false,
items: [],
});
render();
await waitFor(() => screen.getByText('Preview fill'));
fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, {
target: { value: 'reg-1' },
});
fireEvent.submit(screen.getByText('Preview fill').closest('form')!);
await waitFor(() => screen.getByText('Confirm fill'));
await userEvent.click(screen.getByRole('button', { name: 'Back' }));
// Should return to the form
await waitFor(() => expect(screen.getByText('Preview fill')).toBeInTheDocument());
});
it('shows cabinet breakdown in preview items', async () => {
mockListRegimens.mockResolvedValue({
data: [activeRegimen],
pagination: { cursor: null, hasMore: false },
});
mockPreviewFill.mockResolvedValue({
regimenId: 'reg-1',
regimenName: 'Morning Routine',
numberOfDays: 7,
hasShortages: false,
items: [
{
medicineId: 'med-1',
medicineName: 'Metformin',
quantityNeeded: 7,
quantityAvailable: 30,
isShort: false,
shortage: 0,
cabinetBreakdown: [
{
cabinetItemId: 'ci-1',
quantityToTake: 7,
expirationDate: '2027-06-01T00:00:00.000Z',
},
{ cabinetItemId: 'ci-2', quantityToTake: 3 },
],
},
],
});
render();
await waitFor(() => screen.getByText('Preview fill'));
fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, {
target: { value: 'reg-1' },
});
fireEvent.submit(screen.getByText('Preview fill').closest('form')!);
await waitFor(() => expect(screen.getByText(/7 units/)).toBeInTheDocument());
});
it('shows fallback error when non-Error thrown during preview', async () => {
mockListRegimens.mockResolvedValue({
data: [activeRegimen],
pagination: { cursor: null, hasMore: false },
});
mockPreviewFill.mockRejectedValue('preview failed');
render();
await waitFor(() => screen.getByText('Preview fill'));
fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, {
target: { value: 'reg-1' },
});
fireEvent.submit(screen.getByText('Preview fill').closest('form')!);
await waitFor(() => expect(screen.getByText('Failed to generate preview')).toBeInTheDocument());
});
it('shows fallback error when non-Error thrown during fill', async () => {
mockListRegimens.mockResolvedValue({
data: [activeRegimen],
pagination: { cursor: null, hasMore: false },
});
mockPreviewFill.mockResolvedValue({
regimenId: 'reg-1',
regimenName: 'Morning Routine',
numberOfDays: 7,
hasShortages: false,
items: [],
});
mockExecuteFill.mockRejectedValue('fill failed');
render();
await waitFor(() => screen.getByText('Preview fill'));
fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, {
target: { value: 'reg-1' },
});
fireEvent.submit(screen.getByText('Preview fill').closest('form')!);
await waitFor(() => screen.getByText('Confirm fill'));
await userEvent.click(screen.getByText('Confirm fill'));
await waitFor(() => expect(screen.getByText('Fill failed')).toBeInTheDocument());
});
it('toggles allow partial checkbox in shortage preview', async () => {
mockListRegimens.mockResolvedValue({
data: [activeRegimen],
pagination: { cursor: null, hasMore: false },
});
mockPreviewFill.mockResolvedValue({
regimenId: 'reg-1',
regimenName: 'Morning Routine',
numberOfDays: 7,
hasShortages: true,
items: [
{
medicineId: 'med-1',
medicineName: 'Metformin',
quantityNeeded: 7,
quantityAvailable: 3,
isShort: true,
shortage: 4,
cabinetBreakdown: [],
},
],
});
render();
await waitFor(() => screen.getByText('Preview fill'));
fireEvent.change(screen.getByDisplayValue('Select regimen...').closest('select')!, {
target: { value: 'reg-1' },
});
fireEvent.submit(screen.getByText('Preview fill').closest('form')!);
await waitFor(() => screen.getByText('Allow partial fill (fill what is available)'));
fireEvent.click(screen.getByLabelText('Allow partial fill (fill what is available)'));
expect(screen.getByLabelText('Allow partial fill (fill what is available)')).toBeChecked();
});
it('changes notes field in the organizer form', async () => {
mockListRegimens.mockResolvedValue({
data: [activeRegimen],
pagination: { cursor: null, hasMore: false },
});
render();
await waitFor(() => screen.getByPlaceholderText('Any notes for this fill'));
fireEvent.change(screen.getByPlaceholderText('Any notes for this fill'), {
target: { value: 'Fill for next week' },
});
expect(screen.getByDisplayValue('Fill for next week')).toBeInTheDocument();
});
});