feat(recipes): filter ingredients with quantity <= 0 before API submission
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,30 +3,44 @@ import { render, screen } from '@testing-library/svelte';
|
|||||||
import VarietyWarningCards from './VarietyWarningCards.svelte';
|
import VarietyWarningCards from './VarietyWarningCards.svelte';
|
||||||
|
|
||||||
const warnings = [
|
const warnings = [
|
||||||
{ title: 'Chicken zweimal diese Woche', explanation: 'Mo, Mi — erwäge einen Tausch.' },
|
{
|
||||||
{ title: 'Tomaten in 3 Gerichten', explanation: 'Mo, Di, Mi — sorge für Abwechslung.' }
|
title: 'Chicken zweimal diese Woche',
|
||||||
|
items: [
|
||||||
|
{ dayShort: 'Mo', recipeName: 'Chicken Tikka', slotId: 1 },
|
||||||
|
{ dayShort: 'Mi', recipeName: 'Chicken Curry', slotId: 3 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tomaten in 3 Gerichten',
|
||||||
|
items: [
|
||||||
|
{ dayShort: 'Mo', recipeName: 'Pasta Pomodoro', slotId: 1 },
|
||||||
|
{ dayShort: 'Di', recipeName: 'Tomatensuppe', slotId: 2 },
|
||||||
|
{ dayShort: 'Mi', recipeName: 'Pizza Margherita', slotId: 3 }
|
||||||
|
]
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('VarietyWarningCards', () => {
|
describe('VarietyWarningCards', () => {
|
||||||
it('renders one card per warning', () => {
|
it('renders one card per warning', () => {
|
||||||
render(VarietyWarningCards, { props: { warnings } });
|
render(VarietyWarningCards, { props: { warnings, weekStart: '2026-04-07' } });
|
||||||
const cards = screen.getAllByTestId('warning-card');
|
const cards = screen.getAllByTestId('warning-card');
|
||||||
expect(cards.length).toBe(2);
|
expect(cards.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders warning titles', () => {
|
it('renders warning titles', () => {
|
||||||
render(VarietyWarningCards, { props: { warnings } });
|
render(VarietyWarningCards, { props: { warnings, weekStart: '2026-04-07' } });
|
||||||
expect(screen.getByText(/Chicken zweimal/)).toBeTruthy();
|
expect(screen.getByText(/Chicken zweimal/)).toBeTruthy();
|
||||||
expect(screen.getByText(/Tomaten in 3/)).toBeTruthy();
|
expect(screen.getByText(/Tomaten in 3/)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders warning explanations', () => {
|
it('renders warning explanations', () => {
|
||||||
render(VarietyWarningCards, { props: { warnings } });
|
render(VarietyWarningCards, { props: { warnings, weekStart: '2026-04-07' } });
|
||||||
expect(screen.getByText(/erwäge einen Tausch/)).toBeTruthy();
|
expect(screen.getByText('Chicken Tikka')).toBeTruthy();
|
||||||
|
expect(screen.getByText('Chicken Curry')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders nothing when warnings is empty', () => {
|
it('renders nothing when warnings is empty', () => {
|
||||||
render(VarietyWarningCards, { props: { warnings: [] } });
|
render(VarietyWarningCards, { props: { warnings: [], weekStart: '2026-04-07' } });
|
||||||
expect(screen.queryAllByTestId('warning-card').length).toBe(0);
|
expect(screen.queryAllByTestId('warning-card').length).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const editProps = {
|
|||||||
name: 'Spaghetti Bolognese',
|
name: 'Spaghetti Bolognese',
|
||||||
serves: 4,
|
serves: 4,
|
||||||
cookTimeMin: 30,
|
cookTimeMin: 30,
|
||||||
effort: 'Medium',
|
effort: 'medium',
|
||||||
heroImageUrl: undefined as string | undefined,
|
heroImageUrl: undefined as string | undefined,
|
||||||
ingredients: [
|
ingredients: [
|
||||||
{ name: 'Spaghetti', quantity: 200, unit: 'g' }
|
{ name: 'Spaghetti', quantity: 200, unit: 'g' }
|
||||||
|
|||||||
@@ -77,10 +77,10 @@ export const actions: Actions = {
|
|||||||
effort,
|
effort,
|
||||||
heroImageUrl,
|
heroImageUrl,
|
||||||
ingredients: (parsedIngredients as { name: string; quantity: string; unit: string }[])
|
ingredients: (parsedIngredients as { name: string; quantity: string; unit: string }[])
|
||||||
.filter((ing) => ing.name?.trim())
|
.filter((ing) => ing.name?.trim() && Number(ing.quantity) > 0)
|
||||||
.map((ing, i) => ({
|
.map((ing, i) => ({
|
||||||
newIngredientName: ing.name.trim(),
|
newIngredientName: ing.name.trim(),
|
||||||
quantity: Number(ing.quantity) || 0,
|
quantity: Number(ing.quantity),
|
||||||
unit: ing.unit || '',
|
unit: ing.unit || '',
|
||||||
sortOrder: i
|
sortOrder: i
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -204,6 +204,25 @@ describe('edit recipe page — update action', () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('filters out ingredients with quantity <= 0 before PUT', async () => {
|
||||||
|
mockPut.mockResolvedValue({ error: undefined });
|
||||||
|
const fd = makeFormData({
|
||||||
|
ingredientsJson: JSON.stringify([
|
||||||
|
{ name: 'Spaghetti', quantity: 200, unit: 'g' },
|
||||||
|
{ name: 'Salt', quantity: 0, unit: 'tsp' },
|
||||||
|
{ name: 'Pepper', quantity: -1, unit: 'tsp' }
|
||||||
|
])
|
||||||
|
});
|
||||||
|
await actions.update({
|
||||||
|
request: { formData: async () => fd },
|
||||||
|
fetch: vi.fn(),
|
||||||
|
params: { id: 'r1' }
|
||||||
|
} as any).catch(() => {});
|
||||||
|
const body = mockPut.mock.calls[0][1].body;
|
||||||
|
expect(body.ingredients).toHaveLength(1);
|
||||||
|
expect(body.ingredients[0].newIngredientName).toBe('Spaghetti');
|
||||||
|
});
|
||||||
|
|
||||||
it('returns fail(500) when API returns error', async () => {
|
it('returns fail(500) when API returns error', async () => {
|
||||||
mockPut.mockResolvedValue({ error: { status: 500 } });
|
mockPut.mockResolvedValue({ error: { status: 500 } });
|
||||||
const result = await actions.update({
|
const result = await actions.update({
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ export const actions: Actions = {
|
|||||||
effort,
|
effort,
|
||||||
heroImageUrl,
|
heroImageUrl,
|
||||||
ingredients: (parsedIngredients as { name: string; quantity: string; unit: string }[])
|
ingredients: (parsedIngredients as { name: string; quantity: string; unit: string }[])
|
||||||
.filter((ing) => ing.name?.trim())
|
.filter((ing) => ing.name?.trim() && Number(ing.quantity) > 0)
|
||||||
.map((ing, i) => ({
|
.map((ing, i) => ({
|
||||||
newIngredientName: ing.name.trim(),
|
newIngredientName: ing.name.trim(),
|
||||||
quantity: Number(ing.quantity) || 0,
|
quantity: Number(ing.quantity),
|
||||||
unit: ing.unit || '',
|
unit: ing.unit || '',
|
||||||
sortOrder: i
|
sortOrder: i
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -163,6 +163,23 @@ describe('new recipe page — create action', () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('filters out ingredients with quantity <= 0 before POST', async () => {
|
||||||
|
mockPost.mockResolvedValue({ error: undefined });
|
||||||
|
const fd = makeFormData({
|
||||||
|
ingredientsJson: JSON.stringify([
|
||||||
|
{ name: 'Spaghetti', quantity: 200, unit: 'g' },
|
||||||
|
{ name: 'Salt', quantity: 0, unit: 'tsp' },
|
||||||
|
{ name: 'Pepper', quantity: -1, unit: 'tsp' }
|
||||||
|
])
|
||||||
|
});
|
||||||
|
await actions.create({ request: { formData: async () => fd }, fetch: vi.fn() } as any).catch(
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
const body = mockPost.mock.calls[0][1].body;
|
||||||
|
expect(body.ingredients).toHaveLength(1);
|
||||||
|
expect(body.ingredients[0].newIngredientName).toBe('Spaghetti');
|
||||||
|
});
|
||||||
|
|
||||||
it('returns fail(500) when API returns error', async () => {
|
it('returns fail(500) when API returns error', async () => {
|
||||||
mockPost.mockResolvedValue({ error: { status: 500 } });
|
mockPost.mockResolvedValue({ error: { status: 500 } });
|
||||||
const result = await actions.create({
|
const result = await actions.create({
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import Page from './+page.svelte';
|
|||||||
|
|
||||||
const mockData = {
|
const mockData = {
|
||||||
recipes: [
|
recipes: [
|
||||||
{ id: 'r1', name: 'Spaghetti Bolognese', cookTimeMin: 30, effort: 'Easy' },
|
{ id: 'r1', name: 'Spaghetti Bolognese', cookTimeMin: 30, effort: 'easy' },
|
||||||
{ id: 'r2', name: 'Chicken Curry', cookTimeMin: 45, effort: 'Medium' },
|
{ id: 'r2', name: 'Chicken Curry', cookTimeMin: 45, effort: 'medium' },
|
||||||
{ id: 'r3', name: 'Gemüsesuppe', cookTimeMin: 20, effort: 'Easy' }
|
{ id: 'r3', name: 'Gemüsesuppe', cookTimeMin: 20, effort: 'easy' }
|
||||||
],
|
],
|
||||||
activePlan: null
|
activePlan: null
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user