2026-04-21 22:50:58 +02:00
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
|
import { openInMemoryForTest } from '../../src/lib/server/db';
|
|
|
|
|
import { insertRecipe } from '../../src/lib/server/recipes/repository';
|
|
|
|
|
import {
|
|
|
|
|
addRecipeToCart,
|
2026-04-21 22:56:26 +02:00
|
|
|
removeRecipeFromCart,
|
2026-04-21 22:59:12 +02:00
|
|
|
listShoppingList,
|
|
|
|
|
setCartServings
|
2026-04-21 22:50:58 +02:00
|
|
|
} from '../../src/lib/server/shopping/repository';
|
|
|
|
|
import type { Recipe } from '../../src/lib/types';
|
|
|
|
|
|
|
|
|
|
function recipe(overrides: Partial<Recipe> = {}): Recipe {
|
|
|
|
|
return {
|
|
|
|
|
id: null,
|
|
|
|
|
title: 'Test',
|
|
|
|
|
description: null,
|
|
|
|
|
source_url: null,
|
|
|
|
|
source_domain: null,
|
|
|
|
|
image_path: null,
|
|
|
|
|
servings_default: 4,
|
|
|
|
|
servings_unit: null,
|
|
|
|
|
prep_time_min: null,
|
|
|
|
|
cook_time_min: null,
|
|
|
|
|
total_time_min: null,
|
|
|
|
|
cuisine: null,
|
|
|
|
|
category: null,
|
|
|
|
|
ingredients: [],
|
|
|
|
|
steps: [],
|
|
|
|
|
tags: [],
|
|
|
|
|
...overrides
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('addRecipeToCart', () => {
|
|
|
|
|
it('inserts recipe with default servings from recipe.servings_default', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const id = insertRecipe(db, recipe({ title: 'Pasta', servings_default: 4 }));
|
|
|
|
|
addRecipeToCart(db, id, null);
|
|
|
|
|
const snap = listShoppingList(db);
|
|
|
|
|
expect(snap.recipes).toHaveLength(1);
|
|
|
|
|
expect(snap.recipes[0].servings).toBe(4);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('respects explicit servings override', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const id = insertRecipe(db, recipe({ servings_default: 4 }));
|
|
|
|
|
addRecipeToCart(db, id, null, 2);
|
|
|
|
|
expect(listShoppingList(db).recipes[0].servings).toBe(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('is idempotent: second insert updates servings, not fails', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const id = insertRecipe(db, recipe({ servings_default: 4 }));
|
|
|
|
|
addRecipeToCart(db, id, null, 2);
|
|
|
|
|
addRecipeToCart(db, id, null, 6);
|
|
|
|
|
const snap = listShoppingList(db);
|
|
|
|
|
expect(snap.recipes).toHaveLength(1);
|
|
|
|
|
expect(snap.recipes[0].servings).toBe(6);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('falls back to servings=4 when recipe has no default', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const id = insertRecipe(db, recipe({ servings_default: null }));
|
|
|
|
|
addRecipeToCart(db, id, null);
|
|
|
|
|
expect(listShoppingList(db).recipes[0].servings).toBe(4);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-04-21 22:56:26 +02:00
|
|
|
|
|
|
|
|
describe('removeRecipeFromCart', () => {
|
|
|
|
|
it('deletes only the given recipe', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const a = insertRecipe(db, recipe({ title: 'A' }));
|
|
|
|
|
const b = insertRecipe(db, recipe({ title: 'B' }));
|
|
|
|
|
addRecipeToCart(db, a, null);
|
|
|
|
|
addRecipeToCart(db, b, null);
|
|
|
|
|
removeRecipeFromCart(db, a);
|
|
|
|
|
const snap = listShoppingList(db);
|
|
|
|
|
expect(snap.recipes).toHaveLength(1);
|
|
|
|
|
expect(snap.recipes[0].recipe_id).toBe(b);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('is idempotent when recipe is not in cart', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const id = insertRecipe(db, recipe());
|
|
|
|
|
expect(() => removeRecipeFromCart(db, id)).not.toThrow();
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-04-21 22:59:12 +02:00
|
|
|
|
|
|
|
|
describe('setCartServings', () => {
|
|
|
|
|
it('updates servings for a cart recipe', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const id = insertRecipe(db, recipe());
|
|
|
|
|
addRecipeToCart(db, id, null, 4);
|
|
|
|
|
setCartServings(db, id, 8);
|
|
|
|
|
expect(listShoppingList(db).recipes[0].servings).toBe(8);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('rejects non-positive servings', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const id = insertRecipe(db, recipe());
|
|
|
|
|
addRecipeToCart(db, id, null, 4);
|
|
|
|
|
expect(() => setCartServings(db, id, 0)).toThrow();
|
|
|
|
|
expect(() => setCartServings(db, id, -3)).toThrow();
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-04-21 23:02:05 +02:00
|
|
|
|
|
|
|
|
describe('listShoppingList aggregation', () => {
|
|
|
|
|
it('aggregates same name+unit across recipes', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const a = insertRecipe(db, recipe({
|
|
|
|
|
title: 'Carbonara', servings_default: 4,
|
|
|
|
|
ingredients: [{ position: 1, quantity: 200, unit: 'g', name: 'Mehl', note: null, raw_text: '', section_heading: null }]
|
|
|
|
|
}));
|
|
|
|
|
const b = insertRecipe(db, recipe({
|
|
|
|
|
title: 'Lasagne', servings_default: 4,
|
|
|
|
|
ingredients: [{ position: 1, quantity: 200, unit: 'g', name: 'Mehl', note: null, raw_text: '', section_heading: null }]
|
|
|
|
|
}));
|
|
|
|
|
addRecipeToCart(db, a, null, 4);
|
|
|
|
|
addRecipeToCart(db, b, null, 4);
|
|
|
|
|
const rows = listShoppingList(db).rows;
|
|
|
|
|
expect(rows).toHaveLength(1);
|
|
|
|
|
expect(rows[0].name_key).toBe('mehl');
|
|
|
|
|
expect(rows[0].unit_key).toBe('g');
|
|
|
|
|
expect(rows[0].total_quantity).toBe(400);
|
|
|
|
|
expect(rows[0].from_recipes).toContain('Carbonara');
|
|
|
|
|
expect(rows[0].from_recipes).toContain('Lasagne');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('keeps different units as separate rows', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const id = insertRecipe(db, recipe({
|
|
|
|
|
servings_default: 4,
|
|
|
|
|
ingredients: [
|
|
|
|
|
{ position: 1, quantity: 100, unit: 'g', name: 'Mehl', note: null, raw_text: '', section_heading: null },
|
|
|
|
|
{ position: 2, quantity: 1, unit: 'Pck', name: 'Mehl', note: null, raw_text: '', section_heading: null }
|
|
|
|
|
]
|
|
|
|
|
}));
|
|
|
|
|
addRecipeToCart(db, id, null, 4);
|
|
|
|
|
const rows = listShoppingList(db).rows;
|
|
|
|
|
expect(rows).toHaveLength(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('scales quantities by servings/servings_default', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const id = insertRecipe(db, recipe({
|
|
|
|
|
servings_default: 4,
|
|
|
|
|
ingredients: [{ position: 1, quantity: 200, unit: 'g', name: 'Mehl', note: null, raw_text: '', section_heading: null }]
|
|
|
|
|
}));
|
|
|
|
|
addRecipeToCart(db, id, null, 2);
|
|
|
|
|
expect(listShoppingList(db).rows[0].total_quantity).toBe(100);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('null quantity stays null after aggregation', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const id = insertRecipe(db, recipe({
|
|
|
|
|
ingredients: [{ position: 1, quantity: null, unit: null, name: 'Salz', note: null, raw_text: '', section_heading: null }]
|
|
|
|
|
}));
|
|
|
|
|
addRecipeToCart(db, id, null);
|
|
|
|
|
const rows = listShoppingList(db).rows;
|
|
|
|
|
expect(rows[0].total_quantity).toBeNull();
|
|
|
|
|
expect(rows[0].unit_key).toBe('');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('counts unchecked rows in uncheckedCount', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const id = insertRecipe(db, recipe({
|
|
|
|
|
ingredients: [
|
|
|
|
|
{ position: 1, quantity: 1, unit: 'Stk', name: 'Apfel', note: null, raw_text: '', section_heading: null },
|
|
|
|
|
{ position: 2, quantity: 1, unit: 'Stk', name: 'Birne', note: null, raw_text: '', section_heading: null }
|
|
|
|
|
]
|
|
|
|
|
}));
|
|
|
|
|
addRecipeToCart(db, id, null);
|
|
|
|
|
expect(listShoppingList(db).uncheckedCount).toBe(2);
|
|
|
|
|
});
|
2026-04-21 23:06:19 +02:00
|
|
|
|
|
|
|
|
it('does not blow up when servings_default is zero (silent NULL total_quantity)', () => {
|
|
|
|
|
const db = openInMemoryForTest();
|
|
|
|
|
const id = insertRecipe(db, recipe({
|
|
|
|
|
servings_default: 0,
|
|
|
|
|
ingredients: [{ position: 1, quantity: 100, unit: 'g', name: 'Mehl', note: null, raw_text: '', section_heading: null }]
|
|
|
|
|
}));
|
|
|
|
|
addRecipeToCart(db, id, null, 4);
|
|
|
|
|
const rows = listShoppingList(db).rows;
|
|
|
|
|
expect(rows).toHaveLength(1);
|
|
|
|
|
expect(rows[0].total_quantity).toBeNull();
|
|
|
|
|
});
|
2026-04-21 23:02:05 +02:00
|
|
|
});
|