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.
215 lines
6.6 KiB
TypeScript
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);
|
|
});
|