diff --git a/apps/web/cypress/e2e/cooking-session-page.cy.ts b/apps/web/cypress/e2e/cooking-session-page.cy.ts index f3312c0..7001fd5 100644 --- a/apps/web/cypress/e2e/cooking-session-page.cy.ts +++ b/apps/web/cypress/e2e/cooking-session-page.cy.ts @@ -3,6 +3,8 @@ // `GET /cooking-session` behavior (including the optimizer) against a real // database. +import { PHONE_VIEWPORT } from "../support/viewports"; + const authenticatedProfile = { id: 1, firstName: "Alice", @@ -421,3 +423,72 @@ describe("Cooking session page", () => { cy.contains("Impossible de charger").should("be.visible"); }); }); + +// Ticket #89 — this page's first dedicated responsive pass (it previously +// had no `@media` rule at all, see cooking-session-page.scss's own header +// comment). No layout is restructured on phone (the flex-column shell and +// the two horizontally-scrolling rows already degrade safely) — this covers +// the two things that actually needed fixing: comfortable touch targets on +// the screen's own controls, and a real flex overflow bug in the Recettes +// tab (a long recipe name had no upper bound and could crowd its portions +// badge/chevron out of the row). +describe("Cooking session page (phone)", () => { + beforeEach(() => { + cy.setViewport("phone"); + cy.clock(TODAY, ["Date"]); + cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); + }); + + it("gives the task cards and stepper nodes a taller, more tappable padding than on desktop", () => { + cy.intercept("GET", /\/cooking-session\?/, { statusCode: 200, body: planFixture() }); + cy.visit("/cuisiner?date=2026-08-17"); + + cy.get(".cooking-task") + .first() + .should(($el) => { + // --space-md (16px) on phone vs. the shared checkbox-label rule's + // own --space-xs (4px) app-wide default — see cooking-session-page + // .scss's phone block. + expect(getComputedStyle($el[0]).paddingTop).to.equal("16px"); + }); + cy.get(".cooking-stepper__node") + .first() + .should(($el) => { + expect(getComputedStyle($el[0]).paddingTop).to.equal("16px"); + }); + + // The document itself never scrolls sideways, same global rule every + // other page is held to. + cy.document() + .its("documentElement.scrollWidth") + .should("be.lte", PHONE_VIEWPORT.width + 1); + }); + + it("truncates a long recipe name in the Recettes tab instead of pushing its portions badge out of view", () => { + const longName = + "Un très long nom de recette qui ne devrait surtout pas pousser le badge de portions hors de l'écran"; + cy.intercept("GET", /\/cooking-session\?/, { + statusCode: 200, + body: { + startDate: "2026-08-17T00:00:00.000Z", + finishDate: "2026-08-23T00:00:00.000Z", + recipes: [{ recipeId: 1, name: longName, portions: 4 }], + phases: [], + }, + }); + cy.visit("/cuisiner?date=2026-08-17"); + cy.contains(".cooking-session-page__tab", "Recettes").click(); + + cy.get(".cooking-recipes__name").should(($el) => { + const s = getComputedStyle($el[0]); + expect(s.textOverflow).to.equal("ellipsis"); + expect(s.whiteSpace).to.equal("nowrap"); + // The name's own box is clipped narrower than its full text — proof + // the truncation is actually engaged, not just declared. + expect($el[0].scrollWidth).to.be.greaterThan($el[0].clientWidth); + }); + // ...and the badge survives, still on screen, not clipped away by the + // card's own `overflow: hidden`. + cy.contains(".cooking-recipes__portions", "4 portions").should("be.visible"); + }); +}); diff --git a/apps/web/src/pages/cooking-session/cooking-session-page.scss b/apps/web/src/pages/cooking-session/cooking-session-page.scss index 7aec442..0607f52 100644 --- a/apps/web/src/pages/cooking-session/cooking-session-page.scss +++ b/apps/web/src/pages/cooking-session/cooking-session-page.scss @@ -13,6 +13,8 @@ // on the theme. // ============================================================================= +@use "../../styles/breakpoints" as bp; + .cooking-session-page { height: 100%; display: flex; @@ -134,12 +136,24 @@ cursor: pointer; } + // `flex: 1; min-width: 0` + truncation — without it, a long recipe name + // has no upper bound in this flex row and can crowd the portions badge + // and chevron off to the side (or past the card's own `overflow: hidden` + // on `&__item`, silently clipping them) instead of the name itself + // truncating. Same fix as `.recipe-chip__name` (planning-page.scss) for + // the identical shape of bug. &__name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; font-weight: 600; color: var(--color-text); } &__portions { + flex-shrink: 0; padding: 0.1rem var(--space-sm); border-radius: var(--radius-pill); background: var(--color-surface-alt); @@ -486,3 +500,61 @@ color: var(--color-text-muted); } } + +// --- Phone (issue #89, first responsive pass) ------------------------------- +// Nothing here reorganizes the layout — the flex-column shell and the two +// horizontally-scrolling rows (`&__tabs`, `.cooking-stepper`, both the same +// generic `overflow-x: auto` pattern used throughout the app) already +// degrade safely on a narrow screen, and every text block wraps within its +// own flex/block container (no fixed widths). What was actually missing: +// comfortable touch targets on this screen's own controls — the task cards +// especially, since ticking one off is THE primary, repeated, one-handed +// action of the whole "Plan" tab. Same audit `planning-page.scss` got in +// #88, applied here to this page's own elements. +@include bp.phone { + // The task card is a shared "selectable card" checkbox label + // (global.scss's `label:has(> input[type="checkbox"])`, padding + // `--space-xs --space-sm` app-wide — fine with a mouse, too tight to tap + // reliably mid-cooking). `--space-md` all around clears Android's 48dp + // minimum touch target (this app is Android-only, see the root README) + // even in the worst case — a one-line headline with no utensils and no + // "Mutualisé" badge (~25px of text): 25 + 2×16 = 57px. Bumped here, not + // in the global rule: this page's task cards are the one place site-wide + // where quick, repeated tapping actually matters. Scoped under + // `.cooking-phase__tasks` (not a bare `.cooking-task` selector) for the + // same specificity reason as `&__tasks .cooking-task--done` above: a + // plain single class loses to `label:has(> input[type="checkbox"])`'s + // own specificity (a `:has()` argument counts its inner element + + // attribute), so it silently never applied without this. + .cooking-phase__tasks .cooking-task { + padding: var(--space-md); + } + + // Same reasoning for the stepper's own nodes: the enlarged 1.75rem + // (28px) marker below is this node's tallest element, so `--space-md` + // padding puts its total height at 28 + 2×16 = 60px, comfortably past + // 48dp rather than landing exactly on it. + .cooking-stepper__node { + padding: var(--space-md); + } + + .cooking-stepper__marker { + width: 1.75rem; + height: 1.75rem; + } + + // Tab bar, Recettes summary row, Prev/Next — same treatment, so every + // tappable control on this page ends up consistently comfortable rather + // than fixing only the one most obviously broken. + .cooking-session-page__tab { + padding: var(--space-md); + } + + .cooking-recipes__row { + padding: var(--space-md); + } + + .cooking-plan__nav-btn { + padding: var(--space-md) var(--space-lg); + } +}