Dernière PR (4/4) du chantier « gestion des restes » (issue #34). `@dnd-kit/core` (+ `@dnd-kit/utilities`) — première et seule utilisation de DnD de l'app. `PlanningPage` monte un `<DndContext>` (Mouse + Touch + Keyboard sensors, distance d'activation 5px pour ne pas voler le clic « Jeter ») autour de la grille et du panier. - `LeftoversBasket` : chaque ligne a une poignée `⠿` `useDraggable` — le nœud draggable est la poignée elle-même (petite), pas toute la `<li>`, pour qu'une détection de collision au pointeur (`pointerWithin`, repli `closestCenter` pour le clavier) résolve proprement vers la seule case sous le curseur. `data` porte `sourceItemId`/`recipeName`/`available`. - `MealCell` : `useDroppable` (`data: { date, weekDay, meal }`), surbrillance `--drop-over` au survol. Un item « restes » (`sourceItem !== null`) porte un badge ♻️ sur sa pastille. - Au drop, `onDragEnd` ouvre `LeftoverAssignDialog` (quantité, défaut = disponible, plafonné) → `POST /planning/items/leftover` (`apiClient.assignLeftover`) → nouvel item patché dans `planning.items`. Tests : `planning-leftovers.feature` (Gherkin) — parcours complet drag → confirmation quantité → pastille ♻️ visible → compteur du panier à jour ; + jet d'un reste depuis le panier. Le drag est piloté par événements souris bruts (mousedown poignée → mousemove ×2 → mouseup cible). Vérifié localement : 2/2, et aucune régression sur `leftovers-basket.cy.ts` / `planning-page.cy.ts` / `planning.feature`. i18n : `planning.leftovers.*` (badge, poignée, dialogue). Docs : `frontend-architecture.md`. Closes #34 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
132 lines
4.5 KiB
TypeScript
132 lines
4.5 KiB
TypeScript
import { Given, Then, When } from "@badeball/cypress-cucumber-preprocessor";
|
|
|
|
// Mocks the API via cy.intercept — this job has no live backend (see
|
|
// .github/workflows/ci.yml); apps/api's Mocha suite covers the real
|
|
// leftover endpoints. Drives @dnd-kit's MouseSensor with raw mouse
|
|
// events (mousedown on the handle → two mousemoves → mouseup on the
|
|
// target), the reliable way to script one of its drags in Cypress.
|
|
|
|
/** One fresh batch: 12 prepared for 10 requested -> a pool of 2, no leftovers item pointing at it yet. */
|
|
const SOURCE_ITEM = {
|
|
id: 1,
|
|
weekDay: "lundi",
|
|
meal: "diner",
|
|
portions: 12,
|
|
portionsRequested: 10,
|
|
discardedPortions: 0,
|
|
sourceItem: null,
|
|
recipe: { id: 1, name: "Lasagnes" },
|
|
};
|
|
|
|
Given("the planning has a batch with leftovers to spare", () => {
|
|
cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] });
|
|
cy.intercept("GET", /\/planning\?/, {
|
|
statusCode: 200,
|
|
body: {
|
|
id: 1,
|
|
startDate: "2026-08-17T00:00:00.000Z",
|
|
finishDate: "2026-08-23T00:00:00.000Z",
|
|
items: [SOURCE_ITEM],
|
|
},
|
|
}).as("getPlanning");
|
|
});
|
|
|
|
When("I open the leftovers basket", () => {
|
|
cy.get(".leftovers-basket__toggle").click();
|
|
});
|
|
|
|
Then(
|
|
"the leftovers basket lists {string} with {int} portions",
|
|
(recipe: string, portions: number) => {
|
|
cy.contains(".leftovers-basket__line", recipe).should("contain.text", `${portions} en restes`);
|
|
},
|
|
);
|
|
|
|
/** dnd-kit's MouseSensor listens on the drag element for `mousedown`, then on the document for `mousemove`/`mouseup`; a first move past the 5px activation distance, then a move onto the target, then release. */
|
|
function dragLeftoverOnto(targetSelectorChain: () => Cypress.Chainable<JQuery<HTMLElement>>) {
|
|
cy.get(".leftovers-basket__drag-handle")
|
|
.first()
|
|
.then(($handle) => {
|
|
const h = $handle[0].getBoundingClientRect();
|
|
const start = {
|
|
clientX: Math.round(h.left + h.width / 2),
|
|
clientY: Math.round(h.top + h.height / 2),
|
|
};
|
|
targetSelectorChain().then(($target) => {
|
|
const t = $target[0].getBoundingClientRect();
|
|
const end = {
|
|
clientX: Math.round(t.left + t.width / 2),
|
|
clientY: Math.round(t.top + t.height / 2),
|
|
};
|
|
cy.wrap($handle).trigger("mousedown", { button: 0, ...start });
|
|
cy.get("body")
|
|
.trigger("mousemove", {
|
|
button: 0,
|
|
clientX: start.clientX + 12,
|
|
clientY: start.clientY + 12,
|
|
})
|
|
.trigger("mousemove", { button: 0, ...end })
|
|
.trigger("mouseup", { button: 0, ...end });
|
|
});
|
|
});
|
|
}
|
|
|
|
/** The `<td>` for (Wednesday, lunch): MEALS order puts `dejeuner` at row index 2, WEEK_DAYS puts `mercredi` at td index 2. */
|
|
function wednesdayLunchCell() {
|
|
return cy.get(".planning-grid tbody tr").eq(2).find("td").eq(2);
|
|
}
|
|
|
|
When("I drag the {string} leftover onto Wednesday lunch", (_recipe: string) => {
|
|
cy.intercept("POST", "**/planning/items/leftover", (req) => {
|
|
req.reply({
|
|
statusCode: 201,
|
|
body: {
|
|
id: 99,
|
|
weekDay: "mercredi",
|
|
meal: "dejeuner",
|
|
portions: req.body.portions,
|
|
portionsRequested: req.body.portions,
|
|
discardedPortions: 0,
|
|
sourceItem: { id: req.body.sourceItemId },
|
|
recipe: { id: 1, name: "Lasagnes" },
|
|
},
|
|
});
|
|
}).as("assignLeftover");
|
|
|
|
dragLeftoverOnto(wednesdayLunchCell);
|
|
});
|
|
|
|
When("I confirm serving {int} leftover portions", (portions: number) => {
|
|
cy.get("#leftover-assign-portions").clear().type(String(portions));
|
|
cy.contains(".recipe-picker-confirm__confirm", "Ajouter au planning").click();
|
|
cy.wait("@assignLeftover").its("request.body").should("deep.include", {
|
|
sourceItemId: 1,
|
|
weekDay: "mercredi",
|
|
meal: "dejeuner",
|
|
portions,
|
|
});
|
|
});
|
|
|
|
Then("Wednesday lunch shows a leftovers chip for {string}", (recipe: string) => {
|
|
wednesdayLunchCell()
|
|
.find(".recipe-chip--leftover")
|
|
.should("contain.text", recipe)
|
|
.and("contain.text", "♻️");
|
|
});
|
|
|
|
Then("the leftovers basket count is gone", () => {
|
|
cy.get(".leftovers-basket__count").should("not.exist");
|
|
});
|
|
|
|
When("I throw away the {string} leftover", (recipe: string) => {
|
|
cy.intercept("PATCH", "**/planning/items/1/discard-leftover", {
|
|
statusCode: 200,
|
|
body: { ...SOURCE_ITEM, discardedPortions: 2 },
|
|
}).as("discard");
|
|
cy.contains(".leftovers-basket__line", recipe).find(".leftovers-basket__discard").click();
|
|
cy.wait("@discard");
|
|
});
|
|
|
|
Then("the leftovers basket shows nothing left", () => {
|
|
cy.contains(".leftovers-basket__empty", "Aucun reste cette semaine.").should("be.visible");
|
|
});
|