Convertit les parcours utilisateur (goal-driven, "en tant que X je peux Y") en scénarios Gherkin, en réutilisant l'infra Cucumber déjà validée par le smoke test (PR #24). Retire le smoke test jetable maintenant superflu. 8 fichiers .feature ajoutés, chacun avec son fichier de step definitions au même basename (convention de découverte du préprocesseur — voir login-smoke.ts) : - auth.feature : inscription (succès, erreur validation, email déjà pris), connexion (succès, identifiants invalides), déconnexion - onboarding.feature : les 3 scénarios déjà couverts (wizard complet, étapes sautées, rejoindre un foyer pendant l'onboarding) — dépend de household-settings.ts et preferences.ts pour ses steps de création/rejoint de foyer et de sélection de régime/allergies - household-settings.feature : créer un foyer, rejoindre par code d'invitation, renommer (autosave), retirer un membre, supprimer le foyer, quitter le foyer - account.feature : suppression de compte (mauvais mot de passe, succès, annulation) - recipe-form.feature : les 4 scénarios déjà couverts inchangés (ajout d'ingrédient + création, régression crypto.randomUUID, exclusion/ réinclusion d'ingrédient, préchargement + édition d'une recette existante) - recipes.feature : bascule favori, suppression d'une recette - preferences.feature : autosave du régime, autosave des allergies - user-preferences.feature : changement de thème (autosave) En contrepartie, les anciens .cy.ts perdent uniquement les it() migrés vers Gherkin — les scénarios de layout/affichage pur (catalogue de recettes, tabs, recherche, panneau de détail, sidebar, planning grid, etc.) restent en Cypress classique, conformément au découpage "parcours utilisateur (Cucumber) vs layout (Cypress pur)" déjà en place pour les component tests. auth.cy.ts, onboarding.cy.ts et recipe-form.cy.ts sont supprimés : 100% de leur contenu a migré. Les commentaires "voir auth.cy.ts pour la justification" désormais obsolètes (fichier supprimé) sont remplacés par une explication autonome du mock cy.intercept. Vérifié statiquement : les 246 steps Gherkin des 8 .feature résolvent chacun vers exactement une définition (0 non résolu, 0 ambigu) et `pnpm exec biome check` est propre sur tout cypress/. Reste à confirmer en CI que les scénarios passent réellement (pas seulement qu'ils se résolvent).
159 lines
4.9 KiB
TypeScript
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);
|
|
},
|
|
);
|