Files
ben-to/scripts/recipes/classification.unit.test.ts
2026-07-21 16:50:58 +02:00

231 lines
7.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
buildBaseIngredientsSteps,
buildLocalizedParagraph,
buildLocalizedText,
buildRecipeGraph,
buildVariantPayload,
classifyFlavor
} from "./classification.mjs";
describe("classifyFlavor", () => {
it("retombe sur sweet si mots-clés sucrés dominent", () => {
expect(classifyFlavor("gateau-chocolat", [])).toBe("sweet");
expect(classifyFlavor("muffin-banane", [])).toBe("sweet");
});
it("retombe sur savory si mots-clés salés dominent", () => {
expect(classifyFlavor("onigiri-thon", [])).toBe("savory");
expect(classifyFlavor("naan-fromage", [])).toBe("savory");
});
it("considère aussi les tags (pas seulement le slug)", () => {
expect(classifyFlavor("recette-x", ["bento", "riz", "onigiri"])).toBe("savory");
expect(classifyFlavor("recette-x", ["dessert", "chocolat"])).toBe("sweet");
});
it("fallback savory en cas d'égalité (comportement historique)", () => {
// « cake » sucré + « bento » salé = 1-1
expect(classifyFlavor("cake-bento", [])).toBe("savory");
});
});
describe("buildLocalizedText & buildLocalizedParagraph", () => {
it("cascade fr → en → ko → zh avec fallback en cas de locale manquante", () => {
expect(
buildLocalizedText(
{ fr: { title: "Gâteau" }, en: { title: "Cake" } },
"default"
)
).toEqual({ fr: "Gâteau", en: "Cake", ko: "Cake", zh: "Cake" });
expect(
buildLocalizedText(
{ fr: { title: "Gâteau" } },
"default"
)
).toEqual({ fr: "Gâteau", en: "Gâteau", ko: "Gâteau", zh: "Gâteau" });
expect(buildLocalizedText({}, "default")).toEqual({
fr: "default",
en: "default",
ko: "default",
zh: "default"
});
});
it("buildLocalizedParagraph utilise la clé `description`", () => {
expect(
buildLocalizedParagraph(
{ fr: { description: "Une histoire" }, en: { description: "A story" } },
"fallback"
)
).toEqual({ fr: "Une histoire", en: "A story", ko: "A story", zh: "A story" });
});
});
describe("buildBaseIngredientsSteps & buildVariantPayload", () => {
const sheetMeta = { width: 256, height: 256 };
it("produit ingrédients + steps localisés pour une base", () => {
const recipe = {
slug: "onigiri",
mergedIngredients: [
{
id: "onigiri-ingredient-0",
iconPx: { x: 0, y: 0 },
names: { fr: "Riz", en: "Rice" },
notes: { fr: "note fr", en: "" },
quantity: 100,
unit: "g"
}
],
mergedSteps: [
{ id: "step-1", priority: 10, phase: "cook", dependsOn: [], text: { fr: "Cuire", en: "Cook" } }
]
};
const { ingredients, steps } = buildBaseIngredientsSteps(recipe, sheetMeta);
expect(ingredients).toHaveLength(1);
expect(ingredients[0].id).toBe("onigiri-ingredient-0");
expect(ingredients[0].name.fr).toBe("Riz");
expect(ingredients[0].coverSprite).toBeTruthy();
expect(steps[0]).toMatchObject({
id: "step-1",
priority: 10,
phase: "cook",
dependsOn: [],
text: { fr: "Cuire", en: "Cook" }
});
});
it("préfixe les ids et bump la priority pour les variantes", () => {
const recipe = {
slug: "onigiri",
servings: 3,
locales: { fr: { title: "Onigiri thon" } },
mergedIngredients: [
{
id: "onigiri-ingredient-0",
iconPx: null,
names: { fr: "Riz", en: "Rice" },
notes: { fr: "", en: "" },
quantity: 1,
unit: "g"
}
],
mergedSteps: [
{ id: "step-1", priority: 10, phase: "cook", dependsOn: [], text: { fr: "Cuire", en: "Cook" } },
{ id: "step-2", priority: 20, phase: "cook", dependsOn: ["step-1"], text: { fr: "Servir", en: "Serve" } }
]
};
const { title, story, ingredients, steps, servings } = buildVariantPayload(recipe, sheetMeta);
expect(servings).toBe(3);
expect(title).toEqual({ fr: "Onigiri thon", en: "Onigiri thon", ko: "Onigiri thon", zh: "Onigiri thon" });
expect(story.fr).toBe("");
expect(ingredients[0].id).toBe("variant:onigiri:onigiri-ingredient-0");
expect(steps[0].id).toBe("variant:onigiri:step-1");
expect(steps[0].priority).toBe(5010);
expect(steps[1].dependsOn).toEqual(["variant:onigiri:step-1"]);
});
});
describe("buildRecipeGraph", () => {
const sheetMeta = { width: 256, height: 256 };
const spriteByAlias = {
onigiri: { x: 0, y: 0 },
madelaine: { x: 32, y: 0 },
carnet: { x: 64, y: 0 }
};
const preferredVariantAliases = new Set(["onigiri"]);
it("construit 13 bases dans l'ordre d'affichage", () => {
const { bases } = buildRecipeGraph([], sheetMeta, spriteByAlias, preferredVariantAliases);
expect(bases).toHaveLength(13);
expect(bases.map((b) => b.id)).toEqual([
"base:onigiri",
"base:gimbap",
"base:mandu",
"base:empanadas-savory",
"base:empanadas-sweet",
"base:galettes",
"base:bao",
"base:muffin",
"base:mini-cakes",
"base:biscuits-secs",
"base:fluffy-cakes",
"base:cakes",
"base:carnet"
]);
// base sans recette = pas d'ingrédients, pas d'erreur
expect(bases[0].ingredients).toEqual([]);
});
it("attache la base, les ingrédients et les étapes à la base du slug représentatif", () => {
const recipes = [
{
slug: "onigiri-thon",
servings: 2,
flavor: "savory",
tags: ["bento"],
variantTileSpriteAlias: null,
coverImageUrl: null,
locales: { fr: { title: "Onigiri thon" } },
mergedIngredients: [
{
id: "onigiri-thon-ingredient-0",
iconPx: { x: 0, y: 0 },
names: { fr: "Riz", en: "Rice" },
notes: { fr: "", en: "" },
quantity: 1,
unit: "g"
}
],
mergedSteps: [
{ id: "step-1", priority: 10, phase: "cook", dependsOn: [], text: { fr: "Cuire", en: "Cook" } }
]
}
];
const { bases, variants } = buildRecipeGraph(recipes, sheetMeta, spriteByAlias, preferredVariantAliases);
const onigiriBase = bases.find((b) => b.id === "base:onigiri");
expect(onigiriBase.servings).toBe(2);
expect(onigiriBase.ingredients).toHaveLength(1);
expect(onigiriBase.steps).toHaveLength(1);
expect(onigiriBase.tags).toEqual(["bento"]);
expect(variants).toHaveLength(1);
expect(variants[0].baseIds).toEqual(["base:onigiri"]);
expect(variants[0].servings).toBe(2);
expect(variants[0].slug).toBe("onigiri-thon");
expect(variants[0].coverSprite).toBeTruthy();
});
it("utilise un alias sprite: explicite pour la coverSprite variante", () => {
const recipes = [
{
slug: "onigiri-saumon",
flavor: "savory",
tags: ["bento"],
variantTileSpriteAlias: "madelaine",
coverImageUrl: null,
locales: { fr: { title: "Onigiri saumon" } },
mergedIngredients: [
{
id: "onigiri-saumon-ingredient-0",
iconPx: { x: 0, y: 0 },
names: { fr: "Riz", en: "Rice" },
notes: { fr: "", en: "" },
quantity: 1,
unit: "g"
}
],
mergedSteps: [
{ id: "step-1", priority: 10, phase: "cook", dependsOn: [], text: { fr: "Cuire", en: "Cook" } }
]
}
];
const { variants } = buildRecipeGraph(recipes, sheetMeta, spriteByAlias, preferredVariantAliases);
// madelaine → (32, 0) → col=1, row=0
expect(variants[0].coverSprite).toMatchObject({ col: 1, row: 0 });
});
});