batchCooking/apps/web/cypress/e2e/shopping-list.ts
Nicolas 592933b1ac
All checks were successful
CI / lint (push) Successful in 2m18s
CI / build (push) Successful in 3m34s
CI / intent-service-test (push) Successful in 6m36s
CI / e2e (push) Successful in 10m22s
CI / test (push) Successful in 33m30s
CI / build-android (push) Has been skipped
feat(shopping-list): coche les articles « dans le panier » (#109)
La liste de courses était volontairement en lecture seule. Elle gagne un
état « acheté » par article, utile en magasin (au doigt sur mobile).

- `shopping-list-progress.ts` (nouveau) : `ShoppingListProgressStore`,
  calqué sur `CookingProgressStore` — `localStorage` uniquement, par
  semaine (clé `batchcooking:shoppingListChecked:<weekStart>`), aucun état
  serveur. Une coche n'est jamais autoritaire (la liste est recalculée
  depuis le planning à chaque visite) et se perd sans dommage.
- `ShoppingListPage.tsx` : chaque ligne devient un `CheckboxOption` ;
  `<ShoppingListItems key={weekKey}>` remonte à chaque changement de
  semaine (checklist réinitialisée). Clé d'article = `ingredientId-unitId`
  (déjà la `key` React de la ligne).
- `shopping-list-page.scss` : override scopé du look « carte sélectionnable »
  global — un article coché est barré et estompé, pas surligné en primary.
  Cible tactile ≥ 44 px sous le breakpoint phone.
- `specs/frontend-architecture.md` : la note « délibérément en lecture
  seule » est remplacée.

Tests : `shopping-list.feature` — nouveau scénario « cocher un article »
(coché → barré, persiste au reload, n'affecte pas les autres).
`shopping-list-page.cy.ts` — la cible tactile de la ligne (le label
`CheckboxOption`) fait 16 px de padding + ≥ 44 px de haut sur téléphone.
Local : `pnpm lint` + `pnpm --filter web build` OK, `shopping-list.feature`
4/4, `shopping-list-page.cy.ts` 1/1.

Closes #109

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 06:31:37 +02:00

96 lines
3.5 KiB
TypeScript

import { type DataTable, Given, Then, When } from "@badeball/cypress-cucumber-preprocessor";
/**
* One data-table row → a fake `ShoppingListItemView` — same minimal-fixture
* convention as `recipe-form.ts`'s ingredient fixtures (only the fields
* `ShoppingListPage` actually reads at runtime: the ingredient's `key`/
* `icon`/`category` for `IngredientTypeIcon`/`CategoryIcon`/translation, the
* unit's `key`; `id` only needs to be unique per row for the React list
* key). `index` seeds both ids so two rows never collide.
*/
function buildShoppingListItem(
row: { ingredientKey: string; icon: string; category: string; quantity: string; unitKey: string },
index: number,
) {
return {
ingredient: {
id: index,
key: row.ingredientKey,
icon: row.icon,
category: row.category,
subcategory: row.category,
reproducible: false,
allergens: [],
diets: [],
},
quantity: Number(row.quantity),
unit: { id: index, key: row.unitKey, type: "MASS", toBaseFactor: 1 },
};
}
Given("the shopping list for {string} is empty", (date: string) => {
cy.intercept("GET", `**/shopping-list?date=${date}`, {
statusCode: 200,
body: { startDate: date, finishDate: date, items: [] },
});
});
Given("the shopping list for {string} contains:", (date: string, dataTable: DataTable) => {
const items = dataTable.hashes().map((row, i) => buildShoppingListItem(row, i + 1));
cy.intercept("GET", `**/shopping-list?date=${date}`, {
statusCode: 200,
body: { startDate: date, finishDate: date, items },
});
});
// Same class as PlanningPage's own week navigator (`WeekNavigator`, now
// shared between the two pages) — `planning-page.cy.ts` already exercises
// the prev/next arrows directly by class, same approach here.
When("I click the next week arrow", () => {
cy.get(".week-nav__arrow").last().click();
});
Then(
"the shopping list group {string} should appear before {string}",
(first: string, second: string) => {
cy.get(".shopping-list__group-title").then(($titles) => {
const texts = [...$titles].map((el) => el.textContent?.trim() ?? "");
const firstIndex = texts.findIndex((text) => text.includes(first));
const secondIndex = texts.findIndex((text) => text.includes(second));
expect(firstIndex, `"${first}" should be a rendered group`).to.be.greaterThan(-1);
expect(secondIndex, `"${second}" should be a rendered group`).to.be.greaterThan(-1);
expect(firstIndex).to.be.lessThan(secondIndex);
});
},
);
Then(
"the shopping list should show {string} at quantity {string}",
(name: string, quantity: string) => {
cy.contains(".shopping-list__item", name).should("contain.text", quantity);
},
);
// Issue #109 — a line can be ticked off as "in the cart" (local, per-week,
// `localStorage` only — see `shopping-list-progress.ts`).
When("I tick the shopping list item {string}", (name: string) => {
cy.contains(".shopping-list__item", name).find("label.shopping-list__item-check").click();
});
Then("the shopping list item {string} should be ticked", (name: string) => {
cy.contains(".shopping-list__item", name)
.should("have.class", "shopping-list__item--checked")
.find('input[type="checkbox"]')
.should("be.checked");
});
Then("the shopping list item {string} should not be ticked", (name: string) => {
cy.contains(".shopping-list__item", name)
.should("not.have.class", "shopping-list__item--checked")
.find('input[type="checkbox"]')
.should("not.be.checked");
});
When("I reload the page", () => {
cy.reload();
});