batchCooking/apps/web/cypress/e2e/leftovers-basket.cy.ts
Nicolas 8278878898
All checks were successful
CI / lint (push) Successful in 4m15s
CI / build (push) Successful in 4m12s
CI / intent-service-test (push) Successful in 4m55s
CI / e2e (push) Successful in 7m57s
CI / test (push) Successful in 16m14s
feat(planning): panier des restes dans l'en-tête (lecture seule + jeter)
PR 3/4 du chantier « gestion des restes » (issue #34).

`LeftoversBasket` (`features/planning/`) : une icône 🧺 avec badge de
compte dans l'en-tête du planning, à côté de `WeekNavigator`. Ouvre un
popover listant les restes encore disponibles de la semaine affichée —
même patron montage conditionnel + fermeture au `mousedown` extérieur que
`CalendarPopover`.

Les lignes sont dérivées à chaque rendu de `planning.items` par
`collectLeftoverLines` (`leftovers-basket.ts`, pur) : chaque item « frais »
(`sourceItem === null`) dont `computeAvailableLeftover` est `> 0`, le
`leftoverUses` étant reconstitué depuis le tableau plat (les items
« restes » qui pointent vers lui) — aucun aller-retour API en plus.

Chaque ligne : recette, jour/repas d'origine, portions restantes, bouton
« Jeter » → `PATCH /planning/items/:id/discard-leftover` (`apiClient.
discardLeftover`), la réponse est patchée dans `planning.items` par id
(`handleLeftoverDiscarded`). État vide quand rien ne reste.

Le glisser-déposer d'un reste sur un autre créneau arrive en PR 4.

Tests : `leftovers-basket.cy.ts` (mode layout) — badge de compte, ouverture
/ fermeture au clic extérieur, contenu d'une ligne, état vide, « Jeter »
qui appelle le PATCH et retire la ligne. Vérifié localement, 3/3.

i18n : `planning.leftovers.*`. Docs : `frontend-architecture.md`.

Réf #34

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 00:16:00 +02:00

108 lines
3.5 KiB
TypeScript

// Layout-mode spec for the planning header's leftovers basket — mocks
// `GET /planning` (this job has no live backend; apps/api's Mocha suite
// covers the real endpoints). Exercises the popover open/close, an
// outside click, and the "Jeter" button firing `PATCH .../discard-leftover`.
const authenticatedProfile = {
id: 1,
firstName: "Alice",
lastName: "Martin",
email: "alice@example.com",
tokenVersion: 0,
houseId: 1,
dietId: null,
};
/**
* A week with one fresh item that cooked 12 for 10 requested (pool of 2)
* and one leftovers item serving 1 of that pool — so the basket should
* offer 1 line with 1 portion still available.
*/
const planningWithLeftovers = {
id: 1,
startDate: "2026-08-17T00:00:00.000Z",
finishDate: "2026-08-23T00:00:00.000Z",
items: [
{
id: 1,
weekDay: "lundi",
meal: "diner",
portions: 12,
portionsRequested: 10,
discardedPortions: 0,
sourceItem: null,
recipe: { id: 1, name: "Lasagnes" },
},
{
id: 2,
weekDay: "mardi",
meal: "dejeuner",
portions: 1,
portionsRequested: 1,
discardedPortions: 0,
sourceItem: { id: 1 },
recipe: { id: 1, name: "Lasagnes" },
},
],
};
describe("Leftovers basket", () => {
beforeEach(() => {
cy.viewport(1600, 900);
cy.clock(new Date("2026-08-17T09:00:00Z"), ["Date"]);
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile });
});
it("lists a week's available leftovers, opens and closes on an outside click", () => {
cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: planningWithLeftovers });
cy.visit("/");
// Count badge = 1 fresh item with a remaining pool.
cy.get(".leftovers-basket__count").should("have.text", "1");
cy.get(".leftovers-basket__toggle").click();
cy.contains(".leftovers-basket__heading", "Restes disponibles").should("be.visible");
cy.contains(".leftovers-basket__line", "Lasagnes")
.should("contain.text", "Lundi")
.and("contain.text", "1 en restes");
// Clicking away from the popover closes it.
cy.get("h1").click();
cy.get(".leftovers-basket__popover").should("not.exist");
});
it("shows an empty state when nothing is left over", () => {
cy.intercept("GET", /\/planning\?/, {
statusCode: 200,
body: {
...planningWithLeftovers,
items: [{ ...planningWithLeftovers.items[0], portions: 10, portionsRequested: 10 }],
},
});
cy.visit("/");
cy.get(".leftovers-basket__count").should("not.exist");
cy.get(".leftovers-basket__toggle").click();
cy.contains(".leftovers-basket__empty", "Aucun reste cette semaine.").should("be.visible");
});
it("throws away a leftover pool via PATCH and drops it from the list", () => {
cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: planningWithLeftovers });
cy.intercept("PATCH", "**/planning/items/1/discard-leftover", {
statusCode: 200,
body: {
...planningWithLeftovers.items[0],
discardedPortions: 1,
},
}).as("discard");
cy.visit("/");
cy.get(".leftovers-basket__toggle").click();
cy.contains(".leftovers-basket__line", "Lasagnes").find(".leftovers-basket__discard").click();
cy.wait("@discard");
// Pool was 1, now fully discarded -> the line is gone, the popover is empty.
cy.contains(".leftovers-basket__empty", "Aucun reste cette semaine.").should("be.visible");
cy.get(".leftovers-basket__count").should("not.exist");
});
});