All checks were successful
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>
96 lines
3.5 KiB
TypeScript
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();
|
|
});
|