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 { mockUseApi } = vi.hoisted(() => ({ mockUseApi: vi.fn(), })); const { mockListStores, mockCreateStore, mockUpdateStore, mockDeactivateStore } = vi.hoisted( () => ({ mockListStores: vi.fn(), mockCreateStore: vi.fn(), mockUpdateStore: vi.fn(), mockDeactivateStore: vi.fn(), }), ); vi.mock('@/lib/useApi', () => ({ useApi: mockUseApi })); vi.mock('@/services/stores', () => ({ listStores: mockListStores, createStore: mockCreateStore, updateStore: mockUpdateStore, deactivateStore: mockDeactivateStore, })); vi.mock('next/link', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any return { default: (props: any) => props.children }; }); import StoresPage from '../../../../src/app/(dashboard)/stores/page'; beforeEach(() => vi.clearAllMocks()); describe('StoresPage', () => { it('shows loading skeleton when session is loading', () => { mockUseApi.mockReturnValue({ householdId: null, isLoading: true }); render(); expect(screen.getByText('Stores')).toBeInTheDocument(); expect(screen.queryByText('Add Store')).not.toBeInTheDocument(); }); it('shows household prompt when no householdId', () => { mockUseApi.mockReturnValue({ householdId: null, isLoading: false }); render(); expect(screen.getByText(/create or join a household/)).toBeInTheDocument(); }); it('renders store list when household exists', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [ { _id: 'st-1', name: 'Walgreens', tags: ['pharmacy'], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1', }, ], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => { expect(screen.getByText('Walgreens')).toBeInTheDocument(); }); }); it('shows empty state when no stores', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => { expect(screen.getByText(/No stores yet/)).toBeInTheDocument(); }); }); it('shows Add Store form when button clicked', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => { expect(screen.getByText('Add Store')).toBeInTheDocument(); }); await userEvent.click(screen.getByText('Add Store')); expect(screen.getByPlaceholderText('e.g. Walgreens')).toBeInTheDocument(); }); it('shows error when store list fails to load', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockRejectedValue(new Error('Network error')); render(); await waitFor(() => { expect(screen.getByText('Network error')).toBeInTheDocument(); }); }); it('deactivates store after confirmation', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [ { _id: 'st-1', name: 'CVS', tags: [], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1', }, ], pagination: { cursor: null, hasMore: false }, }); mockDeactivateStore.mockResolvedValue({}); vi.spyOn(window, 'confirm').mockReturnValue(true); render(); await waitFor(() => { expect(screen.getByText('CVS')).toBeInTheDocument(); }); await userEvent.click(screen.getByTitle('Deactivate')); expect(mockDeactivateStore).toHaveBeenCalledWith('hh1', 'st-1'); }); it('creates a store on form submit', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } }); mockCreateStore.mockResolvedValue({ _id: 'st-new', name: 'Walmart', tags: [], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1', }); render(); await waitFor(() => screen.getByText('Add Store')); await userEvent.click(screen.getByText('Add Store')); await waitFor(() => screen.getByPlaceholderText('e.g. Walgreens')); fireEvent.change(screen.getByPlaceholderText('e.g. Walgreens'), { target: { value: 'Walmart' }, }); fireEvent.submit(screen.getByPlaceholderText('e.g. Walgreens').closest('form')!); await waitFor(() => expect(mockCreateStore).toHaveBeenCalledWith( 'hh1', expect.objectContaining({ name: 'Walmart' }), ), ); }); it('shows error when create store fails', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } }); mockCreateStore.mockRejectedValue(new Error('Store already exists')); render(); await waitFor(() => screen.getByText('Add Store')); await userEvent.click(screen.getByText('Add Store')); await waitFor(() => screen.getByPlaceholderText('e.g. Walgreens')); fireEvent.change(screen.getByPlaceholderText('e.g. Walgreens'), { target: { value: 'Walmart' }, }); fireEvent.submit(screen.getByPlaceholderText('e.g. Walgreens').closest('form')!); await waitFor(() => expect(screen.getByText('Store already exists')).toBeInTheDocument()); }); it('opens edit form for a store', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [ { _id: 'st-1', name: 'CVS', tags: [], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1', }, ], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => screen.getByText('CVS')); await userEvent.click(screen.getByTitle('Edit')); expect(screen.getByDisplayValue('CVS')).toBeInTheDocument(); expect(screen.getByText('Edit Store')).toBeInTheDocument(); }); it('saves edited store', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [ { _id: 'st-1', name: 'CVS', tags: [], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1', }, ], pagination: { cursor: null, hasMore: false }, }); mockUpdateStore.mockResolvedValue({}); render(); await waitFor(() => screen.getByText('CVS')); await userEvent.click(screen.getByTitle('Edit')); await waitFor(() => screen.getByDisplayValue('CVS')); fireEvent.change(screen.getByDisplayValue('CVS'), { target: { value: 'CVS Pharmacy' } }); fireEvent.submit(screen.getByDisplayValue('CVS Pharmacy').closest('form')!); await waitFor(() => expect(mockUpdateStore).toHaveBeenCalledWith( 'hh1', 'st-1', expect.objectContaining({ name: 'CVS Pharmacy' }), ), ); }); it('dismisses error', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockRejectedValue(new Error('Network error')); render(); await waitFor(() => screen.getByText('Network error')); await userEvent.click(screen.getByText('Dismiss')); expect(screen.queryByText('Network error')).not.toBeInTheDocument(); }); it('shows error when deactivate fails', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [ { _id: 'st-1', name: 'CVS', tags: [], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1', }, ], pagination: { cursor: null, hasMore: false }, }); mockDeactivateStore.mockRejectedValue(new Error('Deactivate failed')); vi.spyOn(window, 'confirm').mockReturnValue(true); render(); await waitFor(() => screen.getByTitle('Deactivate')); await userEvent.click(screen.getByTitle('Deactivate')); await waitFor(() => expect(screen.getByText('Deactivate failed')).toBeInTheDocument()); }); it('cancels the create store form', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } }); render(); await waitFor(() => screen.getByText('Add Store')); await userEvent.click(screen.getByText('Add Store')); await waitFor(() => screen.getByPlaceholderText('e.g. Walgreens')); await userEvent.click( screen.getAllByRole('button', { name: 'Cancel' })[ screen.getAllByRole('button', { name: 'Cancel' }).length - 1 ]!, ); expect(screen.queryByPlaceholderText('e.g. Walgreens')).not.toBeInTheDocument(); }); it('cancels the edit store form', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [ { _id: 'st-1', name: 'CVS', tags: [], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1', }, ], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => screen.getByText('CVS')); await userEvent.click(screen.getByTitle('Edit')); await waitFor(() => screen.getByDisplayValue('CVS')); await userEvent.click(screen.getByRole('button', { name: 'Cancel' })); expect(screen.queryByDisplayValue('CVS')).not.toBeInTheDocument(); }); it('filters stores by tag', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [ { _id: 'st-1', name: 'Walgreens', tags: ['pharmacy'], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1', }, { _id: 'st-2', name: 'Costco', tags: ['supermarket'], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1', }, ], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => screen.getByText('Walgreens')); expect(screen.getByText('Costco')).toBeInTheDocument(); fireEvent.change(screen.getByDisplayValue('All tags'), { target: { value: 'pharmacy' } }); await waitFor(() => expect(screen.queryByText('Costco')).not.toBeInTheDocument()); expect(screen.getByText('Walgreens')).toBeInTheDocument(); }); it('changes store form fields (notes, address, url)', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } }); render(); await waitFor(() => screen.getByText('Add Store')); await userEvent.click(screen.getByText('Add Store')); await waitFor(() => screen.getByPlaceholderText('e.g. Walgreens')); fireEvent.change(screen.getByPlaceholderText('123 Main St'), { target: { value: '456 Oak Ave' }, }); fireEvent.change(screen.getByPlaceholderText('Any notes'), { target: { value: 'Good prices' }, }); expect(screen.getByPlaceholderText('e.g. Walgreens')).toBeInTheDocument(); }); it('toggles a preset tag and adds a custom tag', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } }); render(); await waitFor(() => screen.getByText('Add Store')); await userEvent.click(screen.getByText('Add Store')); await waitFor(() => screen.getByPlaceholderText('Custom tag...')); // Toggle a preset tag (e.g. 'pharmacy') await userEvent.click(screen.getByRole('button', { name: 'pharmacy' })); // Toggle it off again await userEvent.click(screen.getByRole('button', { name: 'pharmacy' })); // Add a custom tag via button fireEvent.change(screen.getByPlaceholderText('Custom tag...'), { target: { value: 'mytag' } }); await userEvent.click(screen.getByRole('button', { name: 'Add' })); await waitFor(() => expect(screen.getByText('mytag')).toBeInTheDocument()); // Remove it via the × button await userEvent.click(screen.getByText('×')); expect(screen.queryByText('mytag')).not.toBeInTheDocument(); }); it('adds custom tag via Enter key', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } }); render(); await waitFor(() => screen.getByText('Add Store')); await userEvent.click(screen.getByText('Add Store')); await waitFor(() => screen.getByPlaceholderText('Custom tag...')); fireEvent.change(screen.getByPlaceholderText('Custom tag...'), { target: { value: 'keytag' } }); fireEvent.keyDown(screen.getByPlaceholderText('Custom tag...'), { key: 'Enter' }); await waitFor(() => expect(screen.getByText('keytag')).toBeInTheDocument()); }); it('changes search filter', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [ { _id: 'st-1', name: 'Walgreens', tags: [], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1', }, { _id: 'st-2', name: 'CVS', tags: [], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1', }, ], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => screen.getByText('Walgreens')); fireEvent.change(screen.getByPlaceholderText('Search stores...'), { target: { value: 'wal' } }); await waitFor(() => expect(screen.queryByText('CVS')).not.toBeInTheDocument()); expect(screen.getByText('Walgreens')).toBeInTheDocument(); }); it('toggles isActive in edit form', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [ { _id: 'st-1', name: 'CVS', tags: [], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1', }, ], pagination: { cursor: null, hasMore: false }, }); mockUpdateStore.mockResolvedValue({}); render(); await waitFor(() => screen.getByText('CVS')); await userEvent.click(screen.getByTitle('Edit')); await waitFor(() => screen.getByLabelText('Active')); fireEvent.click(screen.getByLabelText('Active')); expect(screen.getByLabelText('Active')).not.toBeChecked(); }); it('shows inactive stores when toggle checked', async () => { mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false }); mockListStores.mockResolvedValue({ data: [ { _id: 'st-1', name: 'Active Store', tags: [], isActive: true, createdAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1', }, { _id: 'st-2', name: 'Old Store', tags: [], isActive: false, createdAt: '2026-01-01T00:00:00.000Z', householdId: 'hh1', createdBy: 'u-1', }, ], pagination: { cursor: null, hasMore: false }, }); render(); await waitFor(() => screen.getByText('Active Store')); expect(screen.queryByText('Old Store')).not.toBeInTheDocument(); fireEvent.click(screen.getByLabelText('Show inactive')); expect(screen.getByText('Old Store')).toBeInTheDocument(); }); });