import { Given, Then, When } from "@badeball/cypress-cucumber-preprocessor"; // Mocks the API via cy.intercept — this job doesn't run a live backend (see // .github/workflows/ci.yml); apps/api's own Mocha suite covers real API // behavior against a real database (see test/sources.test.ts, // test/planning.test.ts). // // planning.feature's journey crosses both `RecipePickerDialog` (browsing an // external source from a planning slot) and the import review screen // (`ImportRecipePage`) it hands off to — same "each spec's own // self-contained fixtures" precedent recipe-sources.ts already sets (the // Cucumber preprocessor's step lookup isn't global across cypress/e2e/, see // its own comment for the full reasoning), so most of what's below mirrors // recipe-sources.ts's fixtures rather than importing them. // Flips once, from `false` to `true`, as the single scenario in this file // actually performs the planning-add — module-level `let` rather than // something reset per-scenario, since there's only ever the one here (see // household-settings.ts for the same pattern used across several scenarios // instead). let fishPiePlanned = false; Given("the recipe catalog contains nothing", () => { cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] }); }); Given("the household has enabled TheMealDB", () => { cy.intercept("GET", "**/house/current/sources", { statusCode: 200, body: [1] }); }); Given("browsing TheMealDB returns some items", () => { cy.intercept("GET", "**/sources/theMealDb/browse*", { statusCode: 200, body: { items: [ { externalId: "52795", title: "Chicken Handi", picture: null, url: "https://www.themealdb.com/meal/52795", alreadyImported: true, recipeId: 2, }, { externalId: "9999", title: "Fish Pie", picture: null, url: "https://www.themealdb.com/meal/9999", alreadyImported: false, recipeId: null, }, // Draft's own preview ("previewing TheMealDB item ... is fully // resolved") has nothing left for a person to fix — unlike "Fish // Pie" above, exercises the transparent-import path instead of the // review screen. { externalId: "7777", title: "Ratatouille", picture: null, url: "https://www.themealdb.com/meal/7777", alreadyImported: false, recipeId: null, }, ], nextCursor: null, }, }); }); Given("previewing TheMealDB item {string} is available", (externalId: string) => { cy.intercept("GET", `**/sources/theMealDb/preview/${externalId}`, { statusCode: 200, body: { sourceKey: "theMealDb", externalId, name: "Fish Pie", description: null, picture: null, portions: 4, sourceUrl: "https://www.themealdb.com/meal/9999", ingredients: [ { rawText: "1 onion", quantity: 1, ingredient: { id: 1, key: "onion", icon: "VEGETABLE", category: "freshProduce", subcategory: "vegetables", reproducible: false, allergens: [], diets: [], }, unit: null, }, { rawText: "some mystery paste", quantity: null, ingredient: null, unit: null }, ], steps: [{ description: "Cuire à la poêle.", picture: null, techSteps: [] }], }, }); }); // Unlike "previewing TheMealDB item ... is available" above, every // ingredient line here already resolved to a real ingredient/unit/quantity // — `tryBuildCompleteImport` (RecipePickerDialog.tsx) accepts a draft // shaped exactly like this one as-is, no review screen needed. Given( "previewing TheMealDB item {string} is fully resolved as {string}", (externalId: string, name: string) => { cy.intercept("GET", `**/sources/theMealDb/preview/${externalId}`, { statusCode: 200, body: { sourceKey: "theMealDb", externalId, name, description: null, picture: null, portions: 4, sourceUrl: `https://www.themealdb.com/meal/${externalId}`, ingredients: [ { rawText: "1 onion", quantity: 1, ingredient: { id: 1, key: "onion", icon: "VEGETABLE", category: "freshProduce", subcategory: "vegetables", reproducible: false, allergens: [], diets: [], }, unit: { id: 1, key: "piece", type: "COUNT", toBaseFactor: 1 }, }, ], steps: [{ description: "Cuire à la poêle.", picture: null, techSteps: [] }], }, }); }, ); Given( "importing item {string} will succeed and return id {int}", (externalId: string, id: number) => { cy.intercept("POST", `**/sources/theMealDb/import/${externalId}`, { statusCode: 201, body: { id }, }).as("importItem"); }, ); Given("adding recipe {int} to the planning will succeed", (recipeId: number) => { cy.intercept("POST", "**/planning/items", { statusCode: 201, body: { id: 2, weekDay: "lundi", meal: "petit-dejeuner", portions: 4, recipe: { id: recipeId, name: "Ratatouille" }, }, }).as("addPlanningItem"); }); Then( "the import request for {string} should have included the name {string}", (_externalId: string, name: string) => { cy.wait("@importItem").its("request.body.name").should("eq", name); }, ); // Covers every reference catalog both `RecipePickerDialog` (ingredients/ // diets, for its own filters) and `ImportRecipePage` (ingredients/diets/ // units, for the review form) fetch — same endpoints, one fixture for both. Given("the ingredient and diet catalog is available for import", () => { cy.intercept("GET", "**/reference/ingredients", { statusCode: 200, body: [ { id: 1, key: "onion", icon: "VEGETABLE", category: "freshProduce", subcategory: "vegetables", allergens: [], diets: [], }, { id: 2, key: "salt", icon: "SPICE", category: "condimentsAndSpices", subcategory: "spices", allergens: [], diets: [], }, ], }); cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [{ id: 1, key: "omnivore" }], }); cy.intercept("GET", "**/reference/units", { statusCode: 200, body: [{ id: 1, key: "piece", type: "COUNT", toBaseFactor: 1 }], }); }); Given("importing the previewed item will succeed and return id {int}", (id: number) => { cy.intercept("POST", "**/sources/theMealDb/import/9999", { statusCode: 201, body: { id }, }).as("importRecipe"); }); Given("adding the imported recipe to the planning will succeed", () => { cy.intercept("POST", "**/planning/items", (req) => { fishPiePlanned = true; req.reply({ statusCode: 201, body: { id: 1, weekDay: "lundi", meal: "petit-dejeuner", portions: 4, recipe: { id: 99, name: "Fish Pie" }, }, }); }).as("addPlanningItem"); }); // Stateful — landing back on "/" after the import journey remounts // `PlanningPage` from scratch (a real cross-route navigation, not a // same-component state update: see `ImportRecipePage`'s `navigate("/")`), // so only a fresh `GET /planning?date=` that reflects the just-added item // makes it show up there — nothing client-side survives that remount to // patch it in locally the way `PlanningPage`'s own `patchPlanningItems` // does for an add made without leaving the page. Given("the planning request reflects whatever's been added so far", () => { cy.intercept("GET", /\/planning\?/, (req) => { req.reply({ statusCode: 200, body: fishPiePlanned ? { id: 1, startDate: "2026-08-17T00:00:00.000Z", finishDate: "2026-08-23T00:00:00.000Z", items: [ { id: 1, weekDay: "lundi", meal: "petit-dejeuner", portions: 4, recipe: { id: 99, name: "Fish Pie" }, }, ], } : null, }); }); }); // The very first "+" in DOM order is Lundi's Petit-déjeuner cell (`MEALS`'s // first entry × `WEEK_DAYS`'s first entry, see `PlanningGrid`) — the exact // slot this feature's fixtures above (weekDay "lundi", meal // "petit-dejeuner") are written against. When("I click the add button for the first empty planning slot", () => { cy.get(".add-recipe-btn").first().click(); }); When("I click the source item {string}", (title: string) => { cy.contains(".recipe-table__name", title).click(); }); When("I choose an ingredient for the unresolved line {string}", (rawText: string) => { cy.contains(".import-recipe__unresolved-row", rawText) .contains("button", "Choisir un ingrédient") .click(); }); Then( "the planning add request should have included recipe {int}, weekDay {string}, meal {string}, and portions {int}", (recipeId: number, weekDay: string, meal: string, portions: number) => { cy.wait("@addPlanningItem") .its("request.body") .should("deep.include", { recipeId, weekDay, meal, portions }); }, ); Then( "the recipe {string} should appear in the first planning slot with {int} portions", (name: string, portions: number) => { cy.get(".planning-grid tbody tr") .first() .within(() => { cy.contains(".recipe-chip", `${name} · ×${portions}`).should("be.visible"); }); }, );