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-04-26 18:44:59 +09:00
|
|
|
import type React from 'react';
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
const { mockUseApi } = vi.hoisted(() => ({ mockUseApi: vi.fn() }));
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
mockGetRefillAlerts,
|
|
|
|
|
mockListRefillLists,
|
|
|
|
|
mockCreateRefillList,
|
|
|
|
|
mockUpdateRefillList,
|
|
|
|
|
mockUpdateRefillListItem,
|
|
|
|
|
mockAddToCabinet,
|
|
|
|
|
} = vi.hoisted(() => ({
|
|
|
|
|
mockGetRefillAlerts: vi.fn(),
|
|
|
|
|
mockListRefillLists: vi.fn(),
|
|
|
|
|
mockCreateRefillList: vi.fn(),
|
|
|
|
|
mockUpdateRefillList: vi.fn(),
|
|
|
|
|
mockUpdateRefillListItem: vi.fn(),
|
|
|
|
|
mockAddToCabinet: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock('@/lib/useApi', () => ({ useApi: mockUseApi }));
|
|
|
|
|
vi.mock('@/services/refills', () => ({
|
|
|
|
|
getRefillAlerts: mockGetRefillAlerts,
|
|
|
|
|
listRefillLists: mockListRefillLists,
|
|
|
|
|
createRefillList: mockCreateRefillList,
|
|
|
|
|
updateRefillList: mockUpdateRefillList,
|
|
|
|
|
updateRefillListItem: mockUpdateRefillListItem,
|
|
|
|
|
addToCabinet: mockAddToCabinet,
|
|
|
|
|
}));
|
2026-04-26 18:44:59 +09:00
|
|
|
vi.mock('next/link', () => ({
|
|
|
|
|
default: (props: { href?: string; children: React.ReactNode }) => props.children,
|
|
|
|
|
}));
|
2026-04-18 12:36:29 +09:00
|
|
|
|
2026-05-19 11:06:03 +09:00
|
|
|
import RefillsPage from '../../../../src/app/(dashboard)/refills/page';
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
const emptyAlerts = { data: [], pagination: { cursor: null, hasMore: false } };
|
|
|
|
|
const emptyLists = { data: [], pagination: { cursor: null, hasMore: false } };
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
mockGetRefillAlerts.mockResolvedValue(emptyAlerts);
|
|
|
|
|
mockListRefillLists.mockResolvedValue(emptyLists);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('RefillsPage', () => {
|
|
|
|
|
it('shows loading skeleton when session loading', () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: null, isLoading: true });
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
expect(screen.getByText('Refills')).toBeInTheDocument();
|
|
|
|
|
expect(screen.queryByText('New Refill List')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows household prompt when no householdId', () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: null, isLoading: false });
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
expect(screen.getByText(/create or join a household/)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('renders Refills heading when householdId exists', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
expect(screen.getByText('Refills')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows empty state for alerts', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
render(<RefillsPage />);
|
2026-04-26 18:44:59 +09:00
|
|
|
await waitFor(() => expect(screen.getByText(/No medicines running low/)).toBeInTheDocument());
|
2026-04-18 12:36:29 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows empty state for refill lists', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
render(<RefillsPage />);
|
2026-04-26 18:44:59 +09:00
|
|
|
await waitFor(() => expect(screen.getByText(/No refill lists yet/)).toBeInTheDocument());
|
2026-04-18 12:36:29 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows error when alerts fail', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
mockGetRefillAlerts.mockRejectedValue(new Error('Alert error'));
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
await waitFor(() => expect(screen.getByText('Alert error')).toBeInTheDocument());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows New List button and toggles form', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New List'));
|
|
|
|
|
await userEvent.click(screen.getByText('New List'));
|
|
|
|
|
expect(screen.getByPlaceholderText('List name')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('creates a new refill list', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
mockCreateRefillList.mockResolvedValue({
|
|
|
|
|
_id: 'rl-1',
|
|
|
|
|
name: 'Test List',
|
|
|
|
|
status: 'active',
|
|
|
|
|
items: [],
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
createdBy: 'u1',
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New List'));
|
|
|
|
|
await userEvent.click(screen.getByText('New List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('List name'));
|
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('List name'), { target: { value: 'Test List' } });
|
|
|
|
|
fireEvent.submit(screen.getByPlaceholderText('List name').closest('form')!);
|
|
|
|
|
|
|
|
|
|
await waitFor(() =>
|
2026-04-26 18:44:59 +09:00
|
|
|
expect(mockCreateRefillList).toHaveBeenCalledWith(
|
|
|
|
|
'hh1',
|
|
|
|
|
expect.objectContaining({ name: 'Test List' }),
|
|
|
|
|
),
|
2026-04-18 12:36:29 +09:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows refill list and can select it', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
mockListRefillLists.mockResolvedValue({
|
|
|
|
|
data: [
|
|
|
|
|
{
|
|
|
|
|
_id: 'rl-1',
|
|
|
|
|
name: 'Weekly List',
|
|
|
|
|
status: 'active',
|
|
|
|
|
items: [
|
|
|
|
|
{
|
|
|
|
|
_id: 'item-1',
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Aspirin',
|
|
|
|
|
quantity: 30,
|
|
|
|
|
unit: 'tablet',
|
|
|
|
|
checked: false,
|
|
|
|
|
addedToCabinet: false,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
createdBy: 'u1',
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Weekly List'));
|
|
|
|
|
await userEvent.click(screen.getByText('Weekly List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(screen.getByText('Aspirin')).toBeInTheDocument());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('updates a refill list status', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
const refillList = {
|
|
|
|
|
_id: 'rl-1',
|
|
|
|
|
name: 'Weekly List',
|
|
|
|
|
status: 'active',
|
|
|
|
|
items: [],
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
createdBy: 'u1',
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
};
|
|
|
|
|
mockListRefillLists.mockResolvedValue({
|
|
|
|
|
data: [refillList],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
mockUpdateRefillList.mockResolvedValue({});
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Weekly List'));
|
|
|
|
|
await userEvent.click(screen.getByText('Weekly List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Start shopping'));
|
|
|
|
|
await userEvent.click(screen.getByText('Start shopping'));
|
|
|
|
|
|
|
|
|
|
expect(mockUpdateRefillList).toHaveBeenCalledWith('hh1', 'rl-1', { status: 'shopping' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows error when create list fails', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
mockCreateRefillList.mockRejectedValue(new Error('Create failed'));
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New List'));
|
|
|
|
|
await userEvent.click(screen.getByText('New List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('List name'));
|
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('List name'), { target: { value: 'Test' } });
|
|
|
|
|
fireEvent.submit(screen.getByPlaceholderText('List name').closest('form')!);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(screen.getByText('Create failed')).toBeInTheDocument());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('adds checked items to cabinet', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
const refillList = {
|
|
|
|
|
_id: 'rl-1',
|
|
|
|
|
name: 'Weekly List',
|
|
|
|
|
status: 'active',
|
|
|
|
|
items: [
|
|
|
|
|
{
|
|
|
|
|
_id: 'item-1',
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Aspirin',
|
|
|
|
|
quantity: 30,
|
|
|
|
|
unit: 'tablet',
|
|
|
|
|
checked: true,
|
|
|
|
|
addedToCabinet: false,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
createdBy: 'u1',
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
};
|
|
|
|
|
mockListRefillLists.mockResolvedValue({
|
|
|
|
|
data: [refillList],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
mockAddToCabinet.mockResolvedValue({ addedCount: 1 });
|
|
|
|
|
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
|
|
|
|
vi.spyOn(window, 'alert').mockImplementation(() => {});
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Weekly List'));
|
|
|
|
|
await userEvent.click(screen.getByText('Weekly List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText(/Add .* to Cabinet/));
|
|
|
|
|
await userEvent.click(screen.getByText(/Add .* to Cabinet/));
|
|
|
|
|
|
|
|
|
|
expect(mockAddToCabinet).toHaveBeenCalledWith('hh1', 'rl-1');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('generates a refill list from alerts', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
mockGetRefillAlerts.mockResolvedValue({
|
|
|
|
|
data: [
|
|
|
|
|
{
|
|
|
|
|
medicineId: 'm1',
|
|
|
|
|
medicineName: 'Aspirin',
|
|
|
|
|
medicineStrength: 500,
|
|
|
|
|
medicineStrengthUnit: 'mg',
|
|
|
|
|
currentStock: 5,
|
|
|
|
|
dailyConsumption: 1.5,
|
|
|
|
|
daysUntilEmpty: 3,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
mockCreateRefillList.mockResolvedValue({
|
|
|
|
|
_id: 'rl-gen',
|
|
|
|
|
name: 'Auto Refills',
|
|
|
|
|
status: 'active',
|
|
|
|
|
items: [],
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
createdBy: 'u1',
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Aspirin'));
|
|
|
|
|
await userEvent.click(screen.getByText('Generate Refill List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('List name, e.g. Weekly refills'));
|
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('List name, e.g. Weekly refills'), {
|
|
|
|
|
target: { value: 'Auto Refills' },
|
|
|
|
|
});
|
2026-04-26 18:44:59 +09:00
|
|
|
fireEvent.submit(
|
|
|
|
|
screen.getByPlaceholderText('List name, e.g. Weekly refills').closest('form')!,
|
|
|
|
|
);
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() =>
|
|
|
|
|
expect(mockCreateRefillList).toHaveBeenCalledWith(
|
|
|
|
|
'hh1',
|
|
|
|
|
expect.objectContaining({ name: 'Auto Refills', fromAlerts: true }),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('cancels create list form', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('New List'));
|
|
|
|
|
await userEvent.click(screen.getByText('New List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('List name'));
|
2026-04-26 18:44:59 +09:00
|
|
|
await userEvent.click(
|
|
|
|
|
screen.getAllByRole('button', { name: 'Cancel' })[
|
|
|
|
|
screen.getAllByRole('button', { name: 'Cancel' }).length - 1
|
|
|
|
|
]!,
|
|
|
|
|
);
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
expect(screen.queryByPlaceholderText('List name')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('closes the refill list detail panel', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
mockListRefillLists.mockResolvedValue({
|
|
|
|
|
data: [
|
|
|
|
|
{
|
|
|
|
|
_id: 'rl-1',
|
|
|
|
|
name: 'Weekly List',
|
|
|
|
|
status: 'active',
|
|
|
|
|
items: [],
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
createdBy: 'u1',
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Weekly List'));
|
|
|
|
|
await userEvent.click(screen.getByText('Weekly List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByTitle('Close'));
|
|
|
|
|
await userEvent.click(screen.getByTitle('Close'));
|
|
|
|
|
|
|
|
|
|
// After close, detail panel should not be visible (no Close button)
|
|
|
|
|
expect(screen.queryByTitle('Close')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('archives a refill list', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
const refillList = {
|
|
|
|
|
_id: 'rl-1',
|
|
|
|
|
name: 'Weekly List',
|
|
|
|
|
status: 'active',
|
|
|
|
|
items: [],
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
createdBy: 'u1',
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
};
|
|
|
|
|
mockListRefillLists.mockResolvedValue({
|
|
|
|
|
data: [refillList],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
mockUpdateRefillList.mockResolvedValue({});
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Weekly List'));
|
|
|
|
|
await userEvent.click(screen.getByText('Weekly List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Archive'));
|
|
|
|
|
await userEvent.click(screen.getByText('Archive'));
|
|
|
|
|
|
|
|
|
|
expect(mockUpdateRefillList).toHaveBeenCalledWith('hh1', 'rl-1', { status: 'archived' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows error when generate list fails', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
mockGetRefillAlerts.mockResolvedValue({
|
|
|
|
|
data: [
|
|
|
|
|
{
|
|
|
|
|
medicineId: 'm1',
|
|
|
|
|
medicineName: 'Aspirin',
|
|
|
|
|
medicineStrength: 500,
|
|
|
|
|
medicineStrengthUnit: 'mg',
|
|
|
|
|
currentStock: 5,
|
|
|
|
|
dailyConsumption: 1.5,
|
|
|
|
|
daysUntilEmpty: 3,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
mockCreateRefillList.mockRejectedValue(new Error('Generate failed'));
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Aspirin'));
|
|
|
|
|
await userEvent.click(screen.getByText('Generate Refill List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('List name, e.g. Weekly refills'));
|
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('List name, e.g. Weekly refills'), {
|
|
|
|
|
target: { value: 'Auto Refills' },
|
|
|
|
|
});
|
2026-04-26 18:44:59 +09:00
|
|
|
fireEvent.submit(
|
|
|
|
|
screen.getByPlaceholderText('List name, e.g. Weekly refills').closest('form')!,
|
|
|
|
|
);
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
await waitFor(() => expect(screen.getByText('Generate failed')).toBeInTheDocument());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('dismisses refill list panel error', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
mockListRefillLists.mockRejectedValue(new Error('List error'));
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('List error'));
|
|
|
|
|
await userEvent.click(screen.getByText('Dismiss'));
|
|
|
|
|
expect(screen.queryByText('List error')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('toggles item checked state', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
const refillList = {
|
|
|
|
|
_id: 'rl-1',
|
|
|
|
|
name: 'Weekly List',
|
|
|
|
|
status: 'active',
|
|
|
|
|
items: [
|
|
|
|
|
{
|
|
|
|
|
_id: 'item-1',
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Aspirin',
|
|
|
|
|
quantity: 30,
|
|
|
|
|
unit: 'tablet',
|
|
|
|
|
checked: false,
|
|
|
|
|
addedToCabinet: false,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
createdBy: 'u1',
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
};
|
|
|
|
|
mockListRefillLists.mockResolvedValue({
|
|
|
|
|
data: [refillList],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
mockUpdateRefillListItem.mockResolvedValue({
|
|
|
|
|
_id: 'rl-1',
|
|
|
|
|
name: 'Weekly List',
|
|
|
|
|
status: 'active',
|
|
|
|
|
items: [
|
|
|
|
|
{
|
|
|
|
|
_id: 'item-1',
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Aspirin',
|
|
|
|
|
quantity: 30,
|
|
|
|
|
unit: 'tablet',
|
|
|
|
|
checked: true,
|
|
|
|
|
addedToCabinet: false,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
createdBy: 'u1',
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Weekly List'));
|
|
|
|
|
await userEvent.click(screen.getByText('Weekly List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Aspirin'));
|
|
|
|
|
await userEvent.click(screen.getByRole('checkbox'));
|
|
|
|
|
|
2026-04-26 18:44:59 +09:00
|
|
|
expect(mockUpdateRefillListItem).toHaveBeenCalledWith('hh1', 'rl-1', 'item-1', {
|
|
|
|
|
checked: true,
|
|
|
|
|
});
|
2026-04-18 12:36:29 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('marks a shopping list as complete', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
const refillList = {
|
|
|
|
|
_id: 'rl-1',
|
|
|
|
|
name: 'Weekly List',
|
|
|
|
|
status: 'shopping',
|
|
|
|
|
items: [],
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
createdBy: 'u1',
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
};
|
|
|
|
|
mockListRefillLists.mockResolvedValue({
|
|
|
|
|
data: [refillList],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
mockUpdateRefillList.mockResolvedValue({});
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Weekly List'));
|
|
|
|
|
await userEvent.click(screen.getByText('Weekly List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Mark complete'));
|
|
|
|
|
await userEvent.click(screen.getByText('Mark complete'));
|
|
|
|
|
|
|
|
|
|
expect(mockUpdateRefillList).toHaveBeenCalledWith('hh1', 'rl-1', { status: 'completed' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('filters refill lists by status', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
mockListRefillLists.mockResolvedValue({
|
|
|
|
|
data: [
|
|
|
|
|
{
|
|
|
|
|
_id: 'rl-1',
|
|
|
|
|
name: 'Active List',
|
|
|
|
|
status: 'active',
|
|
|
|
|
items: [],
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
createdBy: 'u1',
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Active List'));
|
|
|
|
|
fireEvent.change(screen.getByDisplayValue('All statuses'), { target: { value: 'active' } });
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(mockListRefillLists).toHaveBeenCalledTimes(2));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows actual price input after checking item and allows price entry', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
const refillList = {
|
|
|
|
|
_id: 'rl-1',
|
|
|
|
|
name: 'Weekly List',
|
|
|
|
|
status: 'active',
|
|
|
|
|
items: [
|
|
|
|
|
{
|
|
|
|
|
_id: 'item-1',
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Aspirin',
|
|
|
|
|
quantity: 30,
|
|
|
|
|
unit: 'tablet',
|
|
|
|
|
checked: true,
|
|
|
|
|
addedToCabinet: false,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
createdBy: 'u1',
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
};
|
|
|
|
|
mockListRefillLists.mockResolvedValue({
|
|
|
|
|
data: [refillList],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Weekly List'));
|
|
|
|
|
await userEvent.click(screen.getByText('Weekly List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByPlaceholderText('Actual price'));
|
|
|
|
|
fireEvent.change(screen.getByPlaceholderText('Actual price'), { target: { value: '5.99' } });
|
|
|
|
|
|
|
|
|
|
expect(screen.getByPlaceholderText('Actual price')).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('dismisses panel-level error', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
const refillList = {
|
|
|
|
|
_id: 'rl-1',
|
|
|
|
|
name: 'Weekly List',
|
|
|
|
|
status: 'active',
|
|
|
|
|
items: [
|
|
|
|
|
{
|
|
|
|
|
_id: 'item-1',
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Aspirin',
|
|
|
|
|
quantity: 30,
|
|
|
|
|
unit: 'tablet',
|
|
|
|
|
checked: true,
|
|
|
|
|
addedToCabinet: false,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
createdBy: 'u1',
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
};
|
|
|
|
|
mockListRefillLists.mockResolvedValue({
|
|
|
|
|
data: [refillList],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
mockAddToCabinet.mockRejectedValue(new Error('Cabinet error'));
|
|
|
|
|
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Weekly List'));
|
|
|
|
|
await userEvent.click(screen.getByText('Weekly List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText(/Add .* to Cabinet/));
|
|
|
|
|
await userEvent.click(screen.getByText(/Add .* to Cabinet/));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Cabinet error'));
|
|
|
|
|
// Dismiss via inline button in the panel
|
|
|
|
|
const dismissBtns = screen.getAllByText('Dismiss');
|
|
|
|
|
await userEvent.click(dismissBtns[dismissBtns.length - 1]!);
|
|
|
|
|
|
|
|
|
|
expect(screen.queryByText('Cabinet error')).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows fallback error when non-Error thrown on update item', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
const refillList = {
|
|
|
|
|
_id: 'rl-1',
|
|
|
|
|
name: 'Weekly List',
|
|
|
|
|
status: 'active',
|
|
|
|
|
items: [
|
|
|
|
|
{
|
|
|
|
|
_id: 'item-1',
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Aspirin',
|
|
|
|
|
quantity: 30,
|
|
|
|
|
unit: 'tablet',
|
|
|
|
|
checked: false,
|
|
|
|
|
addedToCabinet: false,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
createdBy: 'u1',
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
};
|
|
|
|
|
mockListRefillLists.mockResolvedValue({
|
|
|
|
|
data: [refillList],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
mockUpdateRefillListItem.mockRejectedValue('update failed');
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Weekly List'));
|
|
|
|
|
await userEvent.click(screen.getByText('Weekly List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Aspirin'));
|
|
|
|
|
await userEvent.click(screen.getAllByRole('checkbox')[0]!);
|
|
|
|
|
|
2026-04-26 18:44:59 +09:00
|
|
|
await waitFor(() => expect(screen.getByText('Failed to update item')).toBeInTheDocument());
|
2026-04-18 12:36:29 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows fallback error when non-Error thrown on update status', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
const refillList = {
|
|
|
|
|
_id: 'rl-1',
|
|
|
|
|
name: 'Weekly List',
|
|
|
|
|
status: 'active',
|
|
|
|
|
items: [],
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
createdBy: 'u1',
|
|
|
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
};
|
|
|
|
|
mockListRefillLists.mockResolvedValue({
|
|
|
|
|
data: [refillList],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
mockUpdateRefillList.mockRejectedValue('status failed');
|
|
|
|
|
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Weekly List'));
|
|
|
|
|
await userEvent.click(screen.getByText('Weekly List'));
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByText('Start shopping'));
|
|
|
|
|
await userEvent.click(screen.getByText('Start shopping'));
|
|
|
|
|
|
2026-04-26 18:44:59 +09:00
|
|
|
await waitFor(() => expect(screen.getByText('Failed to update status')).toBeInTheDocument());
|
2026-04-18 12:36:29 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('shows refill alert when present', async () => {
|
|
|
|
|
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
|
|
|
|
mockGetRefillAlerts.mockResolvedValue({
|
|
|
|
|
data: [
|
|
|
|
|
{
|
|
|
|
|
medicineId: 'm1',
|
|
|
|
|
medicineName: 'Aspirin',
|
|
|
|
|
medicineStrength: 500,
|
|
|
|
|
medicineStrengthUnit: 'mg',
|
|
|
|
|
currentStock: 5,
|
|
|
|
|
dailyConsumption: 1.5,
|
|
|
|
|
daysUntilEmpty: 3,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
pagination: { cursor: null, hasMore: false },
|
|
|
|
|
});
|
|
|
|
|
render(<RefillsPage />);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => expect(screen.getByText('Aspirin')).toBeInTheDocument());
|
|
|
|
|
});
|
|
|
|
|
});
|