diff --git a/apps/web/cypress/e2e/home-planning.cy.ts b/apps/web/cypress/e2e/home-planning.cy.ts deleted file mode 100644 index 01e1d02..0000000 --- a/apps/web/cypress/e2e/home-planning.cy.ts +++ /dev/null @@ -1,104 +0,0 @@ -// Mocks the API via cy.intercept — see auth.cy.ts for the rationale (no -// live backend in this CI job; apps/api's own Mocha/Cucumber suites cover -// real API behavior against a real database). - -const authenticatedProfile = { - id: 1, - firstName: "Alice", - lastName: "Martin", - email: "alice@example.com", - tokenVersion: 0, - houseId: 1, - dietId: null, -}; - -describe("Sidebar navigation", () => { - beforeEach(() => { - cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); - cy.intercept("GET", "**/planning/current", { 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 stub 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"); - }); - - it("shows the signed-in user's name and lets them log out from the account menu", () => { - cy.intercept("POST", "**/auth/logout", { statusCode: 204 }).as("logout"); - - cy.contains("button", "Bonjour Alice").should("be.visible").click(); - cy.contains("button", "Se déconnecter").click(); - - cy.wait("@logout"); - cy.url().should("include", "/login"); - }); -}); - -describe("Home planning view", () => { - it("shows an empty state when the household has no current planning", () => { - cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); - cy.intercept("GET", "**/planning/current", { statusCode: 200, body: null }); - - cy.visit("/"); - - cy.contains("h1", "Planning de la semaine").should("be.visible"); - cy.contains("Aucun planning pour cette semaine.").should("be.visible"); - cy.get("table").should("not.exist"); - }); - - it("renders the current planning's meals when there is one", () => { - cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); - cy.intercept("GET", "**/planning/current", { - statusCode: 200, - body: { - id: 1, - startDate: "2026-08-10T00:00:00.000Z", - finishDate: "2026-08-16T00:00:00.000Z", - items: [ - { id: 1, weekDay: "lundi", meal: "Dîner", recipe: { id: 1, name: "Ratatouille" } }, - { - id: 2, - weekDay: "mardi", - meal: "Déjeuner", - recipe: { id: 2, name: "Curry de lentilles" }, - }, - ], - }, - }); - - cy.visit("/"); - - cy.contains("Aucun planning pour cette semaine.").should("not.exist"); - cy.get("table.planning-table tbody tr").should("have.length", 2); - cy.contains("td", "Ratatouille").should("be.visible"); - cy.contains("td", "Curry de lentilles").should("be.visible"); - }); - - it("shows an error state when the planning request fails", () => { - cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); - cy.intercept("GET", "**/planning/current", { - statusCode: 500, - body: { code: 5000, message: "boom" }, - }); - - cy.visit("/"); - - cy.contains("Impossible de charger le planning, réessayez plus tard").should("be.visible"); - }); -}); diff --git a/apps/web/cypress/e2e/planning-page.cy.ts b/apps/web/cypress/e2e/planning-page.cy.ts new file mode 100644 index 0000000..ab13c85 --- /dev/null +++ b/apps/web/cypress/e2e/planning-page.cy.ts @@ -0,0 +1,163 @@ +// Mocks the API via cy.intercept — see auth.cy.ts for the rationale (no +// live backend in this CI job; apps/api's own Mocha/Cucumber suites cover +// real API behavior 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 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 stub 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"); + }); + + it("shows the signed-in user's name and lets them log out from the account menu", () => { + cy.intercept("POST", "**/auth/logout", { statusCode: 204 }).as("logout"); + + cy.contains("button", "Bonjour Alice").should("be.visible").click(); + cy.contains("button", "Se déconnecter").click(); + + cy.wait("@logout"); + cy.url().should("include", "/login"); + }); +}); + +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", recipe: { id: 1, name: "Ratatouille" } }, + { + id: 2, + weekDay: "mercredi", + meal: "dejeuner", + 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"); + }); +});