batchCooking/apps/web/cypress/e2e/cooking-session-page.cy.ts
Nicolas dfe0310b63
Some checks failed
CI / lint (push) Successful in 2m16s
CI / build (push) Successful in 3m11s
CI / e2e (push) Successful in 7m53s
CI / intent-service-test (push) Successful in 13m57s
CI / test (push) Failing after 17m23s
feat(cooking-session): onglets recettes/plan + stepper d'étapes cochables
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>
2026-09-03 09:12:08 +02:00

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