[UI mobile] Liste de courses et catalogue de recettes : renforcement mobile #99
4 changed files with 140 additions and 0 deletions
|
|
@ -5,6 +5,8 @@
|
||||||
// The favorite-toggle and delete-recipe journeys moved to recipes.feature —
|
// The favorite-toggle and delete-recipe journeys moved to recipes.feature —
|
||||||
// this file now only covers catalog browsing/display.
|
// this file now only covers catalog browsing/display.
|
||||||
|
|
||||||
|
import { PHONE_VIEWPORT } from "../support/viewports";
|
||||||
|
|
||||||
const authenticatedProfile = {
|
const authenticatedProfile = {
|
||||||
id: 1,
|
id: 1,
|
||||||
firstName: "Alice",
|
firstName: "Alice",
|
||||||
|
|
@ -460,3 +462,38 @@ describe("Recipe catalog", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ticket #90 — `.recipe-table-wrap` had no horizontal-scroll guard at all
|
||||||
|
// (unlike the planning grid's own wrapper), so a narrow viewport just let
|
||||||
|
// the browser compress every column instead of scrolling. Below the
|
||||||
|
// tablet breakpoint (1024px) the master-detail split already collapses to
|
||||||
|
// one column (recipes.scss's own `bp.tablet-down` rule) and gives the
|
||||||
|
// table the full page width — still comfortably under `.recipe-table`'s
|
||||||
|
// own 36rem floor on a phone, which is what this covers.
|
||||||
|
describe("Recipe catalog (phone)", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.setViewport("phone");
|
||||||
|
interceptAuth();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the table on its own horizontal scroll, at full width, instead of squeezing its columns", () => {
|
||||||
|
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [ratatouille, omelette] });
|
||||||
|
|
||||||
|
cy.visit("/recettes");
|
||||||
|
|
||||||
|
cy.contains(".recipe-table__name", "Ratatouille").should("be.visible");
|
||||||
|
cy.contains(".recipe-table__name", "Omelette").should("be.visible");
|
||||||
|
|
||||||
|
cy.get(".recipe-table-wrap").should(($el) => {
|
||||||
|
const s = getComputedStyle($el[0]);
|
||||||
|
expect(s.overflowX).to.be.oneOf(["auto", "scroll"]);
|
||||||
|
expect($el[0].scrollWidth).to.be.greaterThan($el[0].clientWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ...and that overflow stays *inside* the wrapper — same global rule
|
||||||
|
// every other page is held to.
|
||||||
|
cy.document()
|
||||||
|
.its("documentElement.scrollWidth")
|
||||||
|
.should("be.lte", PHONE_VIEWPORT.width + 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
67
apps/web/cypress/e2e/shopping-list-page.cy.ts
Normal file
67
apps/web/cypress/e2e/shopping-list-page.cy.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Plain layout-mode spec (not Gherkin) for ShoppingListPage's own
|
||||||
|
// phone-viewport pass (issue #90) — the actual grouped-list rendering,
|
||||||
|
// aggregation and week-navigation journey is already covered end-to-end by
|
||||||
|
// shopping-list.feature; this only exercises the responsive change added
|
||||||
|
// here. Mocks the API via cy.intercept — this job doesn't run a live
|
||||||
|
// backend (see .github/workflows/ci.yml).
|
||||||
|
|
||||||
|
import { PHONE_VIEWPORT } from "../support/viewports";
|
||||||
|
|
||||||
|
const authenticatedProfile = {
|
||||||
|
id: 1,
|
||||||
|
firstName: "Alice",
|
||||||
|
lastName: "Martin",
|
||||||
|
email: "alice@example.com",
|
||||||
|
tokenVersion: 0,
|
||||||
|
houseId: 1,
|
||||||
|
dietId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Same minimal-fixture convention as shopping-list.ts's own `buildShoppingListItem` step-def helper — only the fields this page actually reads. */
|
||||||
|
function shoppingListItem(id: number, ingredientKey: string, icon: string, category: string) {
|
||||||
|
return {
|
||||||
|
ingredient: {
|
||||||
|
id,
|
||||||
|
key: ingredientKey,
|
||||||
|
icon,
|
||||||
|
category,
|
||||||
|
subcategory: category,
|
||||||
|
reproducible: false,
|
||||||
|
allergens: [],
|
||||||
|
diets: [],
|
||||||
|
},
|
||||||
|
quantity: 2,
|
||||||
|
unit: { id, key: "piece", type: "COUNT", toBaseFactor: 1 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Shopping list page (phone)", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.setViewport("phone");
|
||||||
|
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gives each row a taller, more legible padding than on desktop, with no horizontal body scroll", () => {
|
||||||
|
cy.intercept("GET", /\/shopping-list\?/, {
|
||||||
|
statusCode: 200,
|
||||||
|
body: {
|
||||||
|
startDate: "2026-08-17",
|
||||||
|
finishDate: "2026-08-23",
|
||||||
|
items: [shoppingListItem(1, "egg", "EGG", "dairyAndCheese")],
|
||||||
|
},
|
||||||
|
}).as("getList");
|
||||||
|
|
||||||
|
cy.visit("/liste-de-courses");
|
||||||
|
cy.wait("@getList");
|
||||||
|
|
||||||
|
cy.get(".shopping-list__item").should(($el) => {
|
||||||
|
// --space-md (16px) on phone vs. the desktop default --space-sm
|
||||||
|
// (8px) — see shopping-list-page.scss's phone block.
|
||||||
|
expect(getComputedStyle($el[0]).paddingTop).to.equal("16px");
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.document()
|
||||||
|
.its("documentElement.scrollWidth")
|
||||||
|
.should("be.lte", PHONE_VIEWPORT.width + 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -260,6 +260,17 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
// Issue #90: unlike the planning grid's own wrapper (planning-page.scss),
|
||||||
|
// this one had no horizontal guard at all — on a narrow viewport the
|
||||||
|
// table's `width: 100%` below just let the browser compress every column
|
||||||
|
// (photo/name/allergens/diets) to fit instead of scrolling. Same fix,
|
||||||
|
// scaled down: 4 narrower columns don't need anywhere near the planning
|
||||||
|
// grid's 62rem, but they do need *some* floor so the badge columns don't
|
||||||
|
// get squeezed to the point of wrapping every single pill onto its own
|
||||||
|
// line. Shared by both the full catalog (`/recettes`) and
|
||||||
|
// `RecipePickerDialog`'s own instance of this same table/wrapper — fixes
|
||||||
|
// the identical compression there too, not just the full-page case.
|
||||||
|
overflow-x: auto;
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
|
@ -267,6 +278,7 @@
|
||||||
|
|
||||||
.recipe-table {
|
.recipe-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 36rem;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
// `__status`), a simple grouped list rather than a grid below it.
|
// `__status`), a simple grouped list rather than a grid below it.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
@use "../../styles/breakpoints" as bp;
|
||||||
|
|
||||||
.shopping-list-page {
|
.shopping-list-page {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -100,3 +102,25 @@
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Phone (issue #90) ------------------------------------------------------
|
||||||
|
// This list has no checkbox/delete button at all — it's deliberately
|
||||||
|
// read-only (see ShoppingListPage.tsx's own doc comment) — so, unlike the
|
||||||
|
// planning grid or the cooking-session task cards, there's no *tap* target
|
||||||
|
// to enlarge here; the chantier's original ticket assumed some existed
|
||||||
|
// (written before this page's actual code was read). The structure also
|
||||||
|
// already doesn't overflow: `&-name` is `flex: 1; min-width: 0` and wraps
|
||||||
|
// rather than pushing `&-quantity` out. What phone-viewport reading
|
||||||
|
// genuinely rewards here — held in one hand, glanced at repeatedly while
|
||||||
|
// pushing a cart — is a slightly roomier row and a clearer icon, not a
|
||||||
|
// structural change.
|
||||||
|
@include bp.phone {
|
||||||
|
.shopping-list__item {
|
||||||
|
padding: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shopping-list__item-icon svg {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue