import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import { SWRConfig } from 'swr'; const { mockUseApi } = vi.hoisted(() => ({ mockUseApi: vi.fn(), })); const { mockListPantryItems, mockTransitionPantryItem, mockDeletePantryItem } = vi.hoisted(() => ({ mockListPantryItems: vi.fn(), mockTransitionPantryItem: vi.fn(), mockDeletePantryItem: vi.fn(), })); vi.mock('@/lib/useApi', () => ({ useApi: mockUseApi })); vi.mock('@/services/pantry', () => ({ listPantryItems: mockListPantryItems, transitionPantryItem: mockTransitionPantryItem, deletePantryItem: mockDeletePantryItem, })); vi.mock('next/link', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any return { default: (props: any) => props.children }; }); import PantryPage from '../../../../src/app/(dashboard)/pantry/page'; beforeEach(() => vi.clearAllMocks()); const wrapper = ({ children }: { children: React.ReactNode }) => ( new Map(), dedupingInterval: 0 }}>{children} ); const SAMPLE_ITEM = { _id: 'pi-1', householdId: 'hh1', productId: 'p1', productName: 'Whole Milk', storageLocation: 'fridge', quantity: 1, unit: 'liter', purchaseDate: '2026-05-01T00:00:00.000Z', status: 'sealed', freshnessEstimate: { estimatedExpiryDate: '2026-05-15T00:00:00.000Z', daysRemaining: 11, urgency: 'fresh', source: 'rule', }, createdBy: 'u1', createdAt: '2026-05-01T00:00:00.000Z', updatedAt: '2026-05-01T00:00:00.000Z', }; describe('PantryPage', () => { it('shows loading skeleton when session is loading', () => { mockUseApi.mockReturnValue({ householdId: null, isLoading: true }); render(, { wrapper }); expect(screen.getByText('Pantry')).toBeInTheDocument(); expect(screen.queryByText('All')).not.toBeInTheDocument(); }); it('shows household prompt when no householdId', () => { mockUseApi.mockReturnValue({ householdId: null, isLoading: false }); render(, { wrapper }); expect(screen.getByText(/create or join a household/)).toBeInTheDocument(); }); it('renders pantry items when household exists', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListPantryItems.mockResolvedValue({ data: [SAMPLE_ITEM], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => { expect(screen.getByText('Whole Milk')).toBeInTheDocument(); }); }); it('shows empty state when no items', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListPantryItems.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => { expect(screen.getByText(/No pantry items yet/)).toBeInTheDocument(); }); }); it('shows freshness badge', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListPantryItems.mockResolvedValue({ data: [SAMPLE_ITEM], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => { expect(screen.getByText('Fresh')).toBeInTheDocument(); expect(screen.getByText('11d left')).toBeInTheDocument(); }); }); it('shows overdue text for negative days', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListPantryItems.mockResolvedValue({ data: [ { ...SAMPLE_ITEM, _id: 'pi-2', freshnessEstimate: { ...SAMPLE_ITEM.freshnessEstimate, daysRemaining: -2, urgency: 'expired', }, }, ], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => { expect(screen.getByText('2d overdue')).toBeInTheDocument(); expect(screen.getByText('Expired')).toBeInTheDocument(); }); }); it('shows transition buttons for active items', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListPantryItems.mockResolvedValue({ data: [SAMPLE_ITEM], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => { expect(screen.getByText('Open')).toBeInTheDocument(); expect(screen.getByText('Consume')).toBeInTheDocument(); expect(screen.getByText('Discard')).toBeInTheDocument(); }); }); it('handles transition click', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListPantryItems.mockResolvedValue({ data: [SAMPLE_ITEM], pagination: { cursor: null, hasMore: false }, }); mockTransitionPantryItem.mockResolvedValue({ ...SAMPLE_ITEM, status: 'opened', }); render(, { wrapper }); await waitFor(() => { expect(screen.getByText('Open')).toBeInTheDocument(); }); fireEvent.click(screen.getByText('Open')); await waitFor(() => { expect(mockTransitionPantryItem).toHaveBeenCalledWith('hh1', 'pi-1', { status: 'opened' }); }); }); it('handles delete with confirmation', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListPantryItems.mockResolvedValue({ data: [SAMPLE_ITEM], pagination: { cursor: null, hasMore: false }, }); mockDeletePantryItem.mockResolvedValue(undefined); vi.spyOn(window, 'confirm').mockReturnValue(true); render(, { wrapper }); await waitFor(() => { expect(screen.getByText('Delete')).toBeInTheDocument(); }); fireEvent.click(screen.getByText('Delete')); await waitFor(() => { expect(mockDeletePantryItem).toHaveBeenCalledWith('hh1', 'pi-1'); }); }); it('does not delete when confirm is cancelled', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListPantryItems.mockResolvedValue({ data: [SAMPLE_ITEM], pagination: { cursor: null, hasMore: false }, }); vi.spyOn(window, 'confirm').mockReturnValue(false); render(, { wrapper }); await waitFor(() => { expect(screen.getByText('Delete')).toBeInTheDocument(); }); fireEvent.click(screen.getByText('Delete')); expect(mockDeletePantryItem).not.toHaveBeenCalled(); }); it('filters by storage tab', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListPantryItems.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => { expect(screen.getByText('Fridge')).toBeInTheDocument(); }); fireEvent.click(screen.getByText('Fridge')); await waitFor(() => { expect(mockListPantryItems).toHaveBeenCalledWith( 'hh1', expect.objectContaining({ storageLocation: 'fridge' }), ); }); }); it('filters by status select', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListPantryItems.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false }, }); render(, { wrapper }); await waitFor(() => { expect(screen.getByDisplayValue('All statuses')).toBeInTheDocument(); }); fireEvent.change(screen.getByDisplayValue('All statuses'), { target: { value: 'opened' } }); await waitFor(() => { expect(mockListPantryItems).toHaveBeenCalledWith( 'hh1', expect.objectContaining({ status: 'opened' }), ); }); }); it('shows error when fetch fails', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListPantryItems.mockRejectedValue(new Error('Network error')); render(, { wrapper }); await waitFor(() => { expect(screen.getByText('Network error')).toBeInTheDocument(); }); }); it('shows error when transition fails', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListPantryItems.mockResolvedValue({ data: [SAMPLE_ITEM], pagination: { cursor: null, hasMore: false }, }); mockTransitionPantryItem.mockRejectedValue(new Error('Transition failed')); render(); await waitFor(() => { expect(screen.getByText('Open')).toBeInTheDocument(); }); fireEvent.click(screen.getByText('Open')); await waitFor(() => { expect(screen.getByText('Transition failed')).toBeInTheDocument(); }); }); it('shows filter empty state message', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListPantryItems.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false }, }); render(); // Click a filter tab to trigger filter state await waitFor(() => { expect(screen.getByText('Fridge')).toBeInTheDocument(); }); fireEvent.click(screen.getByText('Fridge')); await waitFor(() => { expect(screen.getByText(/No items match your filters/)).toBeInTheDocument(); }); }); });