e25bdca4fa
Bred handskriven batch sa alla kategorier ar valfyllda (var extremt middagstung forut): - Frukost: omelett, amerikanska pannkakor, yoghurt m granola, avokadomacka, blabarssmoothie. - Lunch: broccolisoppa, grekisk sallad, kycklingwrap, tomat-linssoppa, varm skinkmacka. - Mellanmal: hummus, gronsaksstavar m dipp, notmix, fruktspett. - Efterratt: chokladdoppade jordgubbar, hallonsorbet, chokladpudding, appelkompott, bar m vaniljyoghurt. - Bak (sub-typer): vitt formbrod + scones (bread), havreflarn (cookie), blabarsmuffins + chokladtarta (cake), rabarberpaj (pie). - Middag: ugnsbakad lax, kremig kycklinggryta, pastagratang, fiskgryta m kokos, korv-potatisgratang. Alla med verifierade ingredienser + berakningsbara enheter + ratt kategori. Ravaror med genomstekning/genomkokning har explicit matsakerhetssteg. Invariant-testet kor riktig computeRecipeNutrition pa alla 59 (grona). Typecheck gront.
85 lines
3.2 KiB
TypeScript
85 lines
3.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { computeRecipeNutrition, type IngredientNutritionSource } from "@app/nutrition-engine";
|
||
import { SEED_INGREDIENTS } from "../src/seed/data/ingredients.js";
|
||
import { EXTRA_RECIPES } from "../src/seed/data/extra-recipes.js";
|
||
import { VERIFIED_RECIPES } from "../src/seed/data/verified-recipes.js";
|
||
|
||
/**
|
||
* Rent invariant-test (ingen DB) för de nya extrarecepten. Speglar exakt de
|
||
* kontroller som seed-körningen gör (run.ts rad 675 + 691) så att ett trasigt
|
||
* recept fångas här i stället för mitt i en seed-körning:
|
||
* - varje ingrediens-id måste finnas i katalogen,
|
||
* - näringen måste gå att beräkna för varje ingrediens (rätt enhet),
|
||
* - inga slug-krockar med den befintliga katalogen.
|
||
*/
|
||
|
||
const byId = new Map(SEED_INGREDIENTS.map((i) => [i.id, i]));
|
||
|
||
function sourcesFor(
|
||
recipe: (typeof EXTRA_RECIPES)[number],
|
||
): Map<string, IngredientNutritionSource> {
|
||
const sources = new Map<string, IngredientNutritionSource>();
|
||
for (const ri of recipe.ingredients) {
|
||
const info = byId.get(ri.ing);
|
||
if (!info) continue; // fångas separat av id-testet nedan
|
||
sources.set(ri.ing, {
|
||
nutritionPer100: info.nutritionPer100,
|
||
densityGPerMl: info.densityGPerMl ?? null,
|
||
gramsPerPiece: info.gramsPerPiece ?? null,
|
||
});
|
||
}
|
||
return sources;
|
||
}
|
||
|
||
describe("extrarecept – seed-invarianter", () => {
|
||
it("har minst 59 recept", () => {
|
||
expect(EXTRA_RECIPES.length).toBeGreaterThanOrEqual(59);
|
||
});
|
||
|
||
it("alla ingrediens-id:n finns i katalogen (run.ts rad 675)", () => {
|
||
const unknown: string[] = [];
|
||
for (const recipe of EXTRA_RECIPES) {
|
||
for (const ri of recipe.ingredients) {
|
||
if (!byId.has(ri.ing)) unknown.push(`${recipe.slug}: ${ri.ing}`);
|
||
}
|
||
}
|
||
expect(unknown).toEqual([]);
|
||
});
|
||
|
||
it("näringen går att beräkna för varje recept (run.ts rad 691)", () => {
|
||
const failed: string[] = [];
|
||
for (const recipe of EXTRA_RECIPES) {
|
||
const calc = recipe.ingredients.map((ri) => ({
|
||
canonicalIngredientId: ri.ing,
|
||
quantity: ri.qty,
|
||
unit: ri.unit,
|
||
optional: ri.optional ?? false,
|
||
}));
|
||
const nutrition = computeRecipeNutrition(calc, recipe.portions, sourcesFor(recipe));
|
||
if (nutrition.uncomputableIngredientIds.length > 0) {
|
||
failed.push(`${recipe.slug}: ${nutrition.uncomputableIngredientIds.join(", ")}`);
|
||
}
|
||
// Rimlig näring per portion (fångar grova enhetsfel).
|
||
expect(nutrition.perPortion.kcal).toBeGreaterThan(0);
|
||
}
|
||
expect(failed).toEqual([]);
|
||
});
|
||
|
||
it("inga slug-krockar med befintliga recept och inga interna dubbletter", () => {
|
||
const existing = new Set(VERIFIED_RECIPES.map((r) => r.slug));
|
||
const seen = new Set<string>();
|
||
const collisions: string[] = [];
|
||
for (const recipe of EXTRA_RECIPES) {
|
||
if (existing.has(recipe.slug)) collisions.push(`krock: ${recipe.slug}`);
|
||
if (seen.has(recipe.slug)) collisions.push(`dubblett: ${recipe.slug}`);
|
||
seen.add(recipe.slug);
|
||
}
|
||
expect(collisions).toEqual([]);
|
||
});
|
||
|
||
it("varje bak-recept är taggat 'baking' (driver Baka-kategorin)", () => {
|
||
const baking = EXTRA_RECIPES.filter((r) => r.tags.includes("baking"));
|
||
expect(baking.length).toBeGreaterThanOrEqual(4);
|
||
});
|
||
});
|