// 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 API // behavior against a real database. // // The logout journey (also reachable from here, via the account menu) moved // to auth.feature — same behavior, no need to cover it twice. 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 via `cy.clock` so "today"/"this week" // assertions are deterministic instead of depending on the day the suite // happens to run. const TODAY = new Date("2026-08-17T09:00:00Z"); function freezeToday() { cy.clock(TODAY, ["Date"]); } describe("Sidebar navigation", () => { beforeEach(() => { cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }); cy.visit("/"); }); // The Foyer/Compte/Préférences links — behind the sidebar's "Paramètres" // toggle, not the main nav tested here — are covered by sidebar.cy.ts. it("highlights the current section and navigates between pages", () => { cy.contains("nav a", "Planning").should("have.class", "active"); cy.contains("nav a", "Recettes").click(); cy.url().should("include", "/recettes"); cy.contains("h1", "Recettes").should("be.visible"); cy.contains("nav a", "Recettes").should("have.class", "active"); cy.contains("nav a", "Planning").should("not.have.class", "active"); cy.contains("nav a", "Liste de courses").click(); cy.url().should("include", "/liste-de-courses"); cy.contains("h1", "Liste de courses").should("be.visible"); cy.contains("nav a", "Planning").click(); cy.url().should("eq", `${Cypress.config().baseUrl}/`); cy.contains("h1", "Planning de la semaine").should("be.visible"); }); }); describe("Planning grid", () => { beforeEach(() => { // Desktop-only design (see the plan/PR description) — wider than // Cypress's default 1000×660 so all 7 day columns fit without the grid's // horizontal scroll hiding the later ones from visibility assertions. cy.viewport(1600, 900); freezeToday(); cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); }); it("shows an empty grid (every slot just offering '+') when the household has no planning yet", () => { cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }).as("getPlanning"); cy.visit("/"); cy.wait("@getPlanning").its("request.url").should("include", "date=2026-08-17"); cy.contains("h1", "Planning de la semaine").should("be.visible"); // 5 meal rows × 7 days = 35 empty slots, each just a "+". cy.get(".add-recipe-btn").should("have.length", 35); cy.get(".recipe-chip").should("not.exist"); }); it("renders each recipe in its (day, meal) cell, and highlights today's column", () => { cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: { id: 1, startDate: "2026-08-17T00:00:00.000Z", finishDate: "2026-08-23T00:00:00.000Z", items: [ { id: 1, weekDay: "mardi", meal: "diner", portions: 4, recipe: { id: 1, name: "Ratatouille" }, }, { id: 2, weekDay: "mercredi", meal: "dejeuner", portions: 2, recipe: { id: 2, name: "Curry de lentilles" }, }, ], }, }); cy.visit("/"); cy.contains("th", "Lundi").should("be.visible"); cy.contains("th", "Dimanche").should("be.visible"); cy.contains(".recipe-chip", "Ratatouille").should("be.visible"); cy.contains(".recipe-chip", "Curry de lentilles").should("be.visible"); // Today (17 août, Lundi) is marked — its column header carries `.today`. cy.contains("th.today .day-date", "17").should("be.visible"); }); it("shows a loading state, then an error state when the request fails", () => { cy.intercept("GET", /\/planning\?/, { statusCode: 500, body: { code: 5000, message: "boom" }, }); cy.visit("/"); cy.contains("Impossible de charger le planning, réessayez plus tard").should("be.visible"); }); // Assertions below check the rendered week label/badge, not the intercepted // request count — React StrictMode (see main.tsx) double-invokes effects in // dev, so the `GET /planning` mount effect can fire twice per navigation; // counting exact `cy.wait` calls against that would be flaky, but the // rendered result is the same either way. it("navigates to the next/previous week, re-fetching each time", () => { cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }).as("getPlanning"); cy.visit("/"); cy.wait("@getPlanning").its("request.url").should("include", "date=2026-08-17"); cy.contains("Semaine du 17 au 23 août 2026").should("be.visible"); cy.contains("Cette semaine").should("be.visible"); cy.get(".week-nav__arrow").last().click(); cy.contains("Semaine du 24 au 30 août 2026").should("be.visible"); cy.contains("Cette semaine").should("not.exist"); cy.get(".week-nav__arrow").first().click(); cy.contains("Semaine du 17 au 23 août 2026").should("be.visible"); cy.get(".week-nav__arrow").first().click(); cy.contains("Semaine du 10 au 16 août 2026").should("be.visible"); }); it("jumps to an arbitrary week by picking a day in the calendar popover", () => { cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null }).as("getPlanning"); cy.visit("/"); cy.wait("@getPlanning"); cy.contains("button", "Semaine du").click(); cy.get(".calendar-popover").should("be.visible"); // Picking the 25th (still August, unambiguous in the visible grid) // should jump to the week of the 24th–30th. cy.get(".calendar-grid__day").contains(/^25$/).click(); cy.contains("Semaine du 24 au 30 août 2026").should("be.visible"); cy.get(".calendar-popover").should("not.exist"); }); });