From bdc1b4c9e92c73858d667352fc36786c6f1c1bb0 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 4 Sep 2026 23:08:47 +0200 Subject: [PATCH] =?UTF-8?q?feat(cooking-session):=20d=C3=A9plie=20une=20re?= =?UTF-8?q?cette=20de=20l'onglet=20Recettes=20au=20lieu=20de=20rediriger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cliquer sur une recette de l'onglet Recettes de /cuisiner naviguait vers /recettes/:id, quittant la session de cuisine en cours. La ligne devient un bouton qui déplie désormais le détail complet de la recette directement sous elle (RecipeDetailPanel, showActions=false — même traitement "aperçu" que RecipePickerDialog), chargé à la demande via GET /recipes/:id, un seul dépliant ouvert à la fois (même logique que le sous-menu Paramètres de la sidebar). RecipesTab est désormais keyed par semaine comme PlanTab, pour que changer de semaine referme un dépliant devenu obsolète. Clé i18n recipesTab.viewRecipe supprimée (plus d'appelant). Tests : cooking-session-page.cy.ts (déplier/replier, un seul ouvert à la fois) + cooking-session.feature (scénario dédié). pnpm lint + pnpm --filter web build clean ; 16/16 tests passants (Chrome headless). Co-Authored-By: Claude Sonnet 5 --- .../cypress/e2e/cooking-session-page.cy.ts | 72 +++++++++-- apps/web/cypress/e2e/cooking-session.feature | 12 +- apps/web/cypress/e2e/cooking-session.ts | 37 +++++- apps/web/src/locales/fr/translation.json | 3 +- .../cooking-session/CookingSessionPage.tsx | 114 ++++++++++++++---- .../cooking-session/cooking-session-page.scss | 68 +++++++++-- 6 files changed, 258 insertions(+), 48 deletions(-) diff --git a/apps/web/cypress/e2e/cooking-session-page.cy.ts b/apps/web/cypress/e2e/cooking-session-page.cy.ts index 22ac292..f3312c0 100644 --- a/apps/web/cypress/e2e/cooking-session-page.cy.ts +++ b/apps/web/cypress/e2e/cooking-session-page.cy.ts @@ -186,6 +186,28 @@ function setupPlanFixture() { }; } +/** + * Minimal `RecipeView` detail for one of `planFixture()`'s recipes — just + * enough (name/portions/one step) to prove the right recipe's own detail + * rendered inline, not a full catalog-style fixture. + */ +function recipeDetail(id: number, name: string, portions: number, stepDescription: string) { + return { + id, + name, + description: null, + picture: null, + portions, + authorId: 1, + visibility: "PERSONAL", + allergens: [], + diets: [], + isFavorite: false, + ingredients: [], + steps: [{ id: 1, description: stepDescription, picture: null, order: 1, techSteps: [] }], + }; +} + describe("Cooking session page", () => { beforeEach(() => { cy.viewport(1400, 900); @@ -287,25 +309,59 @@ describe("Cooking session page", () => { 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", () => { + it("lists the week's recipes on the Recettes tab, each showing its portions", () => { 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"); + cy.contains(".cooking-recipes__item", "Soupe à l'oignon").should("be.visible"); + cy.contains(".cooking-recipes__item", "Tarte à l'oignon").should("contain.text", "6 portions"); // The stepper isn't part of this tab. cy.get(".cooking-stepper").should("not.exist"); }); + it("expands a recipe's detail inline on the Recettes tab instead of navigating away", () => { + cy.intercept("GET", /\/cooking-session\?/, { statusCode: 200, body: planFixture() }); + cy.intercept("GET", "**/recipes/1", { + statusCode: 200, + body: recipeDetail(1, "Soupe à l'oignon", 4, "Émincer les oignons."), + }).as("getSoupe"); + cy.intercept("GET", "**/recipes/2", { + statusCode: 200, + body: recipeDetail(2, "Tarte à l'oignon", 6, "Étaler la pâte."), + }).as("getTarte"); + cy.visit("/cuisiner?date=2026-08-17"); + cy.contains(".cooking-session-page__tab", "Recettes").click(); + + // Nothing unfolded yet, and no more link out to the catalog page — + // clicking a row is what unfolds it now. + cy.get(".cooking-recipes").find("a").should("not.exist"); + cy.get(".cooking-recipes__detail").should("not.exist"); + + cy.contains(".cooking-recipes__row", "Soupe à l'oignon").click(); + cy.wait("@getSoupe"); + cy.location("pathname").should("eq", "/cuisiner"); // no navigation happened + cy.get(".cooking-recipes__detail .recipe-detail-panel").within(() => { + cy.contains("h2", "Soupe à l'oignon").should("be.visible"); + cy.contains("Émincer les oignons.").should("be.visible"); + }); + + // Only one recipe unfolds at a time — opening Tarte's closes Soupe's. + cy.contains(".cooking-recipes__row", "Tarte à l'oignon").click(); + cy.wait("@getTarte"); + cy.get(".cooking-recipes__detail").should("have.length", 1); + cy.get(".cooking-recipes__detail .recipe-detail-panel") + .should("contain.text", "Étaler la pâte.") + .and("not.contain.text", "Émincer"); + + // Clicking the open one again collapses it back. + cy.contains(".cooking-recipes__row", "Tarte à l'oignon").click(); + cy.get(".cooking-recipes__detail").should("not.exist"); + }); + it("renders a merged-setup preheat with its temperature and the oven-conflict note", () => { cy.intercept("GET", /\/cooking-session\?/, { statusCode: 200, body: setupPlanFixture() }); cy.visit("/cuisiner?date=2026-08-17"); diff --git a/apps/web/cypress/e2e/cooking-session.feature b/apps/web/cypress/e2e/cooking-session.feature index 13a09ee..892d6c3 100644 --- a/apps/web/cypress/e2e/cooking-session.feature +++ b/apps/web/cypress/e2e/cooking-session.feature @@ -52,8 +52,16 @@ Feature: Start cooking an optimized plan When I visit "/cuisiner?date=2026-08-17" And I open the "Recettes" cooking tab Then I should see "Soupe à l'oignon" - And the recipe "Soupe à l'oignon" should link to "/recettes/1" - And the recipe "Tarte à l'oignon" should link to "/recettes/2" + + Scenario: Clicking a recipe on the Recettes tab unfolds it instead of navigating away + Given the planning for this week has recipes "Soupe à l'oignon" and "Tarte à l'oignon" + And the cooking plan for "2026-08-17" has a mise en place then a cooking step + And recipe 1's detail is available + When I visit "/cuisiner?date=2026-08-17" + And I open the "Recettes" cooking tab + And I click the recipe "Soupe à l'oignon" + Then the recipe detail "Soupe à l'oignon" should be expanded + And the URL should include "/cuisiner" Scenario: Two recipes preheat the oven to different temperatures Given the planning for this week has recipes "Gratin" and "Rôti" diff --git a/apps/web/cypress/e2e/cooking-session.ts b/apps/web/cypress/e2e/cooking-session.ts index a6d493f..3040b38 100644 --- a/apps/web/cypress/e2e/cooking-session.ts +++ b/apps/web/cypress/e2e/cooking-session.ts @@ -249,6 +249,39 @@ Then("the current cooking step should be {string}", (position: string) => { cy.contains(".cooking-plan__position", position).should("be.visible"); }); -Then("the recipe {string} should link to {string}", (name: string, href: string) => { - cy.contains(".cooking-recipes__item", name).find("a").should("have.attr", "href", href); +Given("recipe 1's detail is available", () => { + // Minimal `RecipeView` — just enough (name + one step) to prove the + // right recipe's own detail unfolded inline. + cy.intercept("GET", "**/recipes/1", { + statusCode: 200, + body: { + id: 1, + name: "Soupe à l'oignon", + description: null, + picture: null, + portions: 4, + authorId: 1, + visibility: "PERSONAL", + allergens: [], + diets: [], + isFavorite: false, + ingredients: [], + steps: [ + { id: 1, description: "Émincer les oignons.", picture: null, order: 1, techSteps: [] }, + ], + }, + }); +}); + +// The task card is a `