From bd607b17f323a7e14ced9e0839d5016288c1785c Mon Sep 17 00:00:00 2001 From: Nicolas Date: Sat, 5 Sep 2026 08:21:16 +0200 Subject: [PATCH] =?UTF-8?q?fix(recipes):=20corrige=20la=20num=C3=A9rotatio?= =?UTF-8?q?n=20des=20=C3=A9tapes=20fauss=C3=A9e=20par=20le=20popover=20de?= =?UTF-8?q?=20correction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .recipe-detail-panel__steps utilisait un compteur CSS (counter-reset/ counter-increment: step) sur un sélecteur `li` — un descendant, pas seulement les enfants directs. Depuis la refonte du popover de correction (tech-step-metadata-editor-redesign), celui-ci imbrique ses propres
  • plusieurs niveaux plus bas (une par ingrédient/ustensile/température attaché, plus la liste du picker de technique — jusqu'à ~74 entrées) : chacun incrémentait ce même compteur "step", faussant le numéro affiché de toutes les étapes suivantes dès qu'une correction avec métadonnées attachées (ou son picker de technique) était ouverte. C'est le bug rapporté : "les étapes affichées sont numérotées n'importe comment". Correctif : `li`/`img`/`p` → `> li`/`> img`/`> p` (enfants directs uniquement) dans cette règle. Même principe pour `img`/`p` : leurs styles (image bloc, paragraphe inline sans marge) ne doivent s'appliquer qu'au propre

    / de l'étape, pas à ceux du popover imbriqué plus loin. Test de non-régression ajouté (recipes.cy.ts) : ouvre une correction avec ingrédient + ustensile attachés puis le picker de technique, vérifie via getComputedStyle que ces

  • imbriqués n'ont pas rejoint la chaîne du compteur (`counter-increment: none`) alors que les
  • des étapes elles-mêmes l'ont toujours (`counter-increment: step 1`) — confirmé que ce test échoue bien sans le correctif (`step 1` au lieu de `none`) avant de le restaurer. Vérifié : pnpm lint + build clean. recipes.cy.ts (9/9, dont le nouveau test), recipes.feature (5/5), recipe-sources.feature (6/6). Co-Authored-By: Claude Sonnet 5 --- apps/web/cypress/e2e/recipes.cy.ts | 106 +++++++++++++++++++++ apps/web/src/features/recipes/recipes.scss | 22 ++++- 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/apps/web/cypress/e2e/recipes.cy.ts b/apps/web/cypress/e2e/recipes.cy.ts index a3190df..d3a4958 100644 --- a/apps/web/cypress/e2e/recipes.cy.ts +++ b/apps/web/cypress/e2e/recipes.cy.ts @@ -69,6 +69,64 @@ const omeletteDetail = { ], }; +/** + * A 3-step recipe whose middle step already has an existing technique match + * carrying attached ingredient/utensil metadata — for the step-numbering + * regression test below, which needs a correction popover with several + * `
  • ` rows of its own open at once (see that test's own comment). + */ +const tarteDetail = { + id: 5, + name: "Tarte", + description: null, + picture: null, + portions: 4, + authorId: 1, + visibility: "PERSONAL", + allergens: [], + diets: [], + isFavorite: false, + ingredients: [], + steps: [ + { id: 1, description: "Préchauffer le four.", picture: null, order: 1, techSteps: [] }, + { + id: 2, + description: "Faire revenir les oignons avec une poêle.", + picture: null, + order: 2, + techSteps: [ + { + techStep: { id: 2, key: "panFry" }, + start: 0, + end: 13, + source: "auto", + ingredients: [ + { + ingredient: { + id: 10, + key: "onion", + icon: "VEGETABLE", + category: "freshProduce", + subcategory: "vegetables", + allergens: [], + diets: [], + }, + quantity: 2, + unit: { id: 2, key: "piece", type: "COUNT", toBaseFactor: 1 }, + start: 18, + end: 25, + source: "auto", + }, + ], + utensils: [{ utensil: { id: 20, key: "pan" }, start: 31, end: 40, source: "auto" }], + temperatures: [], + }, + ], + }, + { id: 3, description: "Servir chaud.", picture: null, order: 3, techSteps: [] }, + ], +}; + function interceptAuth() { cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] }); @@ -187,6 +245,54 @@ describe("Recipe catalog", () => { }); }); + // Regression: `.recipe-detail-panel__steps`'s CSS step counter + // (recipes.scss) used a bare descendant `li` selector, matching *any* + // `
  • ` nested inside a step — including ones several levels down + // inside `TechStepCorrectionPopover` (each attached ingredient/utensil/ + // temperature row, and the technique picker's own `
  • ` list). Opening + // a correction with attached metadata inflated the counter, throwing off + // every *later* step's own displayed number — exactly the "les étapes + // affichées sont numérotées n'importe comment" bug report this covers. + it("keeps every step's own number correct while a correction popover with attached metadata is open", () => { + cy.viewport(1600, 900); + cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] }); + cy.intercept("GET", "**/recipes/5", { statusCode: 200, body: tarteDetail }); + cy.intercept("GET", "**/reference/tech-steps", { + statusCode: 200, + body: [{ id: 2, key: "panFry" }], + }); + + cy.visit("/recettes/5"); + cy.get(".recipe-detail-panel__steps > li").should("have.length", 3); + + // Open the middle step's correction — pre-seeded with one ingredient + // and one utensil, so its popover renders two `.tech-step-correction-popover__row` + // `
  • `s of its own (plus the technique picker's own `
  • ` list once + // "Changer" is clicked) sitting *inside* the second step's own `
  • `. + cy.contains(".step-tech-step", "Faire revenir").click(); + cy.get(".tech-step-correction-popover__row").should("have.length", 2); + cy.contains(".tech-step-correction-popover__technique-change", "Changer").click(); + cy.get(".catalog-search-picker__list li").should("have.length.greaterThan", 0); + + // None of the popover's own nested `
  • `s join the step counter chain + // — `getComputedStyle(...).content` isn't reliable for reading a + // *resolved* counter() value across browsers (it can return the raw + // `counter(step) ". "` declaration instead), so this checks the actual + // `counter-increment` computed property instead: a real, directly + // testable signal for exactly what `> li` (recipes.scss) does and + // doesn't scope to. + cy.get(".tech-step-correction-popover__row").each(($row) => { + expect(window.getComputedStyle($row[0]).counterIncrement).to.equal("none"); + }); + cy.get(".catalog-search-picker__list li").each(($item) => { + expect(window.getComputedStyle($item[0]).counterIncrement).to.equal("none"); + }); + // The step `
  • `s themselves are still correctly in the chain. + cy.get(".recipe-detail-panel__steps > li").each(($step) => { + expect(window.getComputedStyle($step[0]).counterIncrement).to.equal("step 1"); + }); + }); + it("shows a not-found message for a selected id the API rejects", () => { cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] }); cy.intercept("GET", "**/recipes/999", { diff --git a/apps/web/src/features/recipes/recipes.scss b/apps/web/src/features/recipes/recipes.scss index de13290..8f6b260 100644 --- a/apps/web/src/features/recipes/recipes.scss +++ b/apps/web/src/features/recipes/recipes.scss @@ -630,7 +630,17 @@ gap: var(--space-md); counter-reset: step; - li { + // `> li`, not a bare descendant `li` — this used to match *any* `
  • ` + // nested anywhere inside a step, not just the step's own direct one. + // Harmless while `StepDescription`'s editable mode only ever added + // plain text/buttons, but `TechStepCorrectionPopover` renders its own + // `
  • `s a few levels down (each attached ingredient/utensil/ + // temperature row, `.tech-step-correction-popover__row`; the technique + // picker's `.catalog-search-picker__list`, up to ~74 entries) — + // every one of those was incrementing this *same* counter, throwing + // every step's own number off as soon as any correction popover with + // attached metadata (or its technique picker) was open. + > li { counter-increment: step; &::before { @@ -639,14 +649,20 @@ color: var(--color-primary); } - img { + // Same descendant-selector fix as `> li` above, for the same reason: + // both the step's own picture and its `StepDescription`-rendered `

    ` + // are direct children of this `

  • ` — `TechStepCorrectionPopover`'s + // own `

    `s (the selection line, hints, `.field-error`) sit deeper + // inside it and must not inherit "inline, no margin" meant only for + // the step's own paragraph. + > img { display: block; max-width: 100%; border-radius: var(--radius-base); margin-bottom: var(--space-xs); } - p { + > p { display: inline; margin: 0; }