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
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>
221 lines
8.1 KiB
TypeScript
221 lines
8.1 KiB
TypeScript
// 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");
|
||
});
|
||
});
|