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 <noreply@anthropic.com>
287 lines
9.1 KiB
TypeScript
287 lines
9.1 KiB
TypeScript
import { Given, Then, When } from "@badeball/cypress-cucumber-preprocessor";
|
|
|
|
/** Bare `IngredientView` — only `key` drives the page's `catalog.ingredients.*` lookup. */
|
|
function ingredient(key: string) {
|
|
return {
|
|
id: 1,
|
|
key,
|
|
icon: "VEGETABLE",
|
|
category: "freshProduce",
|
|
subcategory: "vegetables",
|
|
reproducible: false,
|
|
allergens: [],
|
|
diets: [],
|
|
};
|
|
}
|
|
|
|
Given(
|
|
"the planning for this week has recipes {string} and {string}",
|
|
(first: string, second: string) => {
|
|
cy.intercept("GET", /\/planning\?/, {
|
|
statusCode: 200,
|
|
body: {
|
|
id: 1,
|
|
startDate: "2026-08-17T00:00:00.000Z",
|
|
finishDate: "2026-08-23T00:00:00.000Z",
|
|
items: [
|
|
{
|
|
id: 1,
|
|
weekDay: "lundi",
|
|
meal: "dejeuner",
|
|
portions: 4,
|
|
recipe: { id: 1, name: first },
|
|
},
|
|
{ id: 2, weekDay: "mardi", meal: "diner", portions: 4, recipe: { id: 2, name: second } },
|
|
],
|
|
},
|
|
});
|
|
},
|
|
);
|
|
|
|
Given(
|
|
"the cooking plan for {string} pools {string} of {string} across both recipes",
|
|
(date: string, _techniqueLabel: string, _ingredientLabel: string) => {
|
|
// The page composes the headline itself from the technique/ingredient
|
|
// *keys* via i18n — `chop`→"Hacher", `onion`→"Oignon" — so the fixture
|
|
// carries keys; the `Then` step checks the rendered French labels the
|
|
// feature line names.
|
|
cy.intercept("GET", `**/cooking-session?date=${date}`, {
|
|
statusCode: 200,
|
|
body: {
|
|
startDate: `${date}T00: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: 4 },
|
|
],
|
|
phases: [
|
|
{
|
|
index: 0,
|
|
kind: "mise-en-place",
|
|
background: [],
|
|
notes: [],
|
|
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: [],
|
|
temperatures: [],
|
|
sourceRecipes: [
|
|
{ recipeId: 1, name: "Soupe à l'oignon", portions: 4 },
|
|
{ recipeId: 2, name: "Tarte à l'oignon", portions: 4 },
|
|
],
|
|
originalSteps: [],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
},
|
|
);
|
|
|
|
Then(
|
|
"the pooled prep task should mention {string} and {string}",
|
|
(techniqueLabel: string, ingredientLabel: string) => {
|
|
cy.get(".cooking-task--merged-prep")
|
|
.should("contain.text", techniqueLabel)
|
|
.and("contain.text", ingredientLabel);
|
|
},
|
|
);
|
|
|
|
Then("the pooled prep task should be flagged as shared", () => {
|
|
cy.get(".cooking-task--merged-prep").contains("Mutualisé").should("be.visible");
|
|
});
|
|
|
|
Then("the pooled prep task should sit in the recipe section {string}", (sectionTitle: string) => {
|
|
cy.contains(".cooking-phase__recipe-group", sectionTitle)
|
|
.find(".cooking-task--merged-prep")
|
|
.should("exist");
|
|
});
|
|
|
|
Given("the cooking plan for {string} has a mise en place then a cooking step", (date: string) => {
|
|
// Two phases so the stepper actually has somewhere to step to: the
|
|
// pooled `chop onion` prep, then a `simmer` cooking step.
|
|
cy.intercept("GET", `**/cooking-session?date=${date}`, {
|
|
statusCode: 200,
|
|
body: {
|
|
startDate: `${date}T00: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: [],
|
|
notes: [],
|
|
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: [],
|
|
temperatures: [],
|
|
sourceRecipes: [
|
|
{ recipeId: 1, name: "Soupe à l'oignon", portions: 4 },
|
|
{ recipeId: 2, name: "Tarte à l'oignon", portions: 6 },
|
|
],
|
|
originalSteps: [],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
index: 1,
|
|
kind: "cooking",
|
|
background: [],
|
|
notes: [],
|
|
tasks: [
|
|
{
|
|
id: "step:0:1",
|
|
kind: "step",
|
|
technique: { id: 5, key: "simmer" },
|
|
description: "Faire mijoter le bouillon",
|
|
ingredients: [],
|
|
utensils: [],
|
|
temperatures: [],
|
|
sourceRecipes: [{ recipeId: 1, name: "Soupe à l'oignon", portions: 4 }],
|
|
originalSteps: [],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
Given("the cooking plan for {string} needs the oven at 180 °C and 210 °C", (date: string) => {
|
|
const celsius = (value: number) => ({ celsius: value, gasMark: null, qualitative: null });
|
|
const preheatStep = (id: string, recipe: { recipeId: number; name: string }, value: number) => ({
|
|
id,
|
|
kind: "step",
|
|
technique: { id: 9, key: "preheat" },
|
|
description: `Préchauffer le four à ${value}°C`,
|
|
ingredients: [],
|
|
utensils: [],
|
|
temperatures: [celsius(value)],
|
|
sourceRecipes: [recipe],
|
|
originalSteps: [],
|
|
});
|
|
const gratin = { recipeId: 1, name: "Gratin", portions: 4 };
|
|
const roti = { recipeId: 2, name: "Rôti", portions: 4 };
|
|
cy.intercept("GET", `**/cooking-session?date=${date}`, {
|
|
statusCode: 200,
|
|
body: {
|
|
startDate: `${date}T00:00:00.000Z`,
|
|
finishDate: "2026-08-23T00:00:00.000Z",
|
|
recipes: [gratin, roti],
|
|
phases: [
|
|
{
|
|
index: 0,
|
|
kind: "mise-en-place",
|
|
background: [],
|
|
notes: [
|
|
{
|
|
kind: "appliance-temperature-conflict",
|
|
technique: { id: 9, key: "preheat" },
|
|
temperatures: [celsius(180), celsius(210)],
|
|
},
|
|
],
|
|
// Coolest first, as the optimizer emits them.
|
|
tasks: [preheatStep("step:0:1", gratin, 180), preheatStep("step:1:2", roti, 210)],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
Then("I should see the oven conflict note {string}", (text: string) => {
|
|
cy.get(".cooking-phase__note").should("contain.text", text);
|
|
});
|
|
|
|
Then("I should see the recipe sections {string} and {string}", (first: string, second: string) => {
|
|
cy.contains(".cooking-phase__recipe-group-title", first).should("be.visible");
|
|
cy.contains(".cooking-phase__recipe-group-title", second).should("be.visible");
|
|
});
|
|
|
|
When("I open the {string} cooking tab", (label: string) => {
|
|
cy.contains(".cooking-session-page__tab", label).click();
|
|
});
|
|
|
|
// The task card is a `<label>` wrapping a visually-hidden checkbox — click
|
|
// the card, same approach as common.steps.ts's "I click the radio".
|
|
When("I tick the cooking task {string}", (headline: string) => {
|
|
cy.contains(".cooking-task", headline).click();
|
|
});
|
|
|
|
Then("the cooking task {string} should be done", (headline: string) => {
|
|
cy.contains(".cooking-task", headline).should("have.class", "cooking-task--done");
|
|
});
|
|
|
|
When("I go to the next cooking step", () => {
|
|
cy.contains(".cooking-plan__nav-btn", "Étape suivante").click();
|
|
});
|
|
|
|
When("I go to the previous cooking step", () => {
|
|
cy.contains(".cooking-plan__nav-btn", "Étape précédente").click();
|
|
});
|
|
|
|
Then("the current cooking step should be {string}", (position: string) => {
|
|
cy.contains(".cooking-plan__position", position).should("be.visible");
|
|
});
|
|
|
|
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 `<label>` wrapping a visually-hidden checkbox — see
|
|
// "I tick the cooking task" above; a recipe row is a plain `<button>`,
|
|
// clicked the same straightforward way.
|
|
When("I click the recipe {string}", (name: string) => {
|
|
cy.contains(".cooking-recipes__row", name).click();
|
|
});
|
|
|
|
Then("the recipe detail {string} should be expanded", (name: string) => {
|
|
cy.get(".cooking-recipes__detail .recipe-detail-panel").within(() => {
|
|
cy.contains("h2", name).should("be.visible");
|
|
});
|
|
});
|