batchCooking/apps/web/cypress/e2e/recipes.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

215 lines
6.6 KiB
TypeScript

import { Given, Then, When } from "@badeball/cypress-cucumber-preprocessor";
const vegetarien = { id: 1, key: "vegetarian" };
const oeufs = { id: 2, key: "eggs", kind: "ALLERGY" };
const ratatouille = {
id: 1,
name: "Ratatouille",
description: null,
picture: null,
authorId: 1,
visibility: "PERSONAL",
allergens: [],
diets: [vegetarien],
isFavorite: true,
};
const omelette = {
id: 2,
name: "Omelette",
description: null,
picture: null,
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: "CREMERIE_FROMAGE",
subcategory: "OEUFS",
allergens: [oeufs],
diets: [],
},
quantity: 3,
unit: "unité",
},
],
steps: [
{ id: 1, description: "Battre les œufs.", picture: null, order: 1 },
{ id: 2, description: "Cuire à la poêle.", picture: null, order: 2 },
],
};
Given("the disliked ingredients list is empty", () => {
cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] });
});
Given("the recipe catalog defaults to the favorites tab with {string}", () => {
cy.intercept("GET", /\/recipes\?/, (req) => {
expect(req.url).to.include("tab=favoris");
req.reply({ statusCode: 200, body: [ratatouille] });
}).as("listRecipes");
});
Given("the recipe catalog is empty", () => {
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] });
});
Given("the recipe catalog fails to load", () => {
cy.intercept("GET", /\/recipes\?/, { statusCode: 500, body: { code: 5000, message: "boom" } });
});
Given("the recipe catalog switches between tabs", () => {
cy.intercept("GET", /\/recipes\?/, (req) => {
const tab = new URL(req.url).searchParams.get("tab");
const body = tab === "perso" ? [omelette] : [ratatouille];
req.reply({ statusCode: 200, body });
}).as("listRecipes");
});
Given("the recipe catalog supports searching", () => {
cy.intercept("GET", /\/recipes\?/, (req) => {
const search = new URL(req.url).searchParams.get("search");
req.reply({ statusCode: 200, body: search ? [omelette] : [ratatouille, omelette] });
}).as("listRecipes");
});
Given("the recipe catalog contains {string} and {string}", () => {
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [ratatouille, omelette] });
});
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("recipe 999 does not exist", () => {
cy.intercept("GET", "**/recipes/999", {
statusCode: 404,
// ErrorCode.RECIPE_NOT_FOUND (packages/shared/src/errors/error-codes.ts)
// — RecipeDetailPanel only renders the "not found" message for this
// exact code, anything else falls into its generic error state.
body: { code: 4045, message: "not found" },
});
});
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");
});
Then("the recipe list request should have been made", () => {
cy.wait("@listRecipes");
});
Then("the recipe detail request should have been made", () => {
cy.wait("@getRecipe");
});
Then("the active tab should be {string}", (tab: string) => {
cy.get(".recipe-tabs__tab.active").should("contain.text", tab);
});
Then("the recipe {string} should be visible in the table", (name: string) => {
cy.contains(".recipe-table__name", name).should("be.visible");
});
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");
});
Then("the recipe {string} should show the diet badge {string}", (name: string, badge: string) => {
cy.contains(".recipe-table__name", name)
.parents("tr")
.find(".diet-badge")
.should("contain.text", badge);
});
Then("the tab {string} should be disabled", (text: string) => {
cy.contains(".recipe-tabs__tab", text).should("be.disabled");
});
When("I click the tab {string}", (text: string) => {
cy.contains(".recipe-tabs__tab", text).click();
});
When("I search for {string}", (text: string) => {
cy.get(".recipes-page__search").type(text);
});
When("I click the recipe {string} in the table", (text: string) => {
cy.contains(".recipe-table__name", text).click();
});
Then("the selected row should be {string}", (text: string) => {
cy.get("tr.selected .recipe-table__name").should("contain.text", text);
});
Then("the recipe detail panel heading should be {string}", (text: string) => {
cy.get(".recipe-detail-panel").contains("h2", text).should("be.visible");
});
Then("the recipe detail panel should show {string}", (text: string) => {
cy.get(".recipe-detail-panel").contains(text).should("be.visible");
});
Then("the recipe detail panel should say the recipe doesn't exist", () => {
cy.contains(".recipe-detail-panel", "Cette recette n'existe pas.").should("be.visible");
});
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\/?$/);
});
Then("the new-recipe button should link to {string}", (href: string) => {
cy.contains(".recipes-page__new-button", "Nouvelle recette").should("have.attr", "href", href);
});