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 assert from "node:assert/strict";
|
||||||
import { Then, When } from "@cucumber/cucumber";
|
import { Then, When } from "@cucumber/cucumber";
|
||||||
import { prisma } from "../../src/db/prisma.js";
|
|
||||||
import { getEnglishKey } from "../../src/db/catalog-en-keys.js";
|
import { getEnglishKey } from "../../src/db/catalog-en-keys.js";
|
||||||
|
import { prisma } from "../../src/db/prisma.js";
|
||||||
import type { CustomWorld } from "../support/world.js";
|
import type { CustomWorld } from "../support/world.js";
|
||||||
|
|
||||||
/** Splits a comma-separated list of names from a `.feature` string into trimmed, non-empty parts. */
|
/** 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 assert from "node:assert/strict";
|
||||||
import { Given, Then, When } from "@cucumber/cucumber";
|
import { Given, Then, When } from "@cucumber/cucumber";
|
||||||
import { prisma } from "../../src/db/prisma.js";
|
|
||||||
import { getEnglishKey } from "../../src/db/catalog-en-keys.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 { TEST_REFERENCE_DATE } from "../../test-support/reference-date.js";
|
||||||
import type { CustomWorld } from "../support/world.js";
|
import type { CustomWorld } from "../support/world.js";
|
||||||
|
|
||||||
|
|
@ -12,7 +12,9 @@ import type { CustomWorld } from "../support/world.js";
|
||||||
* matching.
|
* matching.
|
||||||
*/
|
*/
|
||||||
async function findIngredientId(name: string): Promise<number> {
|
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;
|
return ingredient.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ Then(
|
||||||
function (this: CustomWorld, name: string) {
|
function (this: CustomWorld, name: string) {
|
||||||
const keys = (this.response.body as Array<{ key: string }>).map((item) => item.key);
|
const keys = (this.response.body as Array<{ key: string }>).map((item) => item.key);
|
||||||
const expectedKey = getEnglishKey(name);
|
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}`,
|
`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"));
|
const locale = JSON.parse(readFileSync(localePath, "utf8"));
|
||||||
locale.catalog = { diets, allergens, ingredients };
|
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}`);
|
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
|
* One-off validation, run by hand: checks that `catalog-en-keys.ts` has an
|
||||||
* entry for every diet/allergen/ingredient currently in
|
* entry for every diet/allergen/ingredient currently in
|
||||||
|
|
@ -6,7 +7,6 @@
|
||||||
* pre-flight check while building/editing the dictionary by hand.
|
* pre-flight check while building/editing the dictionary by hand.
|
||||||
*/
|
*/
|
||||||
import { ALLERGENS, DIETS, INGREDIENT_GROUPS } from "../src/db/reference-seed-data.js";
|
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[]) {
|
function check(label: string, names: string[]) {
|
||||||
const keys = new Map<string, string>();
|
const keys = new Map<string, string>();
|
||||||
|
|
@ -36,7 +36,10 @@ function check(label: string, names: string[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
check("Diets", DIETS);
|
check("Diets", DIETS);
|
||||||
check("Allergens", ALLERGENS.map((a) => a.name));
|
check(
|
||||||
|
"Allergens",
|
||||||
|
ALLERGENS.map((a) => a.name),
|
||||||
|
);
|
||||||
check(
|
check(
|
||||||
"Ingredients",
|
"Ingredients",
|
||||||
INGREDIENT_GROUPS.flatMap((g) => g.items.map((i) => i.name)),
|
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) {
|
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
|
// 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 { expect } from "chai";
|
||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
import { createApp } from "../src/app.js";
|
import { createApp } from "../src/app.js";
|
||||||
import { prisma } from "../src/db/prisma.js";
|
|
||||||
import { getEnglishKey } from "../src/db/catalog-en-keys.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 { resetDatabase } from "../test-support/reset-db.js";
|
||||||
|
|
||||||
function buildSignupPayload(): SignupInput {
|
function buildSignupPayload(): SignupInput {
|
||||||
|
|
@ -40,7 +40,9 @@ describe("Profile", () => {
|
||||||
it("sets the profile's regime to a valid, seeded diet", async () => {
|
it("sets the profile's regime to a valid, seeded diet", async () => {
|
||||||
const agent = request.agent(app);
|
const agent = request.agent(app);
|
||||||
await agent.post("/auth/signup").send(buildSignupPayload());
|
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 });
|
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 () => {
|
it("starts empty, then reflects a saved selection", async () => {
|
||||||
const agent = request.agent(app);
|
const agent = request.agent(app);
|
||||||
await agent.post("/auth/signup").send(buildSignupPayload());
|
await agent.post("/auth/signup").send(buildSignupPayload());
|
||||||
const tomate = await prisma.ingredient.findFirstOrThrow({ where: { key: getEnglishKey("Tomate") } });
|
const tomate = await prisma.ingredient.findFirstOrThrow({
|
||||||
const oignon = await prisma.ingredient.findFirstOrThrow({ where: { key: getEnglishKey("Oignon") } });
|
where: { key: getEnglishKey("Tomate") },
|
||||||
|
});
|
||||||
|
const oignon = await prisma.ingredient.findFirstOrThrow({
|
||||||
|
where: { key: getEnglishKey("Oignon") },
|
||||||
|
});
|
||||||
|
|
||||||
const initial = await agent.get("/profile/disliked-ingredients");
|
const initial = await agent.get("/profile/disliked-ingredients");
|
||||||
expect(initial.body).to.deep.equal([]);
|
expect(initial.body).to.deep.equal([]);
|
||||||
|
|
@ -160,8 +166,12 @@ describe("Profile", () => {
|
||||||
it("replaces (not merges) the previous selection", async () => {
|
it("replaces (not merges) the previous selection", async () => {
|
||||||
const agent = request.agent(app);
|
const agent = request.agent(app);
|
||||||
await agent.post("/auth/signup").send(buildSignupPayload());
|
await agent.post("/auth/signup").send(buildSignupPayload());
|
||||||
const tomate = await prisma.ingredient.findFirstOrThrow({ where: { key: getEnglishKey("Tomate") } });
|
const tomate = await prisma.ingredient.findFirstOrThrow({
|
||||||
const oignon = await prisma.ingredient.findFirstOrThrow({ where: { key: getEnglishKey("Oignon") } });
|
where: { key: getEnglishKey("Tomate") },
|
||||||
|
});
|
||||||
|
const oignon = await prisma.ingredient.findFirstOrThrow({
|
||||||
|
where: { key: getEnglishKey("Oignon") },
|
||||||
|
});
|
||||||
|
|
||||||
await agent
|
await agent
|
||||||
.patch("/profile/disliked-ingredients")
|
.patch("/profile/disliked-ingredients")
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import { faker } from "@faker-js/faker";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
import { createApp } from "../src/app.js";
|
import { createApp } from "../src/app.js";
|
||||||
import { prisma } from "../src/db/prisma.js";
|
|
||||||
import { getEnglishKey } from "../src/db/catalog-en-keys.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 { 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. */
|
/** 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`). */
|
/** 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> {
|
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;
|
return ingredient.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
import { createApp } from "../src/app.js";
|
import { createApp } from "../src/app.js";
|
||||||
|
import { getEnglishKey } from "../src/db/catalog-en-keys.js";
|
||||||
import { prisma } from "../src/db/prisma.js";
|
import { prisma } from "../src/db/prisma.js";
|
||||||
import { resetDatabase } from "../test-support/reset-db.js";
|
import { resetDatabase } from "../test-support/reset-db.js";
|
||||||
import { getEnglishKey } from "../src/db/catalog-en-keys.js";
|
|
||||||
|
|
||||||
describe("Reference data", () => {
|
describe("Reference data", () => {
|
||||||
const app = createApp();
|
const app = createApp();
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ describe("Onboarding wizard (regime → foyer → allergens)", () => {
|
||||||
cy.intercept("GET", "**/reference/diets", {
|
cy.intercept("GET", "**/reference/diets", {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
body: [
|
body: [
|
||||||
{ id: 1, name: "Omnivore" },
|
{ id: 1, key: "omnivore" },
|
||||||
{ id: 2, name: "Végétarien" },
|
{ id: 2, key: "vegetarian" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
cy.intercept("PATCH", "**/profile/diet", {
|
cy.intercept("PATCH", "**/profile/diet", {
|
||||||
|
|
@ -48,8 +48,8 @@ describe("Onboarding wizard (regime → foyer → allergens)", () => {
|
||||||
cy.intercept("GET", "**/reference/allergies", {
|
cy.intercept("GET", "**/reference/allergies", {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
body: [
|
body: [
|
||||||
{ id: 1, name: "Arachides", kind: "ALLERGY" },
|
{ id: 1, key: "peanuts", kind: "ALLERGY" },
|
||||||
{ id: 2, name: "Gluten", kind: "INTOLERANCE" },
|
{ id: 2, key: "gluten", kind: "INTOLERANCE" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [1] }).as(
|
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", {
|
cy.intercept("GET", "**/reference/diets", {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
body: [
|
body: [
|
||||||
{ id: 1, name: "Omnivore" },
|
{ id: 1, key: "omnivore" },
|
||||||
{ id: 2, name: "Végétarien" },
|
{ id: 2, key: "vegetarian" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
cy.intercept("GET", "**/reference/allergies", {
|
cy.intercept("GET", "**/reference/allergies", {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
body: [
|
body: [
|
||||||
{ id: 1, name: "Arachides", kind: "ALLERGY" },
|
{ id: 1, key: "peanuts", kind: "ALLERGY" },
|
||||||
{ id: 2, name: "Gluten", kind: "INTOLERANCE" },
|
{ id: 2, key: "gluten", kind: "INTOLERANCE" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
cy.intercept("GET", "**/profile/allergies", { statusCode: 200, body: [2] });
|
cy.intercept("GET", "**/profile/allergies", { statusCode: 200, body: [2] });
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,9 @@ describe("Recipe form — associating ingredients", () => {
|
||||||
|
|
||||||
cy.contains("button", "Enregistrer").should("not.be.disabled").click();
|
cy.contains("button", "Enregistrer").should("not.be.disabled").click();
|
||||||
|
|
||||||
cy.wait("@createRecipe").its("request.body").should("deep.include", {
|
cy.wait("@createRecipe")
|
||||||
|
.its("request.body")
|
||||||
|
.should("deep.include", {
|
||||||
name: "Salade de tomates",
|
name: "Salade de tomates",
|
||||||
ingredients: [{ ingredientId: 1, quantity: 3, unit: "unité" }],
|
ingredients: [{ ingredientId: 1, quantity: 3, unit: "unité" }],
|
||||||
});
|
});
|
||||||
|
|
@ -152,9 +154,7 @@ describe("Recipe form — associating ingredients", () => {
|
||||||
steps: [{ id: 1, description: "Battre les œufs.", picture: null, order: 0 }],
|
steps: [{ id: 1, description: "Battre les œufs.", picture: null, order: 0 }],
|
||||||
};
|
};
|
||||||
cy.intercept("GET", "**/recipes/7", { statusCode: 200, body: existingRecipe });
|
cy.intercept("GET", "**/recipes/7", { statusCode: 200, body: existingRecipe });
|
||||||
cy.intercept("PATCH", "**/recipes/7", { statusCode: 200, body: { id: 7 } }).as(
|
cy.intercept("PATCH", "**/recipes/7", { statusCode: 200, body: { id: 7 } }).as("updateRecipe");
|
||||||
"updateRecipe",
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.visit("/recettes/7/modifier");
|
cy.visit("/recettes/7/modifier");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ const authenticatedProfile = {
|
||||||
dietId: null,
|
dietId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const vegetarien = { id: 1, name: "Végétarien" };
|
const vegetarien = { id: 1, key: "vegetarian" };
|
||||||
const gluten = { id: 1, name: "Gluten", kind: "INTOLERANCE" };
|
const gluten = { id: 1, key: "gluten", kind: "INTOLERANCE" };
|
||||||
const oeufs = { id: 2, name: "Œufs", kind: "ALLERGY" };
|
const oeufs = { id: 2, key: "eggs", kind: "ALLERGY" };
|
||||||
|
|
||||||
const ratatouille = {
|
const ratatouille = {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
|
@ -47,7 +47,7 @@ const omeletteDetail = {
|
||||||
{
|
{
|
||||||
ingredient: {
|
ingredient: {
|
||||||
id: 10,
|
id: 10,
|
||||||
name: "Œuf",
|
key: "egg",
|
||||||
icon: "EGG",
|
icon: "EGG",
|
||||||
category: "CREMERIE_FROMAGE",
|
category: "CREMERIE_FROMAGE",
|
||||||
subcategory: "OEUFS",
|
subcategory: "OEUFS",
|
||||||
|
|
@ -181,7 +181,10 @@ describe("Recipe catalog", () => {
|
||||||
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] });
|
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] });
|
||||||
cy.intercept("GET", "**/recipes/999", {
|
cy.intercept("GET", "**/recipes/999", {
|
||||||
statusCode: 404,
|
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");
|
cy.visit("/recettes/999");
|
||||||
|
|
@ -204,9 +207,7 @@ describe("Recipe catalog", () => {
|
||||||
cy.wait("@favorite");
|
cy.wait("@favorite");
|
||||||
|
|
||||||
cy.get(".favorite-star-button").should("have.class", "is-favorite");
|
cy.get(".favorite-star-button").should("have.class", "is-favorite");
|
||||||
cy.contains(".recipe-table__name", "Omelette")
|
cy.contains(".recipe-table__name", "Omelette").find(".recipe-table__fav-mark").should("exist");
|
||||||
.find(".recipe-table__fav-mark")
|
|
||||||
.should("exist");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("deletes a recipe after a two-step confirmation, then clears the selection", () => {
|
it("deletes a recipe after a two-step confirmation, then clears the selection", () => {
|
||||||
|
|
@ -231,7 +232,10 @@ describe("Recipe catalog", () => {
|
||||||
|
|
||||||
cy.visit("/recettes");
|
cy.visit("/recettes");
|
||||||
|
|
||||||
cy.contains(".recipes-page__new-button", "Nouvelle recette")
|
cy.contains(".recipes-page__new-button", "Nouvelle recette").should(
|
||||||
.should("have.attr", "href", "/recettes/nouvelle");
|
"have.attr",
|
||||||
|
"href",
|
||||||
|
"/recettes/nouvelle",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue