231 lines
7.5 KiB
TypeScript
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 });
|
|
});
|
|
});
|