batchCooking/apps/web/cypress/e2e/cooking-session.ts
Nicolas 85268729e6
All checks were successful
CI / lint (push) Successful in 1m57s
CI / build (push) Successful in 3m2s
CI / e2e (push) Successful in 7m59s
CI / intent-service-test (push) Successful in 18m44s
CI / test (push) Successful in 28m2s
feat(cooking-session): regroupement d'étapes sensible aux métadonnées d'action
L'optimiseur ne mutualisait que le pré-découpage. Il regroupe maintenant
aussi les actions SETUP (préchauffage, ébullition) entre recettes, mais
seulement quand elles sont compatibles : deux préchauffages ne fusionnent
que s'ils visent la même température détectée. Deux préchauffages à des
températures différentes restent séparés, ordonnés du plus froid au plus
chaud, et la mise en place porte une note « Four requis à N températures —
enchaîner … ». Défaut optimiste : deux préchauffages sans température
détectée fusionnent quand même (supposés compatibles).

- cooking-optimizer.ts : temperatureKey (comparaison exacte, aucune
  conversion inter-représentations), isPureSetupStep / setupSignature /
  extractMergedSetup en miroir du chemin merged-prep ; comparableTemp pour
  l'ordre seul ; detectApplianceTemperatureConflicts → CookingPhaseView.notes
- cooking-session.service.ts : `temperatures` dans l'include + mapping
- packages/shared : CookingTemperatureView, kind « merged-setup »,
  CookingTaskView.temperatures, CookingPhaseNoteView, CookingPhaseView.notes
- web : temperature-label.ts extrait de StepDescription et réutilisé ;
  taskHeadline gère merged-setup ; phaseNoteText ; bandeau .cooking-phase__note
- i18n : cookingSession.task.mergedSetupLabel, cookingSession.note.*
- tests : 7 cas Mocha « setup pooling » + test DB cooking-session,
  cas Cypress mode layout + scénario Gherkin

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 12:05:44 +02:00

243 lines
7.7 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");
});
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);
});
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");
});
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);
});