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 }); }); });