Remanie /cuisiner : au lieu d'empiler toutes les phases, deux onglets (Recettes, Plan) et un stepper qui parcourt les phases une à une. Chaque tâche est cochable ; la progression est mémorisée par semaine dans localStorage — aucun état serveur, même parti pris que le reste de la page. - CookingSessionPage : barre d'onglets + PlanTab (stepper, nav précédent/suivant, clic direct sur un nœud) + RecipesTab (liste des recettes de la semaine, lien vers la fiche) - cooking-progress.ts : CookingProgressStore (load/save localStorage, clé batchcooking:cookingProgress:<semaine>) - cooking-session.ts : reconcileChecked / phaseProgress (helpers purs) - réutilise CheckboxOption pour les cartes de tâche cochables - i18n : cookingSession.tabs / stepper / progress / recipesTab - tests Cypress (mode layout + Gherkin) mis à jour et étendus Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
239 lines
8.4 KiB
TypeScript
239 lines
8.4 KiB
TypeScript
// Mocks the API via cy.intercept — this job doesn't run a live backend (see
|
|
// .github/workflows/ci.yml); apps/api's own Mocha suite covers real
|
|
// `GET /cooking-session` behavior (including the optimizer) against a real
|
|
// database.
|
|
|
|
const authenticatedProfile = {
|
|
id: 1,
|
|
firstName: "Alice",
|
|
lastName: "Martin",
|
|
email: "alice@example.com",
|
|
tokenVersion: 0,
|
|
houseId: 1,
|
|
dietId: null,
|
|
};
|
|
|
|
// 2026-08-17 is a Monday — frozen so "this week" is deterministic.
|
|
const TODAY = new Date("2026-08-17T09:00:00Z");
|
|
|
|
/** Bare `IngredientView` — only `key` drives the page's label lookup. */
|
|
function ingredient(key: string) {
|
|
return {
|
|
id: 1,
|
|
key,
|
|
icon: "VEGETABLE",
|
|
category: "freshProduce",
|
|
subcategory: "vegetables",
|
|
reproducible: false,
|
|
allergens: [],
|
|
diets: [],
|
|
};
|
|
}
|
|
|
|
/** A 3-phase plan: a pooled prep task in mise-en-place, then two cooking phases, the last with a passive cook floated into its background. */
|
|
function planFixture() {
|
|
return {
|
|
startDate: "2026-08-17T00:00:00.000Z",
|
|
finishDate: "2026-08-23T00:00:00.000Z",
|
|
recipes: [
|
|
{ recipeId: 1, name: "Soupe à l'oignon", portions: 4 },
|
|
{ recipeId: 2, name: "Tarte à l'oignon", portions: 6 },
|
|
],
|
|
phases: [
|
|
{
|
|
index: 0,
|
|
kind: "mise-en-place",
|
|
background: [],
|
|
tasks: [
|
|
{
|
|
id: "prep:chop:onion",
|
|
kind: "merged-prep",
|
|
technique: { id: 1, key: "chop" },
|
|
description: null,
|
|
ingredients: [
|
|
{
|
|
ingredient: ingredient("onion"),
|
|
quantity: 5,
|
|
unit: { id: 2, key: "piece", type: "COUNT", toBaseFactor: 1 },
|
|
},
|
|
],
|
|
utensils: [{ id: 1, key: "knife" }],
|
|
sourceRecipes: [
|
|
{ recipeId: 1, name: "Soupe à l'oignon", portions: 4 },
|
|
{ recipeId: 2, name: "Tarte à l'oignon", portions: 6 },
|
|
],
|
|
originalSteps: [],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
index: 1,
|
|
kind: "cooking",
|
|
background: [],
|
|
tasks: [
|
|
{
|
|
id: "step:0:1",
|
|
kind: "step",
|
|
technique: { id: 5, key: "simmer" },
|
|
description: "Faire mijoter le bouillon",
|
|
ingredients: [],
|
|
utensils: [],
|
|
sourceRecipes: [{ recipeId: 1, name: "Soupe à l'oignon", portions: 4 }],
|
|
originalSteps: [],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
index: 2,
|
|
kind: "cooking",
|
|
background: [
|
|
{
|
|
id: "bg:step:0:1",
|
|
technique: { id: 5, key: "simmer" },
|
|
description: "Faire mijoter le bouillon",
|
|
recipeId: 1,
|
|
recipeName: "Soupe à l'oignon",
|
|
},
|
|
],
|
|
tasks: [
|
|
{
|
|
id: "step:1:3",
|
|
kind: "step",
|
|
technique: { id: 4, key: "bake" },
|
|
description: "Enfourner la tarte",
|
|
ingredients: [],
|
|
utensils: [],
|
|
sourceRecipes: [{ recipeId: 2, name: "Tarte à l'oignon", portions: 6 }],
|
|
originalSteps: [],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
describe("Cooking session page", () => {
|
|
beforeEach(() => {
|
|
cy.viewport(1400, 900);
|
|
cy.clock(TODAY, ["Date"]);
|
|
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile });
|
|
});
|
|
|
|
it("shows the empty message when nothing is planned that week", () => {
|
|
cy.intercept("GET", /\/cooking-session\?/, {
|
|
statusCode: 200,
|
|
body: {
|
|
startDate: "2026-08-17T00:00:00.000Z",
|
|
finishDate: "2026-08-23T00:00:00.000Z",
|
|
recipes: [],
|
|
phases: [],
|
|
},
|
|
});
|
|
|
|
cy.visit("/cuisiner");
|
|
|
|
cy.contains("h1", "Cuisiner cette semaine").should("be.visible");
|
|
cy.contains("Rien de planifié cette semaine à cuisiner").should("be.visible");
|
|
});
|
|
|
|
it("opens on the Plan tab: a stepper of every phase, only the first phase's tasks shown", () => {
|
|
cy.intercept("GET", /\/cooking-session\?/, { statusCode: 200, body: planFixture() }).as(
|
|
"getPlan",
|
|
);
|
|
|
|
cy.visit("/cuisiner?date=2026-08-17");
|
|
cy.wait("@getPlan").its("request.url").should("include", "date=2026-08-17");
|
|
|
|
// One stepper node per phase; the first is the current one.
|
|
cy.get(".cooking-stepper__node").should("have.length", 3);
|
|
cy.get(".cooking-stepper__node").first().should("have.class", "cooking-stepper__node--current");
|
|
cy.contains(".cooking-plan__position", "Étape 1 / 3").should("be.visible");
|
|
cy.contains(".cooking-plan__count", "0 / 1 tâches").should("be.visible");
|
|
|
|
// Mise en place: the pooled prep task, flagged shared, naming both recipes.
|
|
cy.get(".cooking-task--merged-prep")
|
|
.should("contain.text", "Hacher")
|
|
.and("contain.text", "Oignon")
|
|
.and("contain.text", "Mutualisé");
|
|
|
|
// The next phase's step is not rendered yet.
|
|
cy.contains("Faire mijoter le bouillon").should("not.exist");
|
|
|
|
// Prev is disabled on the first phase.
|
|
cy.contains(".cooking-plan__nav-btn", "Étape précédente").should("be.disabled");
|
|
});
|
|
|
|
it("walks the phases with the next/prev buttons and by clicking a stepper node", () => {
|
|
cy.intercept("GET", /\/cooking-session\?/, { statusCode: 200, body: planFixture() });
|
|
cy.visit("/cuisiner?date=2026-08-17");
|
|
|
|
// Forward to phase 2.
|
|
cy.contains(".cooking-plan__nav-btn", "Étape suivante").click();
|
|
cy.contains(".cooking-plan__position", "Étape 2 / 3").should("be.visible");
|
|
cy.contains(".cooking-phase__tasks", "Faire mijoter le bouillon").should("be.visible");
|
|
|
|
// Forward to phase 3 — its background band shows the soup still simmering.
|
|
cy.contains(".cooking-plan__nav-btn", "Étape suivante").click();
|
|
cy.contains(".cooking-plan__position", "Étape 3 / 3").should("be.visible");
|
|
cy.contains(".cooking-phase__background", "Pendant ce temps")
|
|
.should("contain.text", "Faire mijoter le bouillon")
|
|
.and("contain.text", "Soupe à l'oignon");
|
|
cy.contains(".cooking-phase__tasks", "Enfourner la tarte").should("be.visible");
|
|
cy.contains(".cooking-plan__nav-btn", "Étape suivante").should("be.disabled");
|
|
|
|
// Back one step.
|
|
cy.contains(".cooking-plan__nav-btn", "Étape précédente").click();
|
|
cy.contains(".cooking-plan__position", "Étape 2 / 3").should("be.visible");
|
|
|
|
// Jump straight back to the first phase from the stepper.
|
|
cy.get(".cooking-stepper__node").first().click();
|
|
cy.contains(".cooking-plan__position", "Étape 1 / 3").should("be.visible");
|
|
cy.get(".cooking-task--merged-prep").should("be.visible");
|
|
});
|
|
|
|
it("ticks a task, marks its phase done in the stepper, and keeps the tick after a reload", () => {
|
|
cy.intercept("GET", /\/cooking-session\?/, { statusCode: 200, body: planFixture() });
|
|
cy.visit("/cuisiner?date=2026-08-17");
|
|
|
|
cy.get(".cooking-task").first().find('input[type="checkbox"]').check({ force: true });
|
|
cy.get(".cooking-task").first().should("have.class", "cooking-task--done");
|
|
// The whole phase is now done — its stepper node reflects that.
|
|
cy.get(".cooking-stepper__node").first().should("have.class", "cooking-stepper__node--done");
|
|
cy.contains(".cooking-plan__count", "1 / 1 tâches").should("be.visible");
|
|
|
|
cy.reload();
|
|
|
|
cy.get(".cooking-task").first().should("have.class", "cooking-task--done");
|
|
cy.get(".cooking-stepper__node").first().should("have.class", "cooking-stepper__node--done");
|
|
});
|
|
|
|
it("lists the week's recipes on the Recettes tab, each linking to its own page", () => {
|
|
cy.intercept("GET", /\/cooking-session\?/, { statusCode: 200, body: planFixture() });
|
|
cy.visit("/cuisiner?date=2026-08-17");
|
|
|
|
cy.contains(".cooking-session-page__tab", "Recettes").click();
|
|
|
|
cy.get(".cooking-recipes__item").should("have.length", 2);
|
|
cy.contains(".cooking-recipes__item", "Soupe à l'oignon")
|
|
.find("a")
|
|
.should("have.attr", "href", "/recettes/1");
|
|
cy.contains(".cooking-recipes__item", "Tarte à l'oignon")
|
|
.should("contain.text", "6 portions")
|
|
.find("a")
|
|
.should("have.attr", "href", "/recettes/2");
|
|
|
|
// The stepper isn't part of this tab.
|
|
cy.get(".cooking-stepper").should("not.exist");
|
|
});
|
|
|
|
it("shows an error state when the request fails", () => {
|
|
cy.intercept("GET", /\/cooking-session\?/, {
|
|
statusCode: 500,
|
|
body: { code: 5000, message: "boom" },
|
|
});
|
|
|
|
cy.visit("/cuisiner");
|
|
|
|
cy.contains("Impossible de charger").should("be.visible");
|
|
});
|
|
});
|