diff --git a/frontend/src/lib/recipes/RecipeForm.svelte b/frontend/src/lib/recipes/RecipeForm.svelte new file mode 100644 index 0000000..f62df04 --- /dev/null +++ b/frontend/src/lib/recipes/RecipeForm.svelte @@ -0,0 +1,134 @@ + + +
diff --git a/frontend/src/lib/recipes/RecipeForm.test.ts b/frontend/src/lib/recipes/RecipeForm.test.ts new file mode 100644 index 0000000..3ecf980 --- /dev/null +++ b/frontend/src/lib/recipes/RecipeForm.test.ts @@ -0,0 +1,143 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/svelte'; +import { userEvent } from '@testing-library/user-event'; +import RecipeForm from './RecipeForm.svelte'; + +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'); + }); +});