- onboarding.cy.ts / preferences.cy.ts / recipes.cy.ts mockaient encore
GET /reference/diets|allergies avec l'ancienne forme {id, name}. Depuis
les deux derniers commits l'API renvoie {id, key} (uid anglais) et le
composant résout le libellé via i18n (t(`catalog.diets.${key}`)) — avec
key manquant, ça affichait littéralement "catalog.diets.undefined" au
lieu de "Végétarien"/"Omnivore"/etc., faisant échouer cy.select()/
cy.contains() dans ces 3 specs. Corrigé pour mocker {key: "vegetarian"},
{key: "peanuts"}, etc.
- recipes.cy.ts : le test "shows a not-found message" utilisait le
mauvais code d'erreur (4041 au lieu de ErrorCode.RECIPE_NOT_FOUND =
4045), donc RecipeDetailPanel tombait dans son état d'erreur générique
au lieu du message "Cette recette n'existe pas." — bug dans mon propre
test, sans rapport avec le refactor.
- pnpm lint (biome) : les fichiers touchés par le refactor précédent
avaient quelques soucis de formatage/tri d'imports (des sed multi-
fichiers, pas d'édition via l'outil habituel) — corrigés par
`biome check --write`.
Vérifié : ces 3 specs + recipe-form.cy.ts passent maintenant dans le job
CI GitHub Actions (Linux, Cypress s'y exécute réellement — contrairement
à cet environnement Windows sandboxé, voir les commits précédents) ; 102
tests Mocha + 32 scénarios Cucumber toujours au vert en local.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
178 lines
6.2 KiB
TypeScript
178 lines
6.2 KiB
TypeScript
// Mocks the API via cy.intercept — see auth.cy.ts for the rationale (no
|
|
// live backend in this CI job; apps/api's own Mocha/Cucumber suites cover
|
|
// real API behavior against a real database).
|
|
|
|
const authenticatedProfile = {
|
|
id: 1,
|
|
firstName: "Alice",
|
|
lastName: "Martin",
|
|
email: "alice@example.com",
|
|
tokenVersion: 0,
|
|
houseId: 1,
|
|
dietId: null,
|
|
};
|
|
|
|
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" },
|
|
];
|
|
|
|
function interceptCatalog() {
|
|
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile });
|
|
cy.intercept("GET", "**/reference/ingredients", {
|
|
statusCode: 200,
|
|
body: [tomato, egg, carrot],
|
|
});
|
|
cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: diets });
|
|
}
|
|
|
|
describe("Recipe form — associating ingredients", () => {
|
|
beforeEach(() => {
|
|
interceptCatalog();
|
|
});
|
|
|
|
it("adds an ingredient from the picker, fills its quantity/unit, and creates the recipe", () => {
|
|
cy.intercept("POST", "**/recipes", { statusCode: 201, body: { id: 42 } }).as("createRecipe");
|
|
|
|
cy.visit("/recettes/nouvelle");
|
|
|
|
cy.get("#recipe-name").type("Salade de tomates");
|
|
|
|
cy.get("input[placeholder='Rechercher un ingrédient…']").type("tomat");
|
|
cy.contains(".ingredient-picker__card", "Tomate").click();
|
|
|
|
// The card disappears from the picker once selected (excludeIds), and
|
|
// a row for it appears in the recipe's own ingredient list.
|
|
cy.contains(".ingredient-picker__card", "Tomate").should("not.exist");
|
|
cy.contains(".ingredient-row__name", "Tomate").should("be.visible");
|
|
|
|
cy.get(".ingredient-row .ingredient-row__quantity").type("3");
|
|
cy.get(".ingredient-row .ingredient-row__unit").type("unité");
|
|
|
|
cy.contains("button", "Ajouter une étape").click();
|
|
cy.get(".step-list-editor__item textarea").type("Couper les tomates.");
|
|
|
|
cy.contains("button", "Enregistrer").should("not.be.disabled").click();
|
|
|
|
cy.wait("@createRecipe")
|
|
.its("request.body")
|
|
.should("deep.include", {
|
|
name: "Salade de tomates",
|
|
ingredients: [{ ingredientId: 1, quantity: 3, unit: "unité" }],
|
|
});
|
|
cy.url().should("include", "/recettes/42");
|
|
});
|
|
|
|
// Regression test for the exact bug reported: `crypto.randomUUID()` (used
|
|
// to mint each ingredient/step draft's client-only React key) throws
|
|
// outside a secure context — https, or literally the hostname
|
|
// `localhost` — so a LAN IP during on-device testing or a Capacitor
|
|
// WebView's `capacitor://` origin hit a black screen with "TypeError:
|
|
// crypto.randomUUID is not a function" the instant an ingredient was
|
|
// added. Cypress's own origin is secure, so this forces the same failure
|
|
// by deleting `crypto.randomUUID` before the app boots — see
|
|
// `apps/web/src/lib/client-key.ts`, which replaced it.
|
|
it("still works when crypto.randomUUID is unavailable (insecure-context regression)", () => {
|
|
cy.visit("/recettes/nouvelle", {
|
|
onBeforeLoad(win) {
|
|
Object.defineProperty(win.crypto, "randomUUID", {
|
|
value: undefined,
|
|
configurable: true,
|
|
});
|
|
},
|
|
});
|
|
|
|
cy.get("#recipe-name").type("Recette hors contexte sécurisé");
|
|
|
|
cy.contains(".ingredient-picker__card", "Tomate").click();
|
|
cy.contains(".ingredient-picker__card", "Œuf").click();
|
|
|
|
// Both rows rendered with distinct identities — no crash, no React
|
|
// "same key" warning silently collapsing one of them.
|
|
cy.get(".ingredient-row").should("have.length", 2);
|
|
cy.contains(".ingredient-row__name", "Tomate").should("be.visible");
|
|
cy.contains(".ingredient-row__name", "Œuf").should("be.visible");
|
|
|
|
cy.contains("button", "Ajouter une étape").click();
|
|
cy.contains("button", "Ajouter une étape").click();
|
|
cy.get(".step-list-editor__item").should("have.length", 2);
|
|
});
|
|
|
|
it("excludes an already-selected ingredient from the picker, and removing it brings it back", () => {
|
|
cy.visit("/recettes/nouvelle");
|
|
|
|
cy.contains(".ingredient-picker__card", "Carotte").click();
|
|
cy.contains(".ingredient-picker__card", "Carotte").should("not.exist");
|
|
|
|
cy.contains(".ingredient-row", "Carotte")
|
|
.find("button[title='Retirer cet ingrédient']")
|
|
.click();
|
|
cy.contains(".ingredient-picker__card", "Carotte").should("be.visible");
|
|
cy.get(".ingredient-row").should("have.length", 0);
|
|
});
|
|
|
|
it("preloads an existing recipe's ingredients when editing, and lets you add another", () => {
|
|
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 });
|
|
cy.intercept("PATCH", "**/recipes/7", { statusCode: 200, body: { id: 7 } }).as("updateRecipe");
|
|
|
|
cy.visit("/recettes/7/modifier");
|
|
|
|
cy.contains(".ingredient-row__name", "Œuf").should("be.visible");
|
|
cy.get(".ingredient-row .ingredient-row__quantity").should("have.value", "3");
|
|
|
|
cy.contains(".ingredient-picker__card", "Tomate").click();
|
|
cy.get(".ingredient-row").should("have.length", 2);
|
|
cy.get(".ingredient-row .ingredient-row__quantity").last().type("1");
|
|
cy.get(".ingredient-row .ingredient-row__unit").last().type("unité");
|
|
|
|
cy.contains("button", "Enregistrer").click();
|
|
|
|
cy.wait("@updateRecipe")
|
|
.its("request.body.ingredients")
|
|
.should("deep.equal", [
|
|
{ ingredientId: 2, quantity: 3, unit: "unité" },
|
|
{ ingredientId: 1, quantity: 1, unit: "unité" },
|
|
]);
|
|
});
|
|
});
|