batchCooking/apps/web/cypress/e2e/recipe-form.steps.ts
Nicolas 20d52aee2d feat(web): migre les specs Cypress vers Cucumber/Gherkin
Les tests e2e (apps/web/cypress/e2e/) étaient de simples specs Cypress
(.cy.ts), sans lien avec Cucumber alors qu'apps/api utilise déjà
Gherkin pour ses propres tests BDD. Intègre
@badeball/cypress-cucumber-preprocessor pour écrire les scénarios
utilisateurs en Gherkin des deux côtés, même vocabulaire.

- cypress.config.ts : specPattern sur *.feature, wiring du
  préprocesseur (esbuild bundler + plugin cucumber)
- Les 11 fichiers .cy.ts sont remplacés par des paires .feature/.steps.ts
  (co-localisées, même nom) — conversion complète, comportement
  équivalent (mêmes intercepts, mêmes assertions)
- cypress/support/step_definitions/common.steps.ts : steps partagés
  entre features (connexion, navigation, assertions génériques de
  texte/URL/champ) — globaux à toute la suite, réutilisables tels quels
- cypress/support/profile.ts : profil du compte "connecté" courant,
  assemblé au fil de plusieurs Given avant le premier visit/When
- README : nouvelle section "Cucumber (apps/web)" (miroir de la section
  existante pour apps/api), mise à jour des références aux anciens noms
  de fichiers .cy.ts (déjà obsolètes avant ce changement)

Vérification : impossible d'exécuter Cypress dans cet environnement
(crash Electron/GPU au lancement, limitation déjà documentée dans le
README — reproductible sur main, indépendante de ce changement). À la
place :
- les 447 steps Gherkin des 11 .feature ont été vérifiés
  programmatiquement contre les 165 patterns de step enregistrés : 0
  non résolu, 0 ambigu
- les 11 .feature parsent correctement avec le parser Gherkin officiel
  (57 scénarios au total)
- tous les .steps.ts passent `biome check` (syntaxe + style) sans erreur
- CYPRESS_INSTALL_BINARY déjà géré (voir PR précédente) — le binaire est
  bien présent localement (`cypress verify` OK), donc le blocage est
  spécifiquement le sandbox GPU de cet environnement, pas l'installation

La vraie exécution reste à vérifier via le job `e2e` de la CI GitHub
Actions sur cette PR — c'est le chemin déjà documenté dans le README
pour cet environnement précis.
2026-08-19 09:42:29 +02:00

159 lines
4.9 KiB
TypeScript

import { type DataTable, Given, Then, When } from "@badeball/cypress-cucumber-preprocessor";
const tomato = {
id: 1,
key: "tomato",
icon: "VEGETABLE",
category: "PRODUITS_FRAIS",
subcategory: "LEGUMES",
allergens: [],
diets: [{ id: 2, key: "vegetarian" }],
};
const egg = {
id: 2,
key: "egg",
icon: "EGG",
category: "CREMERIE_FROMAGE",
subcategory: "OEUFS",
allergens: [{ id: 1, key: "eggs", kind: "ALLERGY" }],
diets: [],
};
const carrot = {
id: 3,
key: "carrot",
icon: "VEGETABLE",
category: "PRODUITS_FRAIS",
subcategory: "LEGUMES",
allergens: [],
diets: [{ id: 2, key: "vegetarian" }],
};
const diets = [
{ id: 1, key: "omnivore" },
{ id: 2, key: "vegetarian" },
];
Given("the ingredient/diet catalog is available", () => {
cy.intercept("GET", "**/reference/ingredients", { statusCode: 200, body: [tomato, egg, carrot] });
cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: diets });
});
Given("creating the recipe will succeed and return id {int}", (id: number) => {
cy.intercept("POST", "**/recipes", { statusCode: 201, body: { id } }).as("createRecipe");
});
When("I visit the new recipe form without a secure random UUID", () => {
cy.visit("/recettes/nouvelle", {
onBeforeLoad(win) {
Object.defineProperty(win.crypto, "randomUUID", {
value: undefined,
configurable: true,
});
},
});
});
When("I search the ingredient picker for {string}", (text: string) => {
cy.get("input[placeholder='Rechercher un ingrédient…']").type(text);
});
When("I select the ingredient {string} from the picker", (name: string) => {
cy.contains(".ingredient-picker__card", name).click();
});
Then("the ingredient {string} should no longer be in the picker", (name: string) => {
cy.contains(".ingredient-picker__card", name).should("not.exist");
});
Then("the ingredient {string} should be visible in the picker", (name: string) => {
cy.contains(".ingredient-picker__card", name).should("be.visible");
});
Then("the recipe should include the ingredient {string}", (name: string) => {
cy.contains(".ingredient-row__name", name).should("be.visible");
});
Then("there should be {int} ingredient rows", (count: number) => {
cy.get(".ingredient-row").should("have.length", count);
});
When("I remove the ingredient {string} from the recipe", (name: string) => {
cy.contains(".ingredient-row", name).find("button[title='Retirer cet ingrédient']").click();
});
When(
"I fill in the ingredient's quantity with {string} and unit {string}",
(quantity: string, unit: string) => {
cy.get(".ingredient-row .ingredient-row__quantity").type(quantity);
cy.get(".ingredient-row .ingredient-row__unit").type(unit);
},
);
Then("the ingredient's quantity should be {string}", (quantity: string) => {
cy.get(".ingredient-row .ingredient-row__quantity").should("have.value", quantity);
});
When(
"I fill in the last ingredient's quantity with {string} and unit {string}",
(quantity: string, unit: string) => {
cy.get(".ingredient-row .ingredient-row__quantity").last().type(quantity);
cy.get(".ingredient-row .ingredient-row__unit").last().type(unit);
},
);
When("I add a step", () => {
cy.contains("button", "Ajouter une étape").click();
});
When("I fill in the step description with {string}", (text: string) => {
cy.get(".step-list-editor__item textarea").type(text);
});
Then("there should be {int} step editor items", (count: number) => {
cy.get(".step-list-editor__item").should("have.length", count);
});
Then(
"the recipe creation request should have included name {string} and ingredient {int} with quantity {int} and unit {string}",
(name: string, ingredientId: number, quantity: number, unit: string) => {
cy.wait("@createRecipe")
.its("request.body")
.should("deep.include", {
name,
ingredients: [{ ingredientId, quantity, unit }],
});
},
);
Given("recipe 7 exists with an egg omelette", () => {
const existingRecipe = {
id: 7,
name: "Omelette",
description: null,
picture: null,
authorId: 1,
visibility: "PERSONAL",
allergens: [{ id: 1, key: "eggs", kind: "ALLERGY" }],
diets: [],
isFavorite: false,
ingredients: [{ ingredient: egg, quantity: 3, unit: "unité" }],
steps: [{ id: 1, description: "Battre les œufs.", picture: null, order: 0 }],
};
cy.intercept("GET", "**/recipes/7", { statusCode: 200, body: existingRecipe });
});
Given("updating recipe 7 will succeed", () => {
cy.intercept("PATCH", "**/recipes/7", { statusCode: 200, body: { id: 7 } }).as("updateRecipe");
});
Then(
"the recipe update request should have included these ingredients:",
(dataTable: DataTable) => {
const expected = dataTable.hashes().map((row) => ({
ingredientId: Number(row.ingredientId),
quantity: Number(row.quantity),
unit: row.unit,
}));
cy.wait("@updateRecipe").its("request.body.ingredients").should("deep.equal", expected);
},
);