From 37cf6d4dcd90636923595a8b5d734ca3f5d98734 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Wed, 19 Aug 2026 13:47:56 +0200 Subject: [PATCH] fix(web): fix cross-feature step discovery and slash-alternation bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La CI de la refonte précédente (commit aaace15) a échoué : la découverte par défaut du préprocesseur ne charge, pour un fichier `foo.feature`, QUE `foo.ts` (co-localisé, même basename) et `cypress/support/step_definitions/**` — pas les autres `.ts` du dossier `cypress/e2e/`. onboarding.feature référençait donc des steps qui ne vivaient que dans preferences.ts, household-settings.ts et auth.ts, introuvables lors de son propre run. Déplace les steps réellement partagés entre plusieurs .feature vers cypress/support/step_definitions/ (chargé pour toutes les features) : - reference-data.steps.ts : mocks des listes de référence régimes/ allergies (options ou vides) — partagé entre auth.feature et onboarding.feature - household-mutations.steps.ts : création/adhésion à un foyer et leurs assertions — partagé entre household-settings.feature et onboarding.feature - profile-mutations.steps.ts : sélection du régime, mise à jour des allergies et leur assertion — partagé entre preferences.feature et onboarding.feature Les définitions d'origine sont retirées de auth.ts/household- settings.ts/onboarding.ts/preferences.ts pour éviter un step "Ambiguous" (chargé deux fois pour la feature qui les définissait déjà elle-même). Corrige aussi un second bug distinct révélé par la même CI : "the ingredient/diet catalog is available" (recipe-form.feature) contient un "/" non échappé — en syntaxe Cucumber Expression, "/" hors d'un paramètre {..} signifie une alternative de texte ("ingredient" OU "diet catalog is available"), jamais le caractère littéral. Le texte du .feature ne pouvait donc jamais matcher. Renommé sans "/" : "the ingredient and diet catalog is available". Le script de vérification statique utilisé pour valider aaace15 avant push donnait une fausse confiance : il regroupait tous les steps de tous les fichiers comme disponibles globalement pour chaque feature, sans respecter ce scoping réel. Réécrit pour ne charger, par feature, que son fichier co-localisé + step_definitions/ — et pour détecter les patterns contenant un "/" non échappé. Résultat : toujours 246 steps, 0 non résolu, 0 ambigu, 0 pattern à slash non échappé, cette fois avec un modèle de résolution fidèle au comportement réel du préprocesseur. --- apps/web/cypress/e2e/auth.ts | 4 -- apps/web/cypress/e2e/household-settings.ts | 48 +------------- apps/web/cypress/e2e/onboarding.ts | 24 ------- apps/web/cypress/e2e/preferences.ts | 18 +----- apps/web/cypress/e2e/recipe-form.feature | 2 +- apps/web/cypress/e2e/recipe-form.ts | 2 +- .../household-mutations.steps.ts | 63 +++++++++++++++++++ .../profile-mutations.steps.ts | 21 +++++++ .../step_definitions/reference-data.steps.ts | 33 ++++++++++ 9 files changed, 125 insertions(+), 90 deletions(-) create mode 100644 apps/web/cypress/support/step_definitions/household-mutations.steps.ts create mode 100644 apps/web/cypress/support/step_definitions/profile-mutations.steps.ts create mode 100644 apps/web/cypress/support/step_definitions/reference-data.steps.ts diff --git a/apps/web/cypress/e2e/auth.ts b/apps/web/cypress/e2e/auth.ts index 9aaacc4..d70d1d3 100644 --- a/apps/web/cypress/e2e/auth.ts +++ b/apps/web/cypress/e2e/auth.ts @@ -26,10 +26,6 @@ Given("the signup request will fail because the email is already used", () => { }).as("signup"); }); -Given("the diets reference list is empty", () => { - cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [] }); -}); - When("I sign up with:", (dataTable: DataTable) => { const { firstName, lastName, email, password } = dataTable.rowsHash(); cy.visit("/signup"); diff --git a/apps/web/cypress/e2e/household-settings.ts b/apps/web/cypress/e2e/household-settings.ts index 542f192..6618a1d 100644 --- a/apps/web/cypress/e2e/household-settings.ts +++ b/apps/web/cypress/e2e/household-settings.ts @@ -15,40 +15,9 @@ Given("the household request returns the two-member household", () => { cy.intercept("GET", "**/house/current", { statusCode: 200, body: houseWithTwoMembers }); }); -// The page reloads `GET /house/current` right after each mutation below -// succeeds — these intercepts need to answer differently before/after that -// follow-up GET, hence a shared mutable flag rather than a single static -// `cy.intercept` (a later static one would just win for every request, -// including the initial page load). - -Given("creating a household will succeed", () => { - const createdHouse = { - id: 1, - name: "Chez Alice", - adminId: 1, - inviteCode: "ABCD2345", - members: [{ id: 1, firstName: "Alice", lastName: "Martin" }], - }; - let created = false; - cy.intercept("GET", "**/house/current", (req) => { - req.reply({ statusCode: 200, body: created ? createdHouse : null }); - }); - cy.intercept("POST", "**/house", (req) => { - created = true; - req.reply({ statusCode: 201, body: createdHouse }); - }).as("createHouse"); -}); - -Given("joining a household will succeed", () => { - let joined = false; - cy.intercept("GET", "**/house/current", (req) => { - req.reply({ statusCode: 200, body: joined ? houseWithTwoMembers : null }); - }); - cy.intercept("POST", "**/house/join", (req) => { - joined = true; - req.reply({ statusCode: 200, body: houseWithTwoMembers }); - }).as("joinHouse"); -}); +// Creating/joining a household, and asserting on those two requests, is +// shared with onboarding.feature's household step — see +// cypress/support/step_definitions/household-mutations.steps.ts. Given("renaming the household will succeed", () => { cy.intercept("PATCH", "**/house/current", { @@ -88,17 +57,6 @@ When("I click {string} for the member {string}", (action: string, member: string cy.contains("li", member).contains("button", action).click(); }); -Then("the household creation request should have been made with name {string}", (name: string) => { - cy.wait("@createHouse").its("request.body").should("deep.equal", { name }); -}); - -Then( - "the household join request should have been made with invite code {string}", - (code: string) => { - cy.wait("@joinHouse").its("request.body").should("deep.equal", { inviteCode: code }); - }, -); - Then("the household rename request should have been made with name {string}", (name: string) => { cy.wait("@renameHouse").its("request.body").should("deep.equal", { name }); }); diff --git a/apps/web/cypress/e2e/onboarding.ts b/apps/web/cypress/e2e/onboarding.ts index ff922ba..7fba4e7 100644 --- a/apps/web/cypress/e2e/onboarding.ts +++ b/apps/web/cypress/e2e/onboarding.ts @@ -10,30 +10,6 @@ const signupResponse = { dietId: null, }; -Given("the diets reference list has options", () => { - cy.intercept("GET", "**/reference/diets", { - statusCode: 200, - body: [ - { id: 1, key: "omnivore" }, - { id: 2, key: "vegetarian" }, - ], - }); -}); - -Given("the allergies reference list has options", () => { - cy.intercept("GET", "**/reference/allergies", { - statusCode: 200, - body: [ - { id: 1, key: "peanuts", kind: "ALLERGY" }, - { id: 2, key: "gluten", kind: "INTOLERANCE" }, - ], - }); -}); - -Given("the allergies reference list is empty", () => { - cy.intercept("GET", "**/reference/allergies", { statusCode: 200, body: [] }); -}); - // Signs up and lands on the wizard's first step (regime) — shared setup for // every scenario in this feature, mirroring `signupAndReachOnboarding` from // the pre-conversion spec. diff --git a/apps/web/cypress/e2e/preferences.ts b/apps/web/cypress/e2e/preferences.ts index 30e5809..45bd632 100644 --- a/apps/web/cypress/e2e/preferences.ts +++ b/apps/web/cypress/e2e/preferences.ts @@ -26,21 +26,9 @@ Given("the dietary preferences reference data is ready", () => { cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] }); }); -Given("selecting the diet will succeed", () => { - cy.intercept("PATCH", "**/profile/diet", { statusCode: 200, body: { dietId: 1 } }).as( - "updateDiet", - ); -}); - -Then("the diet update request should have been made with diet id {int}", (dietId: number) => { - cy.wait("@updateDiet").its("request.body").should("deep.equal", { dietId }); -}); - -Given("updating allergies will succeed", () => { - cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [2, 1] }).as( - "updateAllergies", - ); -}); +// Selecting the diet, updating allergies, and asserting on the diet update +// request are shared with onboarding.feature's regime/allergies steps — see +// cypress/support/step_definitions/profile-mutations.steps.ts. Then( "the allergies update request should have been made with allergy ids {int} and {int}", diff --git a/apps/web/cypress/e2e/recipe-form.feature b/apps/web/cypress/e2e/recipe-form.feature index 33cd1fc..604cebe 100644 --- a/apps/web/cypress/e2e/recipe-form.feature +++ b/apps/web/cypress/e2e/recipe-form.feature @@ -6,7 +6,7 @@ Feature: Recipe form — associating ingredients Background: Given I am signed in as "Alice" "Martin" And my household id is 1 - And the ingredient/diet catalog is available + And the ingredient and diet catalog is available Scenario: Adds an ingredient from the picker, fills its quantity/unit, and creates the recipe Given creating the recipe will succeed and return id 42 diff --git a/apps/web/cypress/e2e/recipe-form.ts b/apps/web/cypress/e2e/recipe-form.ts index 4a8ec69..962f16c 100644 --- a/apps/web/cypress/e2e/recipe-form.ts +++ b/apps/web/cypress/e2e/recipe-form.ts @@ -33,7 +33,7 @@ const diets = [ { id: 2, key: "vegetarian" }, ]; -Given("the ingredient/diet catalog is available", () => { +Given("the ingredient and diet catalog is available", () => { cy.intercept("GET", "**/reference/ingredients", { statusCode: 200, body: [tomato, egg, carrot] }); cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: diets }); }); diff --git a/apps/web/cypress/support/step_definitions/household-mutations.steps.ts b/apps/web/cypress/support/step_definitions/household-mutations.steps.ts new file mode 100644 index 0000000..82c6aa4 --- /dev/null +++ b/apps/web/cypress/support/step_definitions/household-mutations.steps.ts @@ -0,0 +1,63 @@ +import { Given, Then } from "@badeball/cypress-cucumber-preprocessor"; + +// Shared across household-settings.feature (dedicated create/join scenarios) +// and onboarding.feature (household step of the wizard) — both need to mock +// the create/join mutations and their up-to-date `GET /house/current` +// follow-up the same way. + +const houseWithTwoMembers = { + id: 1, + name: "Chez Alice", + adminId: 1, + inviteCode: "ABCD2345", + members: [ + { id: 1, firstName: "Alice", lastName: "Martin" }, + { id: 2, firstName: "Bob", lastName: "Dupont" }, + ], +}; + +// The page reloads `GET /house/current` right after each mutation below +// succeeds — these intercepts need to answer differently before/after that +// follow-up GET, hence a shared mutable flag rather than a single static +// `cy.intercept` (a later static one would just win for every request, +// including the initial page load). + +Given("creating a household will succeed", () => { + const createdHouse = { + id: 1, + name: "Chez Alice", + adminId: 1, + inviteCode: "ABCD2345", + members: [{ id: 1, firstName: "Alice", lastName: "Martin" }], + }; + let created = false; + cy.intercept("GET", "**/house/current", (req) => { + req.reply({ statusCode: 200, body: created ? createdHouse : null }); + }); + cy.intercept("POST", "**/house", (req) => { + created = true; + req.reply({ statusCode: 201, body: createdHouse }); + }).as("createHouse"); +}); + +Given("joining a household will succeed", () => { + let joined = false; + cy.intercept("GET", "**/house/current", (req) => { + req.reply({ statusCode: 200, body: joined ? houseWithTwoMembers : null }); + }); + cy.intercept("POST", "**/house/join", (req) => { + joined = true; + req.reply({ statusCode: 200, body: houseWithTwoMembers }); + }).as("joinHouse"); +}); + +Then("the household creation request should have been made with name {string}", (name: string) => { + cy.wait("@createHouse").its("request.body").should("deep.equal", { name }); +}); + +Then( + "the household join request should have been made with invite code {string}", + (code: string) => { + cy.wait("@joinHouse").its("request.body").should("deep.equal", { inviteCode: code }); + }, +); diff --git a/apps/web/cypress/support/step_definitions/profile-mutations.steps.ts b/apps/web/cypress/support/step_definitions/profile-mutations.steps.ts new file mode 100644 index 0000000..3774aaf --- /dev/null +++ b/apps/web/cypress/support/step_definitions/profile-mutations.steps.ts @@ -0,0 +1,21 @@ +import { Given, Then } from "@badeball/cypress-cucumber-preprocessor"; + +// Shared across preferences.feature (dedicated autosave scenarios) and +// onboarding.feature (regime/allergies steps of the wizard) — both need the +// same diet/allergies PATCH mocks and their request-body assertions. + +Given("selecting the diet will succeed", () => { + cy.intercept("PATCH", "**/profile/diet", { statusCode: 200, body: { dietId: 1 } }).as( + "updateDiet", + ); +}); + +Then("the diet update request should have been made with diet id {int}", (dietId: number) => { + cy.wait("@updateDiet").its("request.body").should("deep.equal", { dietId }); +}); + +Given("updating allergies will succeed", () => { + cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [2, 1] }).as( + "updateAllergies", + ); +}); diff --git a/apps/web/cypress/support/step_definitions/reference-data.steps.ts b/apps/web/cypress/support/step_definitions/reference-data.steps.ts new file mode 100644 index 0000000..732da0e --- /dev/null +++ b/apps/web/cypress/support/step_definitions/reference-data.steps.ts @@ -0,0 +1,33 @@ +import { Given } from "@badeball/cypress-cucumber-preprocessor"; + +// Shared across auth.feature (signup) and onboarding.feature (wizard) — both +// need the diets/allergies reference lists mocked before reaching a step +// that reads them, in either their "has options" or "is empty" shape. + +Given("the diets reference list has options", () => { + cy.intercept("GET", "**/reference/diets", { + statusCode: 200, + body: [ + { id: 1, key: "omnivore" }, + { id: 2, key: "vegetarian" }, + ], + }); +}); + +Given("the diets reference list is empty", () => { + cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [] }); +}); + +Given("the allergies reference list has options", () => { + cy.intercept("GET", "**/reference/allergies", { + statusCode: 200, + body: [ + { id: 1, key: "peanuts", kind: "ALLERGY" }, + { id: 2, key: "gluten", kind: "INTOLERANCE" }, + ], + }); +}); + +Given("the allergies reference list is empty", () => { + cy.intercept("GET", "**/reference/allergies", { statusCode: 200, body: [] }); +});