import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, fireEvent, waitFor, cleanup, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import AddIngredientModal from '@/app/dashboard/ingredients/AddIngredientModal'; const mockCategories = [ { _id: 'cat1', name: 'Dairy' }, { _id: 'cat2', name: 'Vegetables' }, ]; const mockSubcategories = [ { _id: 'sub1', name: 'Fresh', categoryId: 'cat1' }, { _id: 'sub2', name: 'Frozen', categoryId: 'cat2' }, ]; const mockSuppliers = [ { _id: 'sup1', name: 'Acme Foods' }, ]; function setupFetchMock(overrides: Record = {}) { vi.spyOn(global, 'fetch').mockImplementation(async (input, init) => { const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : (input as RequestInfo).toString(); const method = init?.method ?? 'GET'; if (/\/api\/ingredients\/[a-zA-Z0-9]+$/.test(url) && method === 'GET') { return { ok: true, json: () => Promise.resolve(overrides.ingredient ?? {}), } as Response; } if (url.endsWith('/api/ingredients') || /\/api\/ingredients\/[a-zA-Z0-9]+$/.test(url)) { if (method === 'POST' || method === 'PUT') { return { ok: overrides.saveOk !== false, status: overrides.saveOk === false ? 400 : 201, json: () => Promise.resolve(overrides.saveResponse ?? { _id: 'new-id' }), } as Response; } } if (url.endsWith('/api/categories')) { return { ok: true, json: () => Promise.resolve(overrides.categories ?? mockCategories) } as Response; } if (url.endsWith('/api/subcategories')) { return { ok: true, json: () => Promise.resolve(overrides.subcategories ?? mockSubcategories) } as Response; } if (url.endsWith('/api/suppliers')) { return { ok: true, json: () => Promise.resolve(overrides.suppliers ?? mockSuppliers) } as Response; } return { ok: true, json: () => Promise.resolve({}) } as Response; }); } function getInputByLabel(container: HTMLElement, labelText: string): HTMLInputElement { const labels = container.querySelectorAll('label'); for (const label of labels) { if (label.textContent?.trim() === labelText) { const input = label.parentElement?.querySelector('input, select, textarea'); if (input) return input as HTMLInputElement; } } throw new Error(`Could not find input for label "${labelText}"`); } async function fillRequiredFields(container: HTMLElement, user: ReturnType) { const nameInput = within(container).getByPlaceholderText('e.g. Organic Butter'); await user.type(nameInput, 'Test Ingredient'); const categorySelect = within(container).getByDisplayValue('Select category\u2026'); fireEvent.change(categorySelect, { target: { value: 'cat1' } }); await waitFor(() => { const subSelects = within(container).getAllByDisplayValue('Select subcategory\u2026'); expect(subSelects[0]).not.toBeDisabled(); }); const subSelect = within(container).getAllByDisplayValue('Select subcategory\u2026')[0]; fireEvent.change(subSelect, { target: { value: 'sub1' } }); const quantityInput = getInputByLabel(container, 'Quantity'); await user.type(quantityInput, '5'); const unitInput = within(container).getByPlaceholderText('kg, L, pcs\u2026'); await user.type(unitInput, 'kg'); const grossInput = getInputByLabel(container, 'Gross Price (\u20AC)'); await user.type(grossInput, '10'); const supplierSelect = within(container).getByDisplayValue('Select supplier\u2026'); fireEvent.change(supplierSelect, { target: { value: 'sup1' } }); } describe('AddIngredientModal', () => { const defaultProps = { open: true, onClose: vi.fn(), onSaved: vi.fn(), }; beforeEach(() => { vi.clearAllMocks(); vi.restoreAllMocks(); }); afterEach(() => { cleanup(); }); it('renders nothing when open=false', () => { setupFetchMock(); const { container } = render( ); expect(container.innerHTML).toBe(''); }); it('loads reference data on open', async () => { setupFetchMock(); const { container } = render(); await waitFor(() => { expect(within(container).getByText('Dairy')).toBeInTheDocument(); }); }); it('shows title "Add Ingredient" in add mode', () => { setupFetchMock(); const { container } = render(); expect(within(container).getByText('Add Ingredient')).toBeInTheDocument(); }); it('shows title "Edit Ingredient" in edit mode', () => { setupFetchMock({ ingredient: { _id: 'ing1', name: 'Butter', categoryId: 'cat1', subcategoryId: 'sub1', quantity: 10, unit: 'kg', unitPrice: 5, vat: 9, supplierId: 'sup1', }, }); const { container } = render(); expect(within(container).getByText('Edit Ingredient')).toBeInTheDocument(); }); it('shows validation error for empty name', async () => { setupFetchMock(); const { container } = render(); await waitFor(() => { expect(within(container).getByText('Dairy')).toBeInTheDocument(); }); fireEvent.click(within(container).getByText('Save')); await waitFor(() => { expect(within(container).getByText('Ingredient name is required.')).toBeInTheDocument(); }); }); it('shows validation error for no category', async () => { setupFetchMock(); const user = userEvent.setup(); const { container } = render(); await waitFor(() => { expect(within(container).getByText('Dairy')).toBeInTheDocument(); }); const nameInput = within(container).getByPlaceholderText('e.g. Organic Butter'); await user.type(nameInput, 'Test'); fireEvent.click(within(container).getByText('Save')); await waitFor(() => { expect(within(container).getByText('Please select a category.')).toBeInTheDocument(); }); }); it('shows validation error for no subcategory', async () => { setupFetchMock(); const user = userEvent.setup(); const { container } = render(); await waitFor(() => { expect(within(container).getByText('Dairy')).toBeInTheDocument(); }); const nameInput = within(container).getByPlaceholderText('e.g. Organic Butter'); await user.type(nameInput, 'Test'); fireEvent.change(within(container).getByDisplayValue('Select category\u2026'), { target: { value: 'cat1' } }); fireEvent.click(within(container).getByText('Save')); await waitFor(() => { expect(within(container).getByText('Please select a subcategory.')).toBeInTheDocument(); }); }); it('shows validation error for quantity <= 0', async () => { setupFetchMock(); const user = userEvent.setup(); const { container } = render(); await waitFor(() => { expect(within(container).getByText('Dairy')).toBeInTheDocument(); }); await user.type(within(container).getByPlaceholderText('e.g. Organic Butter'), 'Test'); fireEvent.change(within(container).getByDisplayValue('Select category\u2026'), { target: { value: 'cat1' } }); await waitFor(() => { expect(within(container).getAllByDisplayValue('Select subcategory\u2026')[0]).not.toBeDisabled(); }); fireEvent.change(within(container).getAllByDisplayValue('Select subcategory\u2026')[0], { target: { value: 'sub1' } }); fireEvent.click(within(container).getByText('Save')); await waitFor(() => { expect(within(container).getByText('Quantity must be greater than 0.')).toBeInTheDocument(); }); }); it('shows validation error for no unit', async () => { setupFetchMock(); const user = userEvent.setup(); const { container } = render(); await waitFor(() => { expect(within(container).getByText('Dairy')).toBeInTheDocument(); }); await user.type(within(container).getByPlaceholderText('e.g. Organic Butter'), 'Test'); fireEvent.change(within(container).getByDisplayValue('Select category\u2026'), { target: { value: 'cat1' } }); await waitFor(() => { expect(within(container).getAllByDisplayValue('Select subcategory\u2026')[0]).not.toBeDisabled(); }); fireEvent.change(within(container).getAllByDisplayValue('Select subcategory\u2026')[0], { target: { value: 'sub1' } }); await user.type(getInputByLabel(container, 'Quantity'), '5'); fireEvent.click(within(container).getByText('Save')); await waitFor(() => { expect(within(container).getByText('Unit is required.')).toBeInTheDocument(); }); }); it('shows validation error for gross price <= 0', async () => { setupFetchMock(); const user = userEvent.setup(); const { container } = render(); await waitFor(() => { expect(within(container).getByText('Dairy')).toBeInTheDocument(); }); await user.type(within(container).getByPlaceholderText('e.g. Organic Butter'), 'Test'); fireEvent.change(within(container).getByDisplayValue('Select category\u2026'), { target: { value: 'cat1' } }); await waitFor(() => { expect(within(container).getAllByDisplayValue('Select subcategory\u2026')[0]).not.toBeDisabled(); }); fireEvent.change(within(container).getAllByDisplayValue('Select subcategory\u2026')[0], { target: { value: 'sub1' } }); await user.type(getInputByLabel(container, 'Quantity'), '5'); await user.type(within(container).getByPlaceholderText('kg, L, pcs\u2026'), 'kg'); fireEvent.click(within(container).getByText('Save')); await waitFor(() => { expect(within(container).getByText('Gross price must be greater than 0.')).toBeInTheDocument(); }); }); it('shows validation error for no supplier', async () => { setupFetchMock(); const user = userEvent.setup(); const { container } = render(); await waitFor(() => { expect(within(container).getByText('Dairy')).toBeInTheDocument(); }); await user.type(within(container).getByPlaceholderText('e.g. Organic Butter'), 'Test'); fireEvent.change(within(container).getByDisplayValue('Select category\u2026'), { target: { value: 'cat1' } }); await waitFor(() => { expect(within(container).getAllByDisplayValue('Select subcategory\u2026')[0]).not.toBeDisabled(); }); fireEvent.change(within(container).getAllByDisplayValue('Select subcategory\u2026')[0], { target: { value: 'sub1' } }); await user.type(getInputByLabel(container, 'Quantity'), '5'); await user.type(within(container).getByPlaceholderText('kg, L, pcs\u2026'), 'kg'); await user.type(getInputByLabel(container, 'Gross Price (\u20AC)'), '10'); fireEvent.click(within(container).getByText('Save')); await waitFor(() => { expect(within(container).getByText('Please select a supplier.')).toBeInTheDocument(); }); }); it('calls onSaved and onClose on successful save', async () => { setupFetchMock(); const user = userEvent.setup(); const onSaved = vi.fn(); const onClose = vi.fn(); const { container } = render(); await waitFor(() => { expect(within(container).getByText('Dairy')).toBeInTheDocument(); }); await fillRequiredFields(container, user); fireEvent.click(within(container).getByText('Save')); await waitFor(() => { expect(onSaved).toHaveBeenCalledOnce(); expect(onClose).toHaveBeenCalledOnce(); }); }); it('shows API error on failed save', async () => { setupFetchMock({ saveOk: false, saveResponse: { error: 'Server error occurred' }, }); const user = userEvent.setup(); const { container } = render(); await waitFor(() => { expect(within(container).getByText('Dairy')).toBeInTheDocument(); }); await fillRequiredFields(container, user); fireEvent.click(within(container).getByText('Save')); await waitFor(() => { expect(within(container).getByText('Server error occurred')).toBeInTheDocument(); }); }); it('fetches and populates form in edit mode', async () => { setupFetchMock({ ingredient: { _id: 'ing1', name: 'Butter', categoryId: 'cat1', subcategoryId: 'sub1', quantity: 10, unit: 'kg', unitPrice: 5, vat: 9, supplierId: 'sup1', }, }); const { container } = render(); await waitFor(() => { const nameInput = within(container).getByPlaceholderText('e.g. Organic Butter') as HTMLInputElement; expect(nameInput.value).toBe('Butter'); }); }); });