524 lines
16 KiB
TypeScript
524 lines
16 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 { 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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
await waitFor(() => expect(screen.getByText('Preview fill')).toBeInTheDocument());
|
|
});
|
|
|
|
it('shows empty fill history', async () => {
|
|
render(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
await waitFor(() => expect(screen.getByText(/Metformin.*short/)).toBeInTheDocument());
|
|
});
|
|
|
|
it('dismisses fill history error', async () => {
|
|
mockListFills.mockRejectedValue(new Error('History error'));
|
|
|
|
render(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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(<OrganizerTab householdId="hh1" />);
|
|
|
|
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();
|
|
});
|
|
});
|