fix(ci): corrige les specs Cypress cassées par le refactor uid+i18n, applique biome

- 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>
This commit is contained in:
Nicolas 2026-08-18 20:49:25 +02:00
parent 3ead09a43f
commit f00485f341
13 changed files with 70 additions and 43 deletions

View file

@ -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. */

View file

@ -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<number> {
const ingredient = await prisma.ingredient.findFirstOrThrow({ where: { key: getEnglishKey(name) } });
const ingredient = await prisma.ingredient.findFirstOrThrow({
where: { key: getEnglishKey(name) },
});
return ingredient.id;
}

View file

@ -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}"`,
);
},
);

View file

@ -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}`);

View file

@ -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<string, string>();
@ -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)),

View file

@ -960,7 +960,10 @@ export async function seedReferenceData(prisma: PrismaClient): Promise<void> {
);
});
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

View file

@ -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")

View file

@ -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<number> {
const ingredient = await prisma.ingredient.findFirstOrThrow({ where: { key: getEnglishKey(name) } });
const ingredient = await prisma.ingredient.findFirstOrThrow({
where: { key: getEnglishKey(name) },
});
return ingredient.id;
}

View file

@ -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();

View file

@ -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(

View file

@ -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] });

View file

@ -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");

View file

@ -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",
);
});
});