StepDescription faisait `useState(techSteps)` puis `liveTechSteps.flatMap(...)` : quand la prop `techSteps` arrivait `undefined`, `.flatMap` levait `TypeError: Cannot read properties of undefined (reading 'flatMap')`, remontée par Cypress comme une erreur applicative non capturée. Observé de façon intermittente sur le parcours « modifier une recette » : après enregistrement, `RecipeFormPage` navigue vers `/recettes/:id`, dont le détail se rendait parfois avant que `techSteps` ne soit peuplé. - `StepDescription` : `techSteps` devient optionnel et `liveTechSteps` retombe sur `[]` (dans le `useState` et le `useEffect` de resync), pour que tous les consommateurs (`.flatMap`, `splitDescriptionSegments`, `findTechStepForTemperature`, lookup `activeStepTechStep`) aient toujours un vrai tableau — même posture « tolérer la donnée manquante, ne pas planter » que le reste du composant. - `recipe-form.ts` : le mock de la recette 7 envoyait une étape sans `techSteps`, contrairement au vrai `StepView` et à tous les autres mocks de la suite — corrigé en `techSteps: []`. - `recipes.cy.ts` : nouveau test de non-régression déterministe — un payload d'étape sans `techSteps` doit rendre la description en texte brut. Vérifié qu'il échoue bien (`flatMap` undefined) sans le correctif du composant. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
134 lines
4.4 KiB
TypeScript
134 lines
4.4 KiB
TypeScript
import { type DataTable, Given, Then, When } from "@badeball/cypress-cucumber-preprocessor";
|
|
|
|
const tomato = {
|
|
id: 1,
|
|
key: "tomato",
|
|
icon: "VEGETABLE",
|
|
category: "freshProduce",
|
|
subcategory: "vegetables",
|
|
allergens: [],
|
|
diets: [{ id: 2, key: "vegetarian" }],
|
|
};
|
|
const egg = {
|
|
id: 2,
|
|
key: "egg",
|
|
icon: "EGG",
|
|
category: "dairyAndCheese",
|
|
subcategory: "eggs",
|
|
allergens: [{ id: 1, key: "eggs", kind: "ALLERGY" }],
|
|
diets: [],
|
|
};
|
|
const carrot = {
|
|
id: 3,
|
|
key: "carrot",
|
|
icon: "VEGETABLE",
|
|
category: "freshProduce",
|
|
subcategory: "vegetables",
|
|
allergens: [],
|
|
diets: [{ id: 2, key: "vegetarian" }],
|
|
};
|
|
|
|
const diets = [
|
|
{ id: 1, key: "omnivore" },
|
|
{ id: 2, key: "vegetarian" },
|
|
];
|
|
|
|
const pieceUnit = { id: 1, key: "piece", type: "COUNT", toBaseFactor: 1 };
|
|
const gramUnit = { id: 2, key: "gram", type: "MASS", toBaseFactor: 1 };
|
|
|
|
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 });
|
|
cy.intercept("GET", "**/reference/units", { statusCode: 200, body: [pieceUnit, gramUnit] });
|
|
});
|
|
|
|
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,
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
Then(
|
|
"the recipe creation request should have included name {string}, portions {int}, and ingredient {int} with quantity {int} and unitId {int}",
|
|
(name: string, portions: number, ingredientId: number, quantity: number, unitId: number) => {
|
|
cy.wait("@createRecipe")
|
|
.its("request.body")
|
|
.should("deep.include", {
|
|
name,
|
|
portions,
|
|
ingredients: [{ ingredientId, quantity, unitId }],
|
|
});
|
|
},
|
|
);
|
|
|
|
Then("the picker should offer to add {string} as a placeholder", (name: string) => {
|
|
cy.get(".ingredient-picker__add-placeholder").should("contain.text", name);
|
|
});
|
|
|
|
When("I add {string} as a placeholder ingredient", () => {
|
|
// The search field already holds the query from the previous step, so the
|
|
// "add « … »" button carries the right name.
|
|
cy.get(".ingredient-picker__add-placeholder").click();
|
|
});
|
|
|
|
Then("the recipe should include the placeholder ingredient {string}", (name: string) => {
|
|
cy.contains(".ingredient-row__name", name)
|
|
.find(".ingredient-row__placeholder-badge")
|
|
.should("be.visible");
|
|
});
|
|
|
|
Then(
|
|
"the recipe creation request should have included a placeholder ingredient {string} with quantity {int} and unitId {int}",
|
|
(placeholderName: string, quantity: number, unitId: number) => {
|
|
cy.wait("@createRecipe")
|
|
.its("request.body.ingredients")
|
|
.should("deep.equal", [{ placeholderName, quantity, unitId }]);
|
|
},
|
|
);
|
|
|
|
Given("recipe 7 exists with an egg omelette", () => {
|
|
const existingRecipe = {
|
|
id: 7,
|
|
name: "Omelette",
|
|
description: null,
|
|
picture: null,
|
|
portions: 2,
|
|
authorId: 1,
|
|
visibility: "PERSONAL",
|
|
allergens: [{ id: 1, key: "eggs", kind: "ALLERGY" }],
|
|
diets: [],
|
|
isFavorite: false,
|
|
ingredients: [{ ingredient: egg, quantity: 3, unit: pieceUnit }],
|
|
// `techSteps: []` matches the real `StepView` shape (packages/shared) —
|
|
// every other recipe-step mock in the suite sends it, and its absence
|
|
// here let `/recettes/7` (rendered after this recipe saves) hit
|
|
// `StepDescription` with `techSteps === undefined`.
|
|
steps: [{ id: 1, description: "Battre les œufs.", picture: null, order: 0, techSteps: [] }],
|
|
};
|
|
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),
|
|
unitId: Number(row.unitId),
|
|
}));
|
|
cy.wait("@updateRecipe").its("request.body.ingredients").should("deep.equal", expected);
|
|
},
|
|
);
|