batchCooking/apps/web/cypress/e2e/recipes.ts
Nicolas 6c5978dd1c
All checks were successful
CI / lint (push) Successful in 2m35s
CI / build (push) Successful in 2m38s
CI / intent-service-test (push) Successful in 4m23s
CI / e2e (push) Successful in 8m0s
CI / test (push) Successful in 15m1s
feat(recipes): refond le panneau de correction des métadonnées de tech-step
Suite à la maquette validée : redessine TechStepCorrectionPopover et ajoute
l'édition de la température, jusqu'ici la seule des 4 métadonnées sans
aucune interface (ni dans l'UI, ni côté API — voir la PR back-end empilée
sous celle-ci).

- StepDescription.tsx : le surlignage de température devient cliquable
  (un <button>, plus un <span> mort) — rouvre le popover de correction de
  la technique à laquelle il est rattaché (findTechStepForTemperature,
  résolu par span puisque la température n'a pas d'existence propre, elle
  est une métadonnée de clause comme les ingrédients/ustensiles).
- TechStepCorrectionPopover.tsx : réécrit. Les 4 sections (Technique,
  Ingrédients, Ustensiles, Température) restent toutes visibles en même
  temps — plus de bascule de tout le popover vers un sous-écran pendant
  l'ajout d'un ingrédient/ustensile/température, seule la section
  concernée affiche son propre mini-formulaire, les autres (dont le
  picker de technique) restent inchangées à côté. Le picker de technique
  se cache derrière "Changer" une fois une technique choisie (rien à
  décider une fois confirmée correcte). Chaque ingrédient/ustensile/
  température attaché s'affiche en ligne avec sa citation exacte du texte
  source (nouveau : la traçabilité vers le texte existait déjà en donnée,
  mais n'était affichée nulle part). Nouveau sélecteur à 3 modes
  (°C / Thermostat / Qualitatif) pour la température, cohérent avec le
  modèle de données (un seul des trois à la fois). Icônes SVG par
  catégorie (tag/feuille/poêle/thermomètre), même langage stroke-based
  que layouts/nav-icons.tsx.
- recipes.scss : nouveau langage visuel du popover (sections avec icône,
  lignes de métadonnées façon carte plutôt que puces, bouton "+ Ajouter"
  en pointillés plutôt qu'un lien texte, sélecteurs segmentés). Les deux
  surlignages (.step-tech-step, .step-temperature) passent de
  `cursor: help` à `cursor: pointer` — les deux sont maintenant de vrais
  déclencheurs cliquables.
- i18n : nouvelles clés (changeTechnique, temperatureSection,
  addTemperature, removeTemperature, temperatureMode.*) ; clé
  currentTechnique supprimée (le pictogramme remplace la phrase "Technique
  retenue : X") ; préfixe "+ " retiré de addIngredient/addUtensil (l'icône
  + le porte déjà visuellement).
- Tests : TechStepCorrectionPopover.cy.tsx largement réécrit (10 tests,
  dont 2 nouveaux sur la température) ; recipes.feature/recipes.ts : nouveau
  scénario Gherkin "cliquer un surlignage de température rouvre le popover
  de sa technique" (fixture Gratin dédiée), step "Changer" ajouté au
  scénario de relabel existant.

Vérifié : pnpm lint + build (shared/api/web) clean. Suites Cypress
exécutées localement (Chrome headless) : TechStepCorrectionPopover.cy.tsx
10/10, suite component complète 50/50, recipes.feature 5/5, recipes.cy.ts
8/8, recipe-sources.feature 6/6, planning-page.cy.ts 7/7. Un échec
pré-existant et sans rapport (recipe-form.feature, "Preloads an existing
recipe's ingredients when editing") reproduit identiquement avec ce
travail entièrement stashé — confirmé non lié.

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

271 lines
9.5 KiB
TypeScript

import { Given, Then, When } from "@badeball/cypress-cucumber-preprocessor";
const oeufs = { id: 2, key: "eggs", kind: "ALLERGY" };
const omelette = {
id: 2,
name: "Omelette",
description: null,
picture: null,
portions: 2,
authorId: 1,
visibility: "PERSONAL",
allergens: [oeufs],
diets: [],
isFavorite: false,
};
const omeletteDetail = {
...omelette,
description: "Une omelette toute simple.",
ingredients: [
{
ingredient: {
id: 10,
key: "egg",
icon: "EGG",
category: "dairyAndCheese",
subcategory: "eggs",
allergens: [oeufs],
diets: [],
},
quantity: 3,
unit: { id: 1, key: "piece", type: "COUNT", toBaseFactor: 1 },
},
],
steps: [
{ id: 1, description: "Battre les œufs.", picture: null, order: 1, techSteps: [] },
{
id: 2,
description: "Cuire à la poêle.",
picture: null,
order: 2,
// "Cuire" -> the `cook` technique, matching real reference-seed-data.ts
// (`\bcui(re|sez|sant|sson)\b`) — "poêle" itself matches nothing
// (that's `panFry`'s "sauter", a different word).
techSteps: [{ techStep: { id: 1, key: "cook" }, start: 0, end: 5, source: "auto" }],
},
],
};
/**
* A separate, self-contained fixture for the "clicking a temperature
* highlight" scenario — kept apart from `omeletteDetail` above rather than
* adding a temperature to its "Cuire" occurrence, so this doesn't entangle
* with that fixture's own "Corrects a detected technique" scenario (which
* relabels the same occurrence without touching metadata, and must keep
* meaning "nothing here changed").
*/
const gratinDetail = {
id: 4,
name: "Gratin",
description: null,
picture: null,
portions: 4,
authorId: 1,
visibility: "PERSONAL",
allergens: [],
diets: [],
isFavorite: false,
ingredients: [],
steps: [
{
id: 10,
// "Préchauffez" = [0, 11), "à feu moyen" = [20, 31) — counted char by
// char (accented/multi-byte characters here are each still one JS
// string index, same convention `start`/`end` use everywhere else).
description: "Préchauffez le four à feu moyen.",
picture: null,
order: 1,
techSteps: [
{
techStep: { id: 9, key: "preheat" },
start: 0,
end: 11,
source: "auto",
temperatures: [
{ celsius: null, gasMark: null, qualitative: "medium", start: 20, end: 31 },
],
},
],
},
],
};
Given("the disliked ingredients list is empty", () => {
cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] });
});
Given("the recipe catalog contains {string}", () => {
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [omelette] });
});
Given("recipe 2's detail is available", () => {
cy.intercept("GET", "**/recipes/2", { statusCode: 200, body: omeletteDetail }).as("getRecipe");
});
Given("toggling recipe 2's favorite will succeed", () => {
cy.intercept("POST", "**/recipes/2/favorite", { statusCode: 204 }).as("favorite");
});
Given("deleting recipe 2 will succeed", () => {
cy.intercept("DELETE", "**/recipes/2", { statusCode: 204 }).as("deleteRecipe");
});
Given("recipe 4's detail is available", () => {
cy.intercept("GET", "**/recipes/4", { statusCode: 200, body: gratinDetail });
});
// TechStepCorrectionPopover fetches the full technique catalog on mount
// (to resolve the chosen technique's own label) — a dedicated fixture
// naming just `preheat` (`gratinDetail`'s own technique), kept separate
// from "the tech steps reference list has options" above (that one's
// `cook`/`simmer` pair is purpose-built for the *other* scenario's
// relabel, not a generic catalog stand-in).
Given("the tech steps reference list includes preheat", () => {
cy.intercept("GET", "**/reference/tech-steps", {
statusCode: 200,
body: [{ id: 9, key: "preheat" }],
});
});
// Step 2 is `omeletteDetail`'s "Cuire à la poêle." step, whose only
// existing match is `cook` (id 1) — see that fixture above. The response
// mirrors `SubmitTechStepCorrectionResult` (packages/shared): the audit
// record (reassigning the match to `simmer`, id 3, "Mijoter" — see `the
// tech steps reference list has options`, reference-data.steps.ts) plus
// the step's fresh `techSteps`, now showing that same reassignment as a
// `"manual"`-sourced entry — the API applies a correction immediately, it
// doesn't just record it (see `StepTechStepView.source`'s own doc comment).
Given('correcting step 2\'s "Cuire" match will succeed', () => {
cy.intercept("POST", "**/recipes/2/steps/2/corrections", {
statusCode: 201,
body: {
correction: {
id: 1,
start: 0,
end: 5,
previousTechStep: { id: 1, key: "cook" },
correctedTechStep: { id: 3, key: "simmer" },
createdAt: new Date().toISOString(),
},
techSteps: [
{
techStep: { id: 3, key: "simmer" },
start: 0,
end: 5,
source: "manual",
ingredients: [],
utensils: [],
temperatures: [],
},
],
},
}).as("correction");
});
When("I click the highlighted technique {string}", (text: string) => {
cy.contains(".step-tech-step", text).click();
});
Then("I should see the technique correction options", () => {
cy.get(".tech-step-correction-popover").should("be.visible");
});
Then("I should see the highlighted temperature {string}", (text: string) => {
// Same "off the fold, needs an explicit scroll" reasoning as
// common.steps.ts's "I should see the highlighted technique" — a bare
// `.should("be.visible")` doesn't auto-scroll.
cy.contains(".step-temperature", text).scrollIntoView().should("be.visible");
});
// Temperature is corrected through its owning technique's own popover, not
// on its own — see `StepDescription.tsx`'s `findTechStepForTemperature`.
When("I click the highlighted temperature {string}", (text: string) => {
cy.contains(".step-temperature", text).click();
});
Then("the chosen technique should be {string}", (label: string) => {
cy.contains(".tech-step-correction-popover__chosen-technique", label).should("be.visible");
});
// Picking a technique only *selects* it — it takes a separate "Valider"
// click to actually submit (room was made for attaching ingredient/utensil
// metadata alongside it, see `TechStepCorrectionPopover.tsx`'s own doc
// comment on its merged editor) — folded into this one step since nothing
// in this scenario cares about that intermediate state on its own. The
// technique catalog is picked via the same searchable `CatalogSearchPicker`
// the ingredient/utensil sub-flows use, scoped to
// `__technique-section` since that same search-and-pick component is
// reused inside this popover for more than just techniques. This step is
// only ever used to *relabel* an existing match (this feature's one
// scenario using it) — the picker hides behind "Changer" in that case
// (nothing to decide once a technique's already confirmed correct, see
// TechStepCorrectionPopover's own doc comment), so it's reopened first.
When("I choose {string} as the correct technique", (label: string) => {
cy.contains(".tech-step-correction-popover__technique-change", "Changer").click();
cy.contains(
".tech-step-correction-popover__technique-section .catalog-search-picker__list button",
label,
).click();
cy.contains(".tech-step-correction-popover__confirm-button", "Valider").click();
});
Then(
"the highlighted technique {string} should be marked as a manual correction",
(text: string) => {
cy.contains(".step-tech-step", text).should("have.class", "step-tech-step--manual");
},
);
Then("the correction request should have been made", () => {
// Asserts the actual span, not just that *a* request fired — a real bug
// (StepDescription.tsx's click handler reading a shared, still-mutating
// `offset` variable by reference instead of a value captured at render
// time) once sent `end` all the way to the end of the description
// instead of "Cuire"'s own tight [0, 5) span, and a request-fired-only
// assertion here didn't catch it — found only via manual testing.
cy.wait("@correction")
.its("request.body")
.should("deep.include", { start: 0, end: 5, previousTechStepId: 1 });
});
Then("the recipe {string} should not be visible in the table", (name: string) => {
cy.contains(".recipe-table__name", name).should("not.exist");
});
Then("the recipe {string} should be marked as favorite", (name: string) => {
cy.contains(".recipe-table__name", name).find(".recipe-table__fav-mark").should("exist");
});
Then("the recipe {string} should not be marked as favorite", (name: string) => {
cy.contains(".recipe-table__name", name).find(".recipe-table__fav-mark").should("not.exist");
});
When("I click the favorite star", () => {
cy.get(".favorite-star-button").click();
});
Then("the favorite request should have been made", () => {
cy.wait("@favorite");
});
Then("the favorite star should be marked as favorite", () => {
cy.get(".favorite-star-button").should("have.class", "is-favorite");
});
When("I click {string} in the recipe detail panel", (text: string) => {
cy.contains(".recipe-detail-panel__danger-button", text).click();
});
When("I confirm the deletion in the recipe detail panel", () => {
cy.contains(".recipe-detail-panel__danger-button", "Confirmer la suppression").click();
});
Then("the delete request should have been made", () => {
cy.wait("@deleteRecipe");
});
Then("the URL should match the recipes list", () => {
cy.url().should("match", /\/recettes\/?$/);
});