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. // Both flip once, from `false` to `true`, as the single scenario in this // file actually performs the import and 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 fishPieImported = false; 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] }); }); // Stateful — "Fish Pie" starts out not imported, and flips the moment // "importing the previewed item will succeed ..." below actually fires, so // re-browsing after the import journey completes reflects it without a page // reload (see this feature's closing assertions). Given("browsing TheMealDB returns some items", () => { cy.intercept("GET", "**/sources/theMealDb/browse*", (req) => { req.reply({ 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: fishPieImported, recipeId: fishPieImported ? 99 : 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: [] }], }, }); }); // 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", (req) => { fishPieImported = true; req.reply({ 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 for the same reason as "browsing TheMealDB returns some items" // above — 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 source item {string} should be marked as already imported", (title: string) => { cy.contains("tr", title).find(".source-item-table__imported-badge").should("be.visible"); }); 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"); }); }, );