All checks were successful
`RecipeIngredient.quantity`/`unitId` deviennent nullable (migration purement additive) : une ligne de recette peut n'avoir ni quantité ni unité — sel, poivre… ajoutés selon le goût. Le schéma partagé impose « quantité et unité, ou aucune des deux » ; `toRecipeView` renvoie `null`/`null`. `aggregateShoppingList` fait apparaître une telle ligne une seule fois, sans quantité ni unité, jamais sommée ni mise à l'échelle. `ShoppingListPage` affiche « selon le goût » à la place de la quantité. Formulaire : `IngredientRow` gagne une case « Selon le goût » qui masque quantité/unité ; `RecipeFormPage` et `RecipeImportForm` n'exigent plus quantité/unité pour ces lignes et les envoient sans. Un import dont la source laisse une ligne résolue sans quantité (« salt to taste ») arrive directement en « selon le goût ». Le chemin clause-de-technique (session de cuisine) tolérait déjà `quantity` null — inchangé. Tests : Mocha (création/édition d'une ligne sans quantité, rejet quantité-sans-unité, liste de courses non sommée) ; Cypress (case à cocher + parcours), régression recette/liste de courses OK. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
128 lines
4.4 KiB
TypeScript
128 lines
4.4 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 },
|
|
});
|
|
});
|
|
|
|
// A "selon le goût" line — `quantity`/`unit` both null (issue #128). Its own
|
|
// step rather than a `buildShoppingListItem` data-table column so that
|
|
// helper's `quantity`/`unitKey` contract stays required for every other row.
|
|
Given(
|
|
"the shopping list for {string} has a « selon le goût » line for {string}",
|
|
(date: string, ingredientKey: string) => {
|
|
cy.intercept("GET", `**/shopping-list?date=${date}`, {
|
|
statusCode: 200,
|
|
body: {
|
|
startDate: date,
|
|
finishDate: date,
|
|
items: [
|
|
{
|
|
ingredient: {
|
|
id: 1,
|
|
key: ingredientKey,
|
|
icon: "JAR",
|
|
category: "condimentsAndSpices",
|
|
subcategory: "spices",
|
|
reproducible: false,
|
|
allergens: [],
|
|
diets: [],
|
|
},
|
|
quantity: null,
|
|
unit: null,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
},
|
|
);
|
|
|
|
// 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();
|
|
});
|