// 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"); }); it("opens inside a bottom sheet on a phone instead of the anchored popover", () => { // Literal 390×844 rather than `cy.setViewport("phone")` (ticket #83) — // that command doesn't exist on this ticket's own base yet; matches its // PHONE_VIEWPORT value on purpose, worth switching to the real import // once both have landed on `main`. cy.viewport(390, 844); cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: planningWithLeftovers }); cy.visit("/"); cy.get(".leftovers-basket__toggle").click(); cy.get(".leftovers-basket__popover").should("not.exist"); cy.get(".bottom-sheet").should("be.visible"); cy.contains(".bottom-sheet", "Restes disponibles").should("be.visible"); cy.contains(".bottom-sheet .leftovers-basket__line", "Lasagnes").should("be.visible"); cy.get(".bottom-sheet__close").click(); cy.get(".bottom-sheet").should("not.exist"); }); });