- Import page store and render role="alert" error banner - Add mock for \$app/stores and \$app/forms in RecipeForm tests - Add tests: error banner shown when form.error set, hidden when null Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
166 lines
5.8 KiB
TypeScript
166 lines
5.8 KiB
TypeScript
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<typeof writable>).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<typeof writable>).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();
|
|
});
|
|
});
|