import { expect } from "chai"; import { prisma } from "../src/db/prisma.js"; import type { ParsedRecipe } from "../src/lib/recipe-source-adapter.js"; import { translateRecipe, translateRecipeSteps } from "../src/lib/recipe-translation.js"; import type { TechStepMappingRule } from "../src/lib/tech-step-matcher.js"; import { resetDatabase } from "../test-support/reset-db.js"; /** A minimal fixture `ParsedRecipe` — only `steps` (built from `descriptions`) matters for most tests here, the rest is filler to prove it survives translation untouched. */ function buildParsedRecipe(descriptions: string[]): ParsedRecipe { return { name: "Test Recipe", description: "A recipe for testing", picture: "https://example.test/recipe.jpg", portions: 4, sourceUrl: "https://example.test/recipes/1", ingredients: [{ rawText: "1 egg", quantity: null, unit: null, name: "egg" }], steps: descriptions.map((description, i) => ({ description, picture: i === 0 ? "https://example.test/step1.jpg" : null, })), }; } describe("recipe-translation", () => { describe("translateRecipeSteps", () => { const simmer: TechStepMappingRule = { techStepId: 1, expression: "\\bmijot(er|ez|e|ant|é)\\b", weight: 15, }; const preheat: TechStepMappingRule = { techStepId: 2, expression: "\\bpr[ée]chauff(er|ez|é|ée)\\b", weight: 20, }; const melt: TechStepMappingRule = { techStepId: 3, expression: "\\bfaire fondre\\b|\\bfaites fondre\\b", weight: 15, }; it("declares each step's technique sequence, preserving order", () => { const recipe = buildParsedRecipe([ "Préchauffer la poêle, puis faire fondre le beurre", "Servir immédiatement", "Faire mijoter à feu doux", ]); const translated = translateRecipeSteps(recipe, [simmer, preheat, melt]); expect(translated.steps.map((step) => step.techStepIds)).to.deep.equal([[2, 3], [], [1]]); }); it("leaves description/picture untouched on each step", () => { const recipe = buildParsedRecipe(["Faire mijoter à feu doux", "Servir"]); const translated = translateRecipeSteps(recipe, [simmer]); expect(translated.steps[0]).to.deep.equal({ description: "Faire mijoter à feu doux", picture: "https://example.test/step1.jpg", techStepIds: [1], }); expect(translated.steps[1]).to.deep.equal({ description: "Servir", picture: null, techStepIds: [], }); }); it("passes every other field through unchanged", () => { const recipe = buildParsedRecipe(["Servir"]); const translated = translateRecipeSteps(recipe, []); expect(translated.name).to.equal(recipe.name); expect(translated.description).to.equal(recipe.description); expect(translated.picture).to.equal(recipe.picture); expect(translated.portions).to.equal(recipe.portions); expect(translated.sourceUrl).to.equal(recipe.sourceUrl); expect(translated.ingredients).to.deep.equal(recipe.ingredients); }); it("gives every step an empty sequence when there are no mappings at all", () => { const recipe = buildParsedRecipe(["Faire mijoter à feu doux", "Préchauffer le four"]); const translated = translateRecipeSteps(recipe, []); expect(translated.steps.map((step) => step.techStepIds)).to.deep.equal([[], []]); }); it("handles a recipe with no steps without error", () => { const recipe = buildParsedRecipe([]); const translated = translateRecipeSteps(recipe, [simmer]); expect(translated.steps).to.deep.equal([]); }); }); describe("translateRecipe", () => { beforeEach(async () => { await resetDatabase(); }); after(async () => { await prisma.$disconnect(); }); it("resolves real TechStep ids from the seeded French catalog", async () => { const simmer = await prisma.techStep.findFirstOrThrow({ where: { key: "simmer" } }); const chop = await prisma.techStep.findFirstOrThrow({ where: { key: "chop" } }); const recipe = buildParsedRecipe(["Hacher les oignons", "Faire mijoter à feu doux"]); const translated = await translateRecipe(recipe, "fr"); expect(translated.steps.map((step) => step.techStepIds)).to.deep.equal([ [chop.id], [simmer.id], ]); }); it("finds nothing for English text against the French catalog — the current locale gap", async () => { // A step lifted verbatim from a real TheMealDB recipe. const recipe = buildParsedRecipe([ "Bring a large saucepan of salted water to the boil", "Chop the onions finely", ]); const translated = await translateRecipe(recipe, "fr"); expect(translated.steps.map((step) => step.techStepIds)).to.deep.equal([[], []]); }); it("finds nothing for a locale with no mappings, even for text that would otherwise match", async () => { const recipe = buildParsedRecipe(["Faire mijoter à feu doux"]); const translated = await translateRecipe(recipe, "en"); expect(translated.steps.map((step) => step.techStepIds)).to.deep.equal([[]]); }); }); });