2026-04-18 12:36:29 +09:00
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
|
|
|
import userEvent from '@testing-library/user-event';
|
2026-05-19 10:13:40 +09:00
|
|
|
import { SWRConfig } from 'swr';
|
2026-04-18 12:36:29 +09:00
|
|
|
|
2026-04-26 18:44:59 +09:00
|
|
|
const {
|
|
|
|
|
mockListRegimens,
|
|
|
|
|
mockCreateRegimen,
|
|
|
|
|
mockUpdateRegimen,
|
|
|
|
|
mockDeleteRegimen,
|
|
|
|
|
mockGetBurnRates,
|
|
|
|
|
} = vi.hoisted(() => ({
|
|
|
|
|
mockListRegimens: vi.fn(),
|
|
|
|
|
mockCreateRegimen: vi.fn(),
|
|
|
|
|
mockUpdateRegimen: vi.fn(),
|
|
|
|
|
mockDeleteRegimen: vi.fn(),
|
|
|
|
|
mockGetBurnRates: vi.fn(),
|
|
|
|
|
}));
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
const { mockListMedicines } = vi.hoisted(() => ({ mockListMedicines: vi.fn() }));
|
|
|
|
|
|
|
|
|
|
vi.mock('@/services/regimens', () => ({
|
|
|
|
|
listRegimens: mockListRegimens,
|
|
|
|
|
createRegimen: mockCreateRegimen,
|
|
|
|
|
updateRegimen: mockUpdateRegimen,
|
|
|
|
|
deleteRegimen: mockDeleteRegimen,
|
|
|
|
|
getBurnRates: mockGetBurnRates,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('@/services/medicines', () => ({ listMedicines: mockListMedicines }));
|
|
|
|
|
|
2026-05-19 11:06:03 +09:00
|
|
|
import { RegimensTab } from '../../../../src/app/(dashboard)/medicines/RegimensTab';
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
const emptyMeds = { data: [], pagination: { cursor: null, hasMore: false } };
|
|
|
|
|
const emptyRegimens = { data: [], pagination: { cursor: null, hasMore: false } };
|
|
|
|
|
|
|
|
|
|
const regimen = {
|
|
|
|
|
_id: 'reg-1',
|
|
|
|
|
name: 'Morning Routine',
|
|
|
|
|
isActive: true,
|
|
|
|
|
medications: [
|
|
|
|
|
{
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Metformin',
|
|
|
|
|
medicineStrength: 500,
|
|
|
|
|
medicineStrengthUnit: 'mg',
|
|
|
|
|
medicineForm: 'tablet',
|
|
|
|
|
dosage: 1,
|
|
|
|
|
dosageUnit: 'tablet',
|
|
|
|
|
frequency: 'once_daily',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
mockListRegimens.mockResolvedValue(emptyRegimens);
|
|
|
|
|
mockListMedicines.mockResolvedValue(emptyMeds);
|
|
|
|
|
mockGetBurnRates.mockResolvedValue({ data: [] });
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
|
|
|
<SWRConfig value={{ provider: () => new Map(), dedupingInterval: 0 }}>{children}</SWRConfig>
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-18 12:36:29 +09:00
|
|
|
describe('RegimensTab', () => {
|
|
|
|
|
it('shows empty state when no regimens', async () => {
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => expect(screen.getByText(/No regimens yet/)).toBeInTheDocument());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders regimen list', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockListRegimens.mockResolvedValue({
|
|
|
|
|
data: [regimen],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
2026-04-18 12:36:29 +09:00
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => expect(screen.getByText('Morning Routine')).toBeInTheDocument());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows error when list fails', async () => {
|
|
|
|
|
mockListRegimens.mockRejectedValue(new Error('Load failed'));
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => expect(screen.getByText('Load failed')).toBeInTheDocument());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('dismisses error', async () => {
|
|
|
|
|
mockListRegimens.mockRejectedValue(new Error('Load failed'));
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Load failed'));
|
|
|
|
|
await userEvent.click(screen.getByText('Dismiss'));
|
|
|
|
|
expect(screen.queryByText('Load failed')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('toggles new regimen form', async () => {
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New Regimen'));
|
|
|
|
|
await userEvent.click(screen.getByText('New Regimen'));
|
|
|
|
|
expect(screen.getByText(/New Regimen/)).toBeInTheDocument();
|
|
|
|
|
expect(screen.getByPlaceholderText('e.g. Morning routine')).toBeInTheDocument();
|
|
|
|
|
|
|
|
|
|
await userEvent.click(screen.getAllByText('Cancel')[0]);
|
|
|
|
|
expect(screen.queryByPlaceholderText('e.g. Morning routine')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('creates a regimen when form is submitted', async () => {
|
|
|
|
|
mockCreateRegimen.mockResolvedValue(regimen);
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New Regimen'));
|
|
|
|
|
await userEvent.click(screen.getAllByText('New Regimen')[0]);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('e.g. Morning routine'));
|
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('e.g. Morning routine'), {
|
|
|
|
|
target: { value: 'Evening Routine' },
|
|
|
|
|
});
|
|
|
|
|
fireEvent.submit(screen.getByPlaceholderText('e.g. Morning routine').closest('form')!);
|
|
|
|
|
|
|
|
|
|
await waitFor(() =>
|
|
|
|
|
expect(mockCreateRegimen).toHaveBeenCalledWith(
|
|
|
|
|
'hh1',
|
|
|
|
|
expect.objectContaining({ name: 'Evening Routine' }),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('deletes regimen after confirmation', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockListRegimens.mockResolvedValue({
|
|
|
|
|
data: [regimen],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
2026-04-18 12:36:29 +09:00
|
|
|
mockDeleteRegimen.mockResolvedValue({});
|
|
|
|
|
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Morning Routine'));
|
|
|
|
|
await userEvent.click(screen.getByTitle('Delete'));
|
|
|
|
|
|
|
|
|
|
expect(mockDeleteRegimen).toHaveBeenCalledWith('hh1', 'reg-1');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows burn rate section when toggled', async () => {
|
|
|
|
|
mockGetBurnRates.mockResolvedValue({ data: [] });
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Burn rate'));
|
|
|
|
|
await userEvent.click(screen.getByText('Burn rate'));
|
|
|
|
|
|
2026-04-26 18:44:59 +09:00
|
|
|
await waitFor(() =>
|
|
|
|
|
expect(screen.getByText('Burn Rate & Spending Projections')).toBeInTheDocument(),
|
|
|
|
|
);
|
2026-04-18 12:36:29 +09:00
|
|
|
expect(mockGetBurnRates).toHaveBeenCalledWith('hh1');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('opens edit form for regimen', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockListRegimens.mockResolvedValue({
|
|
|
|
|
data: [regimen],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
2026-04-18 12:36:29 +09:00
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Morning Routine'));
|
|
|
|
|
await userEvent.click(screen.getByTitle('Edit'));
|
|
|
|
|
|
|
|
|
|
expect(screen.getByDisplayValue('Morning Routine')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('saves edited regimen', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockListRegimens.mockResolvedValue({
|
|
|
|
|
data: [regimen],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
2026-04-18 12:36:29 +09:00
|
|
|
mockUpdateRegimen.mockResolvedValue({ ...regimen, name: 'Evening Routine' });
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Morning Routine'));
|
|
|
|
|
await userEvent.click(screen.getByTitle('Edit'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByDisplayValue('Morning Routine'));
|
2026-04-26 18:44:59 +09:00
|
|
|
fireEvent.change(screen.getByDisplayValue('Morning Routine'), {
|
|
|
|
|
target: { value: 'Evening Routine' },
|
|
|
|
|
});
|
2026-04-18 12:36:29 +09:00
|
|
|
fireEvent.submit(screen.getByDisplayValue('Evening Routine').closest('form')!);
|
|
|
|
|
|
|
|
|
|
await waitFor(() =>
|
2026-04-26 18:44:59 +09:00
|
|
|
expect(mockUpdateRegimen).toHaveBeenCalledWith(
|
|
|
|
|
'hh1',
|
|
|
|
|
'reg-1',
|
|
|
|
|
expect.objectContaining({ name: 'Evening Routine' }),
|
|
|
|
|
),
|
2026-04-18 12:36:29 +09:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows validation error when submitting regimen form with no medications', async () => {
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New Regimen'));
|
|
|
|
|
await userEvent.click(screen.getAllByText('New Regimen')[0]);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('e.g. Morning routine'));
|
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('e.g. Morning routine'), {
|
|
|
|
|
target: { value: 'My Regimen' },
|
|
|
|
|
});
|
|
|
|
|
// Remove the default medication
|
|
|
|
|
await userEvent.click(screen.getByTitle('Remove medication'));
|
|
|
|
|
// Submit with no medications
|
|
|
|
|
fireEvent.submit(screen.getByPlaceholderText('e.g. Morning routine').closest('form')!);
|
|
|
|
|
|
|
|
|
|
await waitFor(() =>
|
|
|
|
|
expect(screen.getByText('At least one medication is required.')).toBeInTheDocument(),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows error when delete fails', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockListRegimens.mockResolvedValue({
|
|
|
|
|
data: [regimen],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
2026-04-18 12:36:29 +09:00
|
|
|
mockDeleteRegimen.mockRejectedValue(new Error('Delete failed'));
|
|
|
|
|
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Morning Routine'));
|
|
|
|
|
await userEvent.click(screen.getByTitle('Delete'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(screen.getByText('Delete failed')).toBeInTheDocument());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows burn rate table with data', async () => {
|
|
|
|
|
mockGetBurnRates.mockResolvedValue({
|
|
|
|
|
data: [
|
|
|
|
|
{
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Metformin',
|
|
|
|
|
dailyConsumption: 2.0,
|
|
|
|
|
totalInCabinet: 60,
|
|
|
|
|
daysUntilEmpty: 30,
|
|
|
|
|
earliestExpiry: null,
|
|
|
|
|
avgUnitPrice: null,
|
|
|
|
|
projectedDailyCost: null,
|
|
|
|
|
projectedMonthlyCost: null,
|
|
|
|
|
projectedYearlyCost: null,
|
|
|
|
|
currency: null,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Burn rate'));
|
|
|
|
|
await userEvent.click(screen.getByText('Burn rate'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(screen.getByText('Metformin')).toBeInTheDocument());
|
|
|
|
|
expect(screen.getByText('2.00')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('cancels edit form and hides it', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockListRegimens.mockResolvedValue({
|
|
|
|
|
data: [regimen],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
2026-04-18 12:36:29 +09:00
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Morning Routine'));
|
|
|
|
|
await userEvent.click(screen.getByTitle('Edit'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByDisplayValue('Morning Routine'));
|
|
|
|
|
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
|
|
|
|
|
|
|
|
expect(screen.queryByDisplayValue('Morning Routine')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('filters regimens by active status', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockListRegimens.mockResolvedValue({
|
|
|
|
|
data: [regimen],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
2026-04-18 12:36:29 +09:00
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('All regimens'));
|
|
|
|
|
fireEvent.change(screen.getByDisplayValue('All regimens'), { target: { value: 'active' } });
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(mockListRegimens).toHaveBeenCalledTimes(2));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('adds a medication in the regimen form', async () => {
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New Regimen'));
|
|
|
|
|
await userEvent.click(screen.getAllByText('New Regimen')[0]);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('+ Add medication'));
|
|
|
|
|
await userEvent.click(screen.getByText('+ Add medication'));
|
|
|
|
|
|
|
|
|
|
// Now 2 medications — verify the button still shows
|
|
|
|
|
expect(screen.getByText('+ Add medication')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('toggles isActive checkbox in regimen form', async () => {
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New Regimen'));
|
|
|
|
|
await userEvent.click(screen.getAllByText('New Regimen')[0]);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByLabelText('Active'));
|
|
|
|
|
expect(screen.getByLabelText('Active')).toBeChecked();
|
|
|
|
|
fireEvent.click(screen.getByLabelText('Active'));
|
|
|
|
|
expect(screen.getByLabelText('Active')).not.toBeChecked();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('changes medicine, dosage, and unit in medication row', async () => {
|
|
|
|
|
mockListMedicines.mockResolvedValue({
|
2026-04-26 18:44:59 +09:00
|
|
|
data: [
|
|
|
|
|
{ _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' },
|
|
|
|
|
],
|
2026-04-18 12:36:29 +09:00
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New Regimen'));
|
|
|
|
|
await userEvent.click(screen.getAllByText('New Regimen')[0]);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Select medicine...'));
|
|
|
|
|
|
|
|
|
|
// Select a medicine in the medication row
|
2026-04-26 18:44:59 +09:00
|
|
|
const medicineSelect = screen
|
|
|
|
|
.getAllByRole('combobox')
|
|
|
|
|
.find(
|
|
|
|
|
(s) => (s as HTMLSelectElement).options[0]?.text === 'Select medicine...',
|
|
|
|
|
) as HTMLSelectElement;
|
2026-04-18 12:36:29 +09:00
|
|
|
expect(medicineSelect).toBeDefined();
|
|
|
|
|
fireEvent.change(medicineSelect!, { target: { value: 'med-1' } });
|
|
|
|
|
|
|
|
|
|
// Change dosage
|
|
|
|
|
const dosageInput = document.querySelector('input[min="0.01"]') as HTMLElement;
|
|
|
|
|
if (dosageInput) fireEvent.change(dosageInput, { target: { value: '2' } });
|
|
|
|
|
|
|
|
|
|
// Change dosage unit
|
2026-04-26 18:44:59 +09:00
|
|
|
const unitSelect = screen
|
|
|
|
|
.getAllByRole('combobox')
|
|
|
|
|
.find((s) => (s as HTMLSelectElement).value === 'tablet') as HTMLSelectElement;
|
2026-04-18 12:36:29 +09:00
|
|
|
if (unitSelect) fireEvent.change(unitSelect!, { target: { value: 'capsule' } });
|
|
|
|
|
|
|
|
|
|
expect(screen.getByPlaceholderText('e.g. Morning routine')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('changes instructions field in medication row', async () => {
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New Regimen'));
|
|
|
|
|
await userEvent.click(screen.getAllByText('New Regimen')[0]);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('e.g. Take with food'));
|
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('e.g. Take with food'), {
|
|
|
|
|
target: { value: 'With meals' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(screen.getByDisplayValue('With meals')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('changes frequency to custom and sets times per day', async () => {
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New Regimen'));
|
|
|
|
|
await userEvent.click(screen.getAllByText('New Regimen')[0]);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('e.g. Morning routine'));
|
|
|
|
|
|
|
|
|
|
// The frequency select has 'daily' as its first option value
|
2026-04-26 18:44:59 +09:00
|
|
|
const frequencySelect = screen
|
|
|
|
|
.getAllByRole('combobox')
|
|
|
|
|
.find((s) => (s as HTMLSelectElement).options[0]?.value === 'daily') as HTMLSelectElement;
|
2026-04-18 12:36:29 +09:00
|
|
|
expect(frequencySelect).toBeDefined();
|
|
|
|
|
fireEvent.change(frequencySelect!, { target: { value: 'custom' } });
|
|
|
|
|
|
|
|
|
|
// customFrequencyPerDay input should appear
|
|
|
|
|
await waitFor(() => expect(document.querySelector('input[min="1"][step="1"]')).not.toBeNull());
|
|
|
|
|
const timesPerDayInput = document.querySelector('input[min="1"][step="1"]') as HTMLElement;
|
|
|
|
|
fireEvent.change(timesPerDayInput, { target: { value: '3' } });
|
|
|
|
|
|
|
|
|
|
// Change back to non-custom (covers the undefined branch)
|
|
|
|
|
fireEvent.change(frequencySelect!, { target: { value: 'twice_daily' } });
|
|
|
|
|
|
|
|
|
|
expect(screen.getByPlaceholderText('e.g. Morning routine')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('changes time of day in medication row', async () => {
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New Regimen'));
|
|
|
|
|
await userEvent.click(screen.getAllByText('New Regimen')[0]);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('e.g. Morning routine'));
|
|
|
|
|
|
|
|
|
|
// The time-of-day select has 'Any time' as its first option text
|
2026-04-26 18:44:59 +09:00
|
|
|
const timeOfDaySelect = screen
|
|
|
|
|
.getAllByRole('combobox')
|
|
|
|
|
.find((s) => (s as HTMLSelectElement).options[0]?.text === 'Any time') as HTMLSelectElement;
|
2026-04-18 12:36:29 +09:00
|
|
|
expect(timeOfDaySelect).toBeDefined();
|
|
|
|
|
fireEvent.change(timeOfDaySelect!, { target: { value: 'morning' } });
|
|
|
|
|
|
|
|
|
|
expect(screen.getByPlaceholderText('e.g. Morning routine')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows error when burn rate fetch fails', async () => {
|
|
|
|
|
mockGetBurnRates.mockRejectedValue(new Error('Burn rate failed'));
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Burn rate'));
|
|
|
|
|
await userEvent.click(screen.getByText('Burn rate'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(screen.getByText('Burn rate failed')).toBeInTheDocument());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows error when update fails', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockListRegimens.mockResolvedValue({
|
|
|
|
|
data: [regimen],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
2026-04-18 12:36:29 +09:00
|
|
|
mockUpdateRegimen.mockRejectedValue(new Error('Update failed'));
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Morning Routine'));
|
|
|
|
|
await userEvent.click(screen.getByTitle('Deactivate'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(screen.getByText('Update failed')).toBeInTheDocument());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('toggles active/inactive status', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockListRegimens.mockResolvedValue({
|
|
|
|
|
data: [regimen],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
2026-04-18 12:36:29 +09:00
|
|
|
mockUpdateRegimen.mockResolvedValue({ ...regimen, isActive: false });
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Morning Routine'));
|
|
|
|
|
await userEvent.click(screen.getByTitle('Deactivate'));
|
|
|
|
|
|
|
|
|
|
expect(mockUpdateRegimen).toHaveBeenCalledWith('hh1', 'reg-1', { isActive: false });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('cancels new regimen form using internal Cancel button', async () => {
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New Regimen'));
|
|
|
|
|
await userEvent.click(screen.getAllByText('New Regimen')[0]);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('e.g. Morning routine'));
|
|
|
|
|
// The Cancel button inside the RegimenForm (not the toggle button)
|
|
|
|
|
const cancelButtons = screen.getAllByRole('button', { name: 'Cancel' });
|
|
|
|
|
await userEvent.click(cancelButtons[cancelButtons.length - 1]!);
|
|
|
|
|
|
|
|
|
|
expect(screen.queryByPlaceholderText('e.g. Morning routine')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('filters regimens by inactive status', async () => {
|
|
|
|
|
mockListRegimens.mockResolvedValue(emptyRegimens);
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => expect(mockListRegimens).toHaveBeenCalledTimes(1));
|
|
|
|
|
|
|
|
|
|
const filterSelect = screen.getByDisplayValue('All regimens');
|
|
|
|
|
fireEvent.change(filterSelect, { target: { value: 'inactive' } });
|
|
|
|
|
|
|
|
|
|
await waitFor(() =>
|
|
|
|
|
expect(mockListRegimens).toHaveBeenCalledWith(
|
|
|
|
|
'hh1',
|
|
|
|
|
expect.objectContaining({ isActive: false }),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows filtered empty state when filter is active and no results', async () => {
|
|
|
|
|
mockListRegimens.mockResolvedValue(emptyRegimens);
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByDisplayValue('All regimens'));
|
|
|
|
|
fireEvent.change(screen.getByDisplayValue('All regimens'), { target: { value: 'active' } });
|
|
|
|
|
|
2026-04-26 18:44:59 +09:00
|
|
|
await waitFor(() => expect(screen.getByText('No active regimens found.')).toBeInTheDocument());
|
2026-04-18 12:36:29 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows null when form is open and regimens list is empty', async () => {
|
|
|
|
|
mockListRegimens.mockResolvedValue(emptyRegimens);
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New Regimen'));
|
|
|
|
|
await userEvent.click(screen.getAllByText('New Regimen')[0]);
|
|
|
|
|
|
|
|
|
|
// Form is open with no regimens — empty state shows null (nothing visible in that spot)
|
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('e.g. Morning routine'));
|
|
|
|
|
expect(screen.queryByText('No regimens yet.')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows plural medications label when regimen has multiple meds', async () => {
|
|
|
|
|
const multiMedRegimen = {
|
|
|
|
|
...regimen,
|
|
|
|
|
medications: [
|
|
|
|
|
{ ...regimen.medications[0] },
|
|
|
|
|
{ ...regimen.medications[0], medicineId: 'med-2', medicineName: 'Aspirin' },
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
mockListRegimens.mockResolvedValue({
|
|
|
|
|
data: [multiMedRegimen],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => expect(screen.getByText(/2 medications/)).toBeInTheDocument());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows fallback error when non-Error thrown on burn rates', async () => {
|
|
|
|
|
mockGetBurnRates.mockRejectedValue('burn failed');
|
|
|
|
|
mockListRegimens.mockResolvedValue(emptyRegimens);
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Burn rate'));
|
|
|
|
|
await userEvent.click(screen.getByText('Burn rate'));
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
await waitFor(() => expect(screen.getByText('Failed to load data')).toBeInTheDocument());
|
2026-04-18 12:36:29 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows fallback error when non-Error thrown on create regimen', async () => {
|
|
|
|
|
mockCreateRegimen.mockRejectedValue('save failed');
|
|
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New Regimen'));
|
|
|
|
|
await userEvent.click(screen.getAllByText('New Regimen')[0]);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('e.g. Morning routine'));
|
|
|
|
|
await userEvent.type(screen.getByPlaceholderText('e.g. Morning routine'), 'Test');
|
|
|
|
|
fireEvent.submit(screen.getByPlaceholderText('e.g. Morning routine').closest('form')!);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(screen.getByText('Failed to save regimen')).toBeInTheDocument());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('initializes edit form with existing medications', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockListRegimens.mockResolvedValue({
|
|
|
|
|
data: [regimen],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
2026-04-18 12:36:29 +09:00
|
|
|
|
2026-05-19 10:13:40 +09:00
|
|
|
render(<RegimensTab householdId="hh1" />, { wrapper });
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Morning Routine'));
|
|
|
|
|
await userEvent.click(screen.getByTitle('Edit'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByDisplayValue('Morning Routine'));
|
|
|
|
|
// The medication row should be pre-filled — Remove button should be present
|
|
|
|
|
expect(screen.getByTitle('Remove medication')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|