// 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 // `GET /cooking-session` behavior (including the optimizer) against a real // database. const authenticatedProfile = { id: 1, firstName: "Alice", lastName: "Martin", email: "alice@example.com", tokenVersion: 0, houseId: 1, dietId: null, }; // 2026-08-17 is a Monday — frozen so "this week" is deterministic. const TODAY = new Date("2026-08-17T09:00:00Z"); /** Bare `IngredientView` — only `key` drives the page's label lookup. */ function ingredient(key: string) { return { id: 1, key, icon: "VEGETABLE", category: "freshProduce", subcategory: "vegetables", reproducible: false, allergens: [], diets: [], }; } /** A 3-phase plan: a pooled prep task in mise-en-place, then two cooking phases, the last with a passive cook floated into its background. */ function planFixture() { return { startDate: "2026-08-17T00:00:00.000Z", finishDate: "2026-08-23T00:00:00.000Z", recipes: [ { recipeId: 1, name: "Soupe à l'oignon", portions: 4 }, { recipeId: 2, name: "Tarte à l'oignon", portions: 6 }, ], phases: [ { index: 0, kind: "mise-en-place", background: [], tasks: [ { id: "prep:chop:onion", kind: "merged-prep", technique: { id: 1, key: "chop" }, description: null, ingredients: [ { ingredient: ingredient("onion"), quantity: 5, unit: { id: 2, key: "piece", type: "COUNT", toBaseFactor: 1 }, }, ], utensils: [{ id: 1, key: "knife" }], sourceRecipes: [ { recipeId: 1, name: "Soupe à l'oignon", portions: 4 }, { recipeId: 2, name: "Tarte à l'oignon", portions: 6 }, ], originalSteps: [], }, ], }, { index: 1, kind: "cooking", background: [], tasks: [ { id: "step:0:1", kind: "step", technique: { id: 5, key: "simmer" }, description: "Faire mijoter le bouillon", ingredients: [], utensils: [], sourceRecipes: [{ recipeId: 1, name: "Soupe à l'oignon", portions: 4 }], originalSteps: [], }, ], }, { index: 2, kind: "cooking", background: [ { id: "bg:step:0:1", technique: { id: 5, key: "simmer" }, description: "Faire mijoter le bouillon", recipeId: 1, recipeName: "Soupe à l'oignon", }, ], tasks: [ { id: "step:1:3", kind: "step", technique: { id: 4, key: "bake" }, description: "Enfourner la tarte", ingredients: [], utensils: [], sourceRecipes: [{ recipeId: 2, name: "Tarte à l'oignon", portions: 6 }], originalSteps: [], }, ], }, ], }; } describe("Cooking session page", () => { beforeEach(() => { cy.viewport(1400, 900); cy.clock(TODAY, ["Date"]); cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); }); it("shows the empty message when nothing is planned that week", () => { cy.intercept("GET", /\/cooking-session\?/, { statusCode: 200, body: { startDate: "2026-08-17T00:00:00.000Z", finishDate: "2026-08-23T00:00:00.000Z", recipes: [], phases: [], }, }); cy.visit("/cuisiner"); cy.contains("h1", "Cuisiner cette semaine").should("be.visible"); cy.contains("Rien de planifié cette semaine à cuisiner").should("be.visible"); }); it("opens on the Plan tab: a stepper of every phase, only the first phase's tasks shown", () => { cy.intercept("GET", /\/cooking-session\?/, { statusCode: 200, body: planFixture() }).as( "getPlan", ); cy.visit("/cuisiner?date=2026-08-17"); cy.wait("@getPlan").its("request.url").should("include", "date=2026-08-17"); // One stepper node per phase; the first is the current one. cy.get(".cooking-stepper__node").should("have.length", 3); cy.get(".cooking-stepper__node").first().should("have.class", "cooking-stepper__node--current"); cy.contains(".cooking-plan__position", "Étape 1 / 3").should("be.visible"); cy.contains(".cooking-plan__count", "0 / 1 tâches").should("be.visible"); // Mise en place: the pooled prep task, flagged shared, naming both recipes. cy.get(".cooking-task--merged-prep") .should("contain.text", "Hacher") .and("contain.text", "Oignon") .and("contain.text", "Mutualisé"); // The next phase's step is not rendered yet. cy.contains("Faire mijoter le bouillon").should("not.exist"); // Prev is disabled on the first phase. cy.contains(".cooking-plan__nav-btn", "Étape précédente").should("be.disabled"); }); it("walks the phases with the next/prev buttons and by clicking a stepper node", () => { cy.intercept("GET", /\/cooking-session\?/, { statusCode: 200, body: planFixture() }); cy.visit("/cuisiner?date=2026-08-17"); // Forward to phase 2. cy.contains(".cooking-plan__nav-btn", "Étape suivante").click(); cy.contains(".cooking-plan__position", "Étape 2 / 3").should("be.visible"); cy.contains(".cooking-phase__tasks", "Faire mijoter le bouillon").should("be.visible"); // Forward to phase 3 — its background band shows the soup still simmering. cy.contains(".cooking-plan__nav-btn", "Étape suivante").click(); cy.contains(".cooking-plan__position", "Étape 3 / 3").should("be.visible"); cy.contains(".cooking-phase__background", "Pendant ce temps") .should("contain.text", "Faire mijoter le bouillon") .and("contain.text", "Soupe à l'oignon"); cy.contains(".cooking-phase__tasks", "Enfourner la tarte").should("be.visible"); cy.contains(".cooking-plan__nav-btn", "Étape suivante").should("be.disabled"); // Back one step. cy.contains(".cooking-plan__nav-btn", "Étape précédente").click(); cy.contains(".cooking-plan__position", "Étape 2 / 3").should("be.visible"); // Jump straight back to the first phase from the stepper. cy.get(".cooking-stepper__node").first().click(); cy.contains(".cooking-plan__position", "Étape 1 / 3").should("be.visible"); cy.get(".cooking-task--merged-prep").should("be.visible"); }); it("ticks a task, marks its phase done in the stepper, and keeps the tick after a reload", () => { cy.intercept("GET", /\/cooking-session\?/, { statusCode: 200, body: planFixture() }); cy.visit("/cuisiner?date=2026-08-17"); cy.get(".cooking-task").first().find('input[type="checkbox"]').check({ force: true }); cy.get(".cooking-task").first().should("have.class", "cooking-task--done"); // The whole phase is now done — its stepper node reflects that. cy.get(".cooking-stepper__node").first().should("have.class", "cooking-stepper__node--done"); cy.contains(".cooking-plan__count", "1 / 1 tâches").should("be.visible"); cy.reload(); cy.get(".cooking-task").first().should("have.class", "cooking-task--done"); cy.get(".cooking-stepper__node").first().should("have.class", "cooking-stepper__node--done"); }); it("lists the week's recipes on the Recettes tab, each linking to its own page", () => { cy.intercept("GET", /\/cooking-session\?/, { statusCode: 200, body: planFixture() }); cy.visit("/cuisiner?date=2026-08-17"); cy.contains(".cooking-session-page__tab", "Recettes").click(); cy.get(".cooking-recipes__item").should("have.length", 2); cy.contains(".cooking-recipes__item", "Soupe à l'oignon") .find("a") .should("have.attr", "href", "/recettes/1"); cy.contains(".cooking-recipes__item", "Tarte à l'oignon") .should("contain.text", "6 portions") .find("a") .should("have.attr", "href", "/recettes/2"); // The stepper isn't part of this tab. cy.get(".cooking-stepper").should("not.exist"); }); it("shows an error state when the request fails", () => { cy.intercept("GET", /\/cooking-session\?/, { statusCode: 500, body: { code: 5000, message: "boom" }, }); cy.visit("/cuisiner"); cy.contains("Impossible de charger").should("be.visible"); }); });