batchCooking/apps/web/cypress/e2e/planning-page.cy.ts
Nicolas a3701acae4
Some checks failed
CI / build-android (push) Blocked by required conditions
CI / lint (push) Successful in 3m18s
CI / intent-service-test (push) Successful in 4m8s
CI / build (push) Successful in 3m42s
CI / test (push) Failing after 10m11s
CI / e2e (push) Has been cancelled
fix(planning): popovers flottants surs sur mobile
Ticket #87 du chantier UI mobile & tablette. LeftoversBasket et
WeekNavigator's CalendarPopover etaient tous les deux des popovers a
largeur fixe (18-20rem), ancres right: 0, sans aucune gestion de bord
d'ecran - sur un telephone etroit ils pouvaient deborder hors du
viewport. Les deux sont declenches depuis l'en-tete du planning, un
ecran central de l'usage quotidien - tres probablement la cause
concrete derriere le retour terrain "je n'ai pas acces a tous les
boutons" sur mobile qui a lance ce chantier.

Les deux basculent desormais sur BottomSheet (ticket #91) sous le
breakpoint phone via useIsMobileViewport() - meme contenu, juste une
autre enveloppe (le popover flottant + son ecouteur de clic exterieur
manuel restent inchanges sur desktop). Le clic-exterieur devient un
no-op propre sur mobile plutot qu'un ecouteur qui ne fait jamais rien
(le ref ne s'attache a rien dans la branche BottomSheet).

Nouvelle cle i18n common.calendar.title ("Choisir une semaine") pour
le titre de la feuille - pas de raccourci en decoupant une autre cle
interpolee.

Verifie en local : tsc -b --force, biome check, build de prod, suite
Cypress e2e complete (142/142, 2 nouveaux tests phone dans
leftovers-basket.cy.ts et planning-page.cy.ts) et composants (63/63).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 12:48:39 +02:00

221 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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("disables 'Commencer à cuisiner' on an empty week and enables + navigates it once a recipe is planned", () => {
cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null });
cy.visit("/");
cy.contains("button", "Commencer à cuisiner").should("be.disabled");
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" },
},
],
},
});
cy.intercept("GET", /\/cooking-session\?/, {
statusCode: 200,
body: {
startDate: "2026-08-17T00:00:00.000Z",
finishDate: "2026-08-23T00:00:00.000Z",
recipes: [],
phases: [],
},
});
cy.visit("/");
cy.contains("button", "Commencer à cuisiner").should("not.be.disabled").click();
cy.url().should("include", "/cuisiner");
cy.url().should("include", "date=2026-08-17");
});
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");
});
it("opens the calendar 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: null }).as("getPlanning");
cy.visit("/");
cy.wait("@getPlanning");
cy.contains("button", "Semaine du").click();
cy.get(".calendar-popover").should("not.exist");
cy.get(".bottom-sheet").should("be.visible");
cy.contains(".bottom-sheet", "Choisir une semaine").should("be.visible");
cy.get(".bottom-sheet .calendar-grid__day").contains(/^25$/).click();
cy.contains("Semaine du 24 au 30 août 2026").should("be.visible");
cy.get(".bottom-sheet").should("not.exist");
});
});