import { describe, it, expect, vi } from 'vitest'; import { render, screen } from '@testing-library/svelte'; import { userEvent } from '@testing-library/user-event'; import { writable } from 'svelte/store'; import RecipeForm from './RecipeForm.svelte'; vi.mock('$app/stores', () => ({ page: writable({ form: null, url: new URL('http://localhost/recipes/new') }) })); vi.mock('$app/forms', () => ({ enhance: () => ({ destroy: () => {} }) })); const mockCategories = [ { id: 'c1', name: 'Pasta', tagType: 'category' }, { id: 'c2', name: 'Fleisch', tagType: 'category' } ]; const emptyProps = { recipe: null, categories: mockCategories, action: '?/create' }; const editProps = { recipe: { id: 'r1', name: 'Spaghetti Bolognese', serves: 4, cookTimeMin: 30, effort: 'Medium', heroImageUrl: undefined as string | undefined, ingredients: [ { name: 'Spaghetti', quantity: 200, unit: 'g' } ], steps: [ { instruction: 'Wasser aufsetzen' } ], tagIds: ['c1'] }, categories: mockCategories, action: '?/update' }; describe('RecipeForm', () => { it('renders recipe name input', () => { render(RecipeForm, { props: emptyProps }); expect(screen.getByLabelText(/name/i)).toBeInTheDocument(); }); it('renders serves input', () => { render(RecipeForm, { props: emptyProps }); expect(screen.getByLabelText(/portionen/i)).toBeInTheDocument(); }); it('renders cook time input', () => { render(RecipeForm, { props: emptyProps }); expect(screen.getByLabelText(/kochzeit/i)).toBeInTheDocument(); }); it('prefills name when editing', () => { render(RecipeForm, { props: editProps }); expect(screen.getByLabelText(/name/i)).toHaveValue('Spaghetti Bolognese'); }); it('prefills serves when editing', () => { render(RecipeForm, { props: editProps }); expect(screen.getByLabelText(/portionen/i)).toHaveValue(4); }); it('renders effort chips', () => { render(RecipeForm, { props: emptyProps }); expect(screen.getByRole('radio', { name: /leicht/i })).toBeInTheDocument(); expect(screen.getByRole('radio', { name: /mittel/i })).toBeInTheDocument(); expect(screen.getByRole('radio', { name: /schwer/i })).toBeInTheDocument(); }); it('prefills effort when editing', () => { render(RecipeForm, { props: editProps }); expect(screen.getByRole('radio', { name: /mittel/i })).toBeChecked(); }); it('renders category chips', () => { render(RecipeForm, { props: emptyProps }); expect(screen.getByRole('checkbox', { name: 'Pasta' })).toBeInTheDocument(); expect(screen.getByRole('checkbox', { name: 'Fleisch' })).toBeInTheDocument(); }); it('prefills selected categories when editing', () => { render(RecipeForm, { props: editProps }); expect(screen.getByRole('checkbox', { name: 'Pasta' })).toBeChecked(); expect(screen.getByRole('checkbox', { name: 'Fleisch' })).not.toBeChecked(); }); it('renders at least one ingredient row initially for empty form', () => { render(RecipeForm, { props: emptyProps }); expect(screen.getByPlaceholderText(/zutat/i)).toBeInTheDocument(); }); it('prefills ingredient rows when editing', () => { render(RecipeForm, { props: editProps }); expect(screen.getByDisplayValue('Spaghetti')).toBeInTheDocument(); }); it('adds ingredient row when "Zutat hinzufügen" is clicked', async () => { const user = userEvent.setup(); render(RecipeForm, { props: emptyProps }); const before = screen.getAllByPlaceholderText(/zutat/i).length; await user.click(screen.getByRole('button', { name: /zutat hinzufügen/i })); expect(screen.getAllByPlaceholderText(/zutat/i)).toHaveLength(before + 1); }); it('removes ingredient row when remove button is clicked', async () => { const user = userEvent.setup(); render(RecipeForm, { props: editProps }); await user.click(screen.getByRole('button', { name: /zutat hinzufügen/i })); const before = screen.getAllByPlaceholderText(/zutat/i).length; const removeButtons = screen.getAllByRole('button', { name: /entfernen/i }); await user.click(removeButtons[0]); expect(screen.getAllByPlaceholderText(/zutat/i)).toHaveLength(before - 1); }); it('renders at least one step row initially for empty form', () => { render(RecipeForm, { props: emptyProps }); expect(screen.getByPlaceholderText(/schritt/i)).toBeInTheDocument(); }); it('prefills step rows when editing', () => { render(RecipeForm, { props: editProps }); expect(screen.getByDisplayValue('Wasser aufsetzen')).toBeInTheDocument(); }); it('adds step row when "Schritt hinzufügen" is clicked', async () => { const user = userEvent.setup(); render(RecipeForm, { props: emptyProps }); const before = screen.getAllByPlaceholderText(/schritt/i).length; await user.click(screen.getByRole('button', { name: /schritt hinzufügen/i })); expect(screen.getAllByPlaceholderText(/schritt/i)).toHaveLength(before + 1); }); it('renders save button', () => { render(RecipeForm, { props: emptyProps }); expect(screen.getByRole('button', { name: /speichern/i })).toBeInTheDocument(); }); it('renders cancel link back to /recipes', () => { render(RecipeForm, { props: emptyProps }); const cancelLink = screen.getByRole('link', { name: /abbrechen/i }); expect(cancelLink).toHaveAttribute('href', '/recipes'); }); it('displays form error message when $page.form.error is set', async () => { const { page } = await import('$app/stores'); (page as ReturnType).set({ form: { error: 'Name ist erforderlich' }, url: new URL('http://localhost/recipes/new') }); render(RecipeForm, { props: emptyProps }); expect(screen.getByRole('alert')).toHaveTextContent('Name ist erforderlich'); (page as ReturnType).set({ form: null, url: new URL('http://localhost/recipes/new') }); }); it('does not display error banner when form has no error', () => { render(RecipeForm, { props: emptyProps }); expect(screen.queryByRole('alert')).not.toBeInTheDocument(); }); });