From f00485f341768ee656052aafb2eff10a9e81f30f Mon Sep 17 00:00:00 2001 From: Nicolas Date: Tue, 18 Aug 2026 20:49:25 +0200 Subject: [PATCH] =?UTF-8?q?fix(ci):=20corrige=20les=20specs=20Cypress=20ca?= =?UTF-8?q?ss=C3=A9es=20par=20le=20refactor=20uid+i18n,=20applique=20biome?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../step-definitions/profile.steps.ts | 2 +- .../features/step-definitions/recipe.steps.ts | 6 +++-- .../step-definitions/reference.steps.ts | 5 +++- apps/api/scripts/generate-catalog-i18n.ts | 4 ++-- apps/api/scripts/validate-catalog-en-keys.ts | 7 ++++-- apps/api/src/db/reference-seed-data.ts | 5 +++- apps/api/test/profile.test.ts | 22 ++++++++++++----- apps/api/test/recipe.test.ts | 6 +++-- apps/api/test/reference.test.ts | 2 +- apps/web/cypress/e2e/onboarding.cy.ts | 8 +++---- apps/web/cypress/e2e/preferences.cy.ts | 8 +++---- apps/web/cypress/e2e/recipe-form.cy.ts | 14 +++++------ apps/web/cypress/e2e/recipes.cy.ts | 24 +++++++++++-------- 13 files changed, 70 insertions(+), 43 deletions(-) diff --git a/apps/api/features/step-definitions/profile.steps.ts b/apps/api/features/step-definitions/profile.steps.ts index 028c9b8..1503d92 100644 --- a/apps/api/features/step-definitions/profile.steps.ts +++ b/apps/api/features/step-definitions/profile.steps.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { Then, When } from "@cucumber/cucumber"; -import { prisma } from "../../src/db/prisma.js"; import { getEnglishKey } from "../../src/db/catalog-en-keys.js"; +import { prisma } from "../../src/db/prisma.js"; import type { CustomWorld } from "../support/world.js"; /** Splits a comma-separated list of names from a `.feature` string into trimmed, non-empty parts. */ diff --git a/apps/api/features/step-definitions/recipe.steps.ts b/apps/api/features/step-definitions/recipe.steps.ts index d5b684f..b18ccbc 100644 --- a/apps/api/features/step-definitions/recipe.steps.ts +++ b/apps/api/features/step-definitions/recipe.steps.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { Given, Then, When } from "@cucumber/cucumber"; -import { prisma } from "../../src/db/prisma.js"; import { getEnglishKey } from "../../src/db/catalog-en-keys.js"; +import { prisma } from "../../src/db/prisma.js"; import { TEST_REFERENCE_DATE } from "../../test-support/reference-date.js"; import type { CustomWorld } from "../support/world.js"; @@ -12,7 +12,9 @@ import type { CustomWorld } from "../support/world.js"; * matching. */ async function findIngredientId(name: string): Promise { - const ingredient = await prisma.ingredient.findFirstOrThrow({ where: { key: getEnglishKey(name) } }); + const ingredient = await prisma.ingredient.findFirstOrThrow({ + where: { key: getEnglishKey(name) }, + }); return ingredient.id; } diff --git a/apps/api/features/step-definitions/reference.steps.ts b/apps/api/features/step-definitions/reference.steps.ts index 722e945..82fb3c7 100644 --- a/apps/api/features/step-definitions/reference.steps.ts +++ b/apps/api/features/step-definitions/reference.steps.ts @@ -12,6 +12,9 @@ Then( function (this: CustomWorld, name: string) { const keys = (this.response.body as Array<{ key: string }>).map((item) => item.key); const expectedKey = getEnglishKey(name); - assert.ok(keys.includes(expectedKey), `expected ${JSON.stringify(keys)} to include "${expectedKey}"`); + assert.ok( + keys.includes(expectedKey), + `expected ${JSON.stringify(keys)} to include "${expectedKey}"`, + ); }, ); diff --git a/apps/api/scripts/generate-catalog-i18n.ts b/apps/api/scripts/generate-catalog-i18n.ts index 6852241..cc1a80c 100644 --- a/apps/api/scripts/generate-catalog-i18n.ts +++ b/apps/api/scripts/generate-catalog-i18n.ts @@ -42,8 +42,8 @@ console.log( `diets: ${Object.keys(diets).length}, allergens: ${Object.keys(allergens).length}, ingredients: ${Object.keys(ingredients).length}`, ); -const localePath = here + "../../web/src/locales/fr/translation.json"; +const localePath = `${here}../../web/src/locales/fr/translation.json`; const locale = JSON.parse(readFileSync(localePath, "utf8")); locale.catalog = { diets, allergens, ingredients }; -writeFileSync(localePath, JSON.stringify(locale, null, 2) + "\n"); +writeFileSync(localePath, `${JSON.stringify(locale, null, 2)}\n`); console.log(`wrote ${localePath}`); diff --git a/apps/api/scripts/validate-catalog-en-keys.ts b/apps/api/scripts/validate-catalog-en-keys.ts index 05bab54..d7199b2 100644 --- a/apps/api/scripts/validate-catalog-en-keys.ts +++ b/apps/api/scripts/validate-catalog-en-keys.ts @@ -1,3 +1,4 @@ +import { getEnglishKey } from "../src/db/catalog-en-keys.js"; /** * One-off validation, run by hand: checks that `catalog-en-keys.ts` has an * entry for every diet/allergen/ingredient currently in @@ -6,7 +7,6 @@ * pre-flight check while building/editing the dictionary by hand. */ import { ALLERGENS, DIETS, INGREDIENT_GROUPS } from "../src/db/reference-seed-data.js"; -import { getEnglishKey } from "../src/db/catalog-en-keys.js"; function check(label: string, names: string[]) { const keys = new Map(); @@ -36,7 +36,10 @@ function check(label: string, names: string[]) { } check("Diets", DIETS); -check("Allergens", ALLERGENS.map((a) => a.name)); +check( + "Allergens", + ALLERGENS.map((a) => a.name), +); check( "Ingredients", INGREDIENT_GROUPS.flatMap((g) => g.items.map((i) => i.name)), diff --git a/apps/api/src/db/reference-seed-data.ts b/apps/api/src/db/reference-seed-data.ts index 5c9dade..1d7d63a 100644 --- a/apps/api/src/db/reference-seed-data.ts +++ b/apps/api/src/db/reference-seed-data.ts @@ -960,7 +960,10 @@ export async function seedReferenceData(prisma: PrismaClient): Promise { ); }); for (const { name, icon, category, subcategory } of changed) { - await prisma.ingredient.update({ where: { key: getEnglishKey(name) }, data: { icon, category, subcategory } }); + await prisma.ingredient.update({ + where: { key: getEnglishKey(name) }, + data: { icon, category, subcategory }, + }); } // Re-resolve every ingredient's id (existing + just-created) and every diff --git a/apps/api/test/profile.test.ts b/apps/api/test/profile.test.ts index 9a57cf0..4368267 100644 --- a/apps/api/test/profile.test.ts +++ b/apps/api/test/profile.test.ts @@ -3,8 +3,8 @@ import { faker } from "@faker-js/faker"; import { expect } from "chai"; import request from "supertest"; import { createApp } from "../src/app.js"; -import { prisma } from "../src/db/prisma.js"; import { getEnglishKey } from "../src/db/catalog-en-keys.js"; +import { prisma } from "../src/db/prisma.js"; import { resetDatabase } from "../test-support/reset-db.js"; function buildSignupPayload(): SignupInput { @@ -40,7 +40,9 @@ describe("Profile", () => { it("sets the profile's regime to a valid, seeded diet", async () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); - const diet = await prisma.diet.findFirstOrThrow({ where: { key: getEnglishKey("Végétarien") } }); + const diet = await prisma.diet.findFirstOrThrow({ + where: { key: getEnglishKey("Végétarien") }, + }); const res = await agent.patch("/profile/diet").send({ dietId: diet.id }); @@ -141,8 +143,12 @@ describe("Profile", () => { it("starts empty, then reflects a saved selection", async () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); - const tomate = await prisma.ingredient.findFirstOrThrow({ where: { key: getEnglishKey("Tomate") } }); - const oignon = await prisma.ingredient.findFirstOrThrow({ where: { key: getEnglishKey("Oignon") } }); + const tomate = await prisma.ingredient.findFirstOrThrow({ + where: { key: getEnglishKey("Tomate") }, + }); + const oignon = await prisma.ingredient.findFirstOrThrow({ + where: { key: getEnglishKey("Oignon") }, + }); const initial = await agent.get("/profile/disliked-ingredients"); expect(initial.body).to.deep.equal([]); @@ -160,8 +166,12 @@ describe("Profile", () => { it("replaces (not merges) the previous selection", async () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); - const tomate = await prisma.ingredient.findFirstOrThrow({ where: { key: getEnglishKey("Tomate") } }); - const oignon = await prisma.ingredient.findFirstOrThrow({ where: { key: getEnglishKey("Oignon") } }); + const tomate = await prisma.ingredient.findFirstOrThrow({ + where: { key: getEnglishKey("Tomate") }, + }); + const oignon = await prisma.ingredient.findFirstOrThrow({ + where: { key: getEnglishKey("Oignon") }, + }); await agent .patch("/profile/disliked-ingredients") diff --git a/apps/api/test/recipe.test.ts b/apps/api/test/recipe.test.ts index 889e2c7..5cb356e 100644 --- a/apps/api/test/recipe.test.ts +++ b/apps/api/test/recipe.test.ts @@ -4,8 +4,8 @@ import { faker } from "@faker-js/faker"; import { expect } from "chai"; import request from "supertest"; import { createApp } from "../src/app.js"; -import { prisma } from "../src/db/prisma.js"; import { getEnglishKey } from "../src/db/catalog-en-keys.js"; +import { prisma } from "../src/db/prisma.js"; import { resetDatabase } from "../test-support/reset-db.js"; /** See `auth.test.ts` — generated rather than hardcoded, no test fixture looks like a real person's data. */ @@ -22,7 +22,9 @@ function buildSignupPayload(): SignupInput { /** Resolves a reference ingredient's id by its `reference-seed-data.ts` French name (slugified to match its `key`). */ async function ingredientId(name: string): Promise { - const ingredient = await prisma.ingredient.findFirstOrThrow({ where: { key: getEnglishKey(name) } }); + const ingredient = await prisma.ingredient.findFirstOrThrow({ + where: { key: getEnglishKey(name) }, + }); return ingredient.id; } diff --git a/apps/api/test/reference.test.ts b/apps/api/test/reference.test.ts index e7ed4e0..8fb423d 100644 --- a/apps/api/test/reference.test.ts +++ b/apps/api/test/reference.test.ts @@ -1,9 +1,9 @@ import { expect } from "chai"; import request from "supertest"; import { createApp } from "../src/app.js"; +import { getEnglishKey } from "../src/db/catalog-en-keys.js"; import { prisma } from "../src/db/prisma.js"; import { resetDatabase } from "../test-support/reset-db.js"; -import { getEnglishKey } from "../src/db/catalog-en-keys.js"; describe("Reference data", () => { const app = createApp(); diff --git a/apps/web/cypress/e2e/onboarding.cy.ts b/apps/web/cypress/e2e/onboarding.cy.ts index c8cf368..93c2af8 100644 --- a/apps/web/cypress/e2e/onboarding.cy.ts +++ b/apps/web/cypress/e2e/onboarding.cy.ts @@ -32,8 +32,8 @@ describe("Onboarding wizard (regime → foyer → allergens)", () => { cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [ - { id: 1, name: "Omnivore" }, - { id: 2, name: "Végétarien" }, + { id: 1, key: "omnivore" }, + { id: 2, key: "vegetarian" }, ], }); cy.intercept("PATCH", "**/profile/diet", { @@ -48,8 +48,8 @@ describe("Onboarding wizard (regime → foyer → allergens)", () => { cy.intercept("GET", "**/reference/allergies", { statusCode: 200, body: [ - { id: 1, name: "Arachides", kind: "ALLERGY" }, - { id: 2, name: "Gluten", kind: "INTOLERANCE" }, + { id: 1, key: "peanuts", kind: "ALLERGY" }, + { id: 2, key: "gluten", kind: "INTOLERANCE" }, ], }); cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [1] }).as( diff --git a/apps/web/cypress/e2e/preferences.cy.ts b/apps/web/cypress/e2e/preferences.cy.ts index fcaaa64..65f3111 100644 --- a/apps/web/cypress/e2e/preferences.cy.ts +++ b/apps/web/cypress/e2e/preferences.cy.ts @@ -16,15 +16,15 @@ describe("Dietary preferences (/parametres/preferences) — hot saving", () => { cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [ - { id: 1, name: "Omnivore" }, - { id: 2, name: "Végétarien" }, + { id: 1, key: "omnivore" }, + { id: 2, key: "vegetarian" }, ], }); cy.intercept("GET", "**/reference/allergies", { statusCode: 200, body: [ - { id: 1, name: "Arachides", kind: "ALLERGY" }, - { id: 2, name: "Gluten", kind: "INTOLERANCE" }, + { id: 1, key: "peanuts", kind: "ALLERGY" }, + { id: 2, key: "gluten", kind: "INTOLERANCE" }, ], }); cy.intercept("GET", "**/profile/allergies", { statusCode: 200, body: [2] }); diff --git a/apps/web/cypress/e2e/recipe-form.cy.ts b/apps/web/cypress/e2e/recipe-form.cy.ts index 1464c8a..44ae9fb 100644 --- a/apps/web/cypress/e2e/recipe-form.cy.ts +++ b/apps/web/cypress/e2e/recipe-form.cy.ts @@ -82,10 +82,12 @@ describe("Recipe form — associating ingredients", () => { 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.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"); }); @@ -152,9 +154,7 @@ describe("Recipe form — associating ingredients", () => { 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.intercept("PATCH", "**/recipes/7", { statusCode: 200, body: { id: 7 } }).as("updateRecipe"); cy.visit("/recettes/7/modifier"); diff --git a/apps/web/cypress/e2e/recipes.cy.ts b/apps/web/cypress/e2e/recipes.cy.ts index 96556e6..318a410 100644 --- a/apps/web/cypress/e2e/recipes.cy.ts +++ b/apps/web/cypress/e2e/recipes.cy.ts @@ -12,9 +12,9 @@ const authenticatedProfile = { dietId: null, }; -const vegetarien = { id: 1, name: "Végétarien" }; -const gluten = { id: 1, name: "Gluten", kind: "INTOLERANCE" }; -const oeufs = { id: 2, name: "Œufs", kind: "ALLERGY" }; +const vegetarien = { id: 1, key: "vegetarian" }; +const gluten = { id: 1, key: "gluten", kind: "INTOLERANCE" }; +const oeufs = { id: 2, key: "eggs", kind: "ALLERGY" }; const ratatouille = { id: 1, @@ -47,7 +47,7 @@ const omeletteDetail = { { ingredient: { id: 10, - name: "Œuf", + key: "egg", icon: "EGG", category: "CREMERIE_FROMAGE", subcategory: "OEUFS", @@ -181,7 +181,10 @@ describe("Recipe catalog", () => { cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] }); cy.intercept("GET", "**/recipes/999", { statusCode: 404, - body: { code: 4041, message: "not found" }, + // 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" }, }); cy.visit("/recettes/999"); @@ -204,9 +207,7 @@ describe("Recipe catalog", () => { cy.wait("@favorite"); cy.get(".favorite-star-button").should("have.class", "is-favorite"); - cy.contains(".recipe-table__name", "Omelette") - .find(".recipe-table__fav-mark") - .should("exist"); + cy.contains(".recipe-table__name", "Omelette").find(".recipe-table__fav-mark").should("exist"); }); it("deletes a recipe after a two-step confirmation, then clears the selection", () => { @@ -231,7 +232,10 @@ describe("Recipe catalog", () => { cy.visit("/recettes"); - cy.contains(".recipes-page__new-button", "Nouvelle recette") - .should("have.attr", "href", "/recettes/nouvelle"); + cy.contains(".recipes-page__new-button", "Nouvelle recette").should( + "have.attr", + "href", + "/recettes/nouvelle", + ); }); });