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:
parent
3ead09a43f
commit
f00485f341
13 changed files with 70 additions and 43 deletions
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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] });
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue