Etend le flux de correction existant (TechStepCorrectionPopover) pour que l'utilisateur associe lui-meme des ingredients (avec quantite/unite) et des ustensiles a la technique qu'il corrige, avec le meme marquage source: "manual" que la technique elle-meme. Backend : - submitTechStepCorrectionSchema (packages/shared) accepte des tableaux ingredients/utensils optionnels, chacun avec son propre span [start,end) selectionne par l'utilisateur. Omis = ne touche pas aux metadonnees existantes ; tableau (meme vide) = remplace tout ce qui existait sur cette occurrence (auto ET manuel precedent - decision validee avec l'utilisateur). - applyManualCorrection (recipe-tech-step-correction.service.ts) ecrit les nouvelles lignes StepTechStepIngredient/StepTechStepUtensil apres avoir vide celles de l'occurrence via deleteMany - meme chemin de code que ce soit une creation ou une mise a jour de la technique. - Nouveaux asserts d'existence (ingredient/unite/ustensile) + validation de span, nouveau code d'erreur UTENSIL_NOT_FOUND. - source ajoute a StepTechStepIngredientView/StepTechStepUtensilView (le calque manquait ce que la colonne DB portait deja). Frontend : - TechStepCorrectionPopover passe d'un clic = soumission immediate a un flux selection-puis-confirmation, avec deux nouvelles sections Ingredients/Ustensiles pre-remplies avec l'existant. - Ajouter un ingredient/ustensile demande une selection de texte dediee dans la description encore visible (StepDescription geree via un nouvel etat pendingSpanRequest/resolvedMetadataSpan) - pas de raccourci sur le span de la correction elle-meme. - Nouveau CatalogSearchPicker.tsx, plus leger que IngredientPicker pour ce contexte de popover, reutilise pour les deux catalogues. - getUtensils() ajoute a apiClient. Tests : nouveaux cas Mocha (attache/remplace/omission/validations) dans recipe-tech-step-correction.test.ts, TechStepCorrectionPopover.cy.tsx etendu avec le nouveau flux, recipes.ts (e2e) ajuste au clic Valider supplementaire. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
180 lines
6.1 KiB
TypeScript
180 lines
6.1 KiB
TypeScript
import { Given, Then, When } from "@badeball/cypress-cucumber-preprocessor";
|
|
|
|
const oeufs = { id: 2, key: "eggs", kind: "ALLERGY" };
|
|
|
|
const omelette = {
|
|
id: 2,
|
|
name: "Omelette",
|
|
description: null,
|
|
picture: null,
|
|
portions: 2,
|
|
authorId: 1,
|
|
visibility: "PERSONAL",
|
|
allergens: [oeufs],
|
|
diets: [],
|
|
isFavorite: false,
|
|
};
|
|
|
|
const omeletteDetail = {
|
|
...omelette,
|
|
description: "Une omelette toute simple.",
|
|
ingredients: [
|
|
{
|
|
ingredient: {
|
|
id: 10,
|
|
key: "egg",
|
|
icon: "EGG",
|
|
category: "dairyAndCheese",
|
|
subcategory: "eggs",
|
|
allergens: [oeufs],
|
|
diets: [],
|
|
},
|
|
quantity: 3,
|
|
unit: { id: 1, key: "piece", type: "COUNT", toBaseFactor: 1 },
|
|
},
|
|
],
|
|
steps: [
|
|
{ id: 1, description: "Battre les œufs.", picture: null, order: 1, techSteps: [] },
|
|
{
|
|
id: 2,
|
|
description: "Cuire à la poêle.",
|
|
picture: null,
|
|
order: 2,
|
|
// "Cuire" -> the `cook` technique, matching real reference-seed-data.ts
|
|
// (`\bcui(re|sez|sant|sson)\b`) — "poêle" itself matches nothing
|
|
// (that's `panFry`'s "sauter", a different word).
|
|
techSteps: [{ techStep: { id: 1, key: "cook" }, start: 0, end: 5, source: "auto" }],
|
|
},
|
|
],
|
|
};
|
|
|
|
Given("the disliked ingredients list is empty", () => {
|
|
cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] });
|
|
});
|
|
|
|
Given("the recipe catalog contains {string}", () => {
|
|
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [omelette] });
|
|
});
|
|
|
|
Given("recipe 2's detail is available", () => {
|
|
cy.intercept("GET", "**/recipes/2", { statusCode: 200, body: omeletteDetail }).as("getRecipe");
|
|
});
|
|
|
|
Given("toggling recipe 2's favorite will succeed", () => {
|
|
cy.intercept("POST", "**/recipes/2/favorite", { statusCode: 204 }).as("favorite");
|
|
});
|
|
|
|
Given("deleting recipe 2 will succeed", () => {
|
|
cy.intercept("DELETE", "**/recipes/2", { statusCode: 204 }).as("deleteRecipe");
|
|
});
|
|
|
|
// Step 2 is `omeletteDetail`'s "Cuire à la poêle." step, whose only
|
|
// existing match is `cook` (id 1) — see that fixture above. The response
|
|
// mirrors `SubmitTechStepCorrectionResult` (packages/shared): the audit
|
|
// record (reassigning the match to `simmer`, id 3, "Mijoter" — see `the
|
|
// tech steps reference list has options`, reference-data.steps.ts) plus
|
|
// the step's fresh `techSteps`, now showing that same reassignment as a
|
|
// `"manual"`-sourced entry — the API applies a correction immediately, it
|
|
// doesn't just record it (see `StepTechStepView.source`'s own doc comment).
|
|
Given('correcting step 2\'s "Cuire" match will succeed', () => {
|
|
cy.intercept("POST", "**/recipes/2/steps/2/corrections", {
|
|
statusCode: 201,
|
|
body: {
|
|
correction: {
|
|
id: 1,
|
|
start: 0,
|
|
end: 5,
|
|
previousTechStep: { id: 1, key: "cook" },
|
|
correctedTechStep: { id: 3, key: "simmer" },
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
techSteps: [
|
|
{
|
|
techStep: { id: 3, key: "simmer" },
|
|
start: 0,
|
|
end: 5,
|
|
source: "manual",
|
|
ingredients: [],
|
|
utensils: [],
|
|
},
|
|
],
|
|
},
|
|
}).as("correction");
|
|
});
|
|
|
|
When("I click the highlighted technique {string}", (text: string) => {
|
|
cy.contains(".step-tech-step", text).click();
|
|
});
|
|
|
|
Then("I should see the technique correction options", () => {
|
|
cy.get(".tech-step-correction-popover").should("be.visible");
|
|
});
|
|
|
|
// Picking a technique only *selects* it now — it takes a separate
|
|
// "Valider" click to actually submit (room was made for attaching
|
|
// ingredient/utensil metadata first, see `TechStepCorrectionPopover.tsx`'s
|
|
// own doc comment) — folded into this one step since nothing in this
|
|
// scenario cares about that intermediate state on its own.
|
|
When("I choose {string} as the correct technique", (label: string) => {
|
|
cy.contains(".tech-step-correction-popover__list button", label).click();
|
|
cy.contains(".tech-step-correction-popover__confirm-button", "Valider").click();
|
|
});
|
|
|
|
Then(
|
|
"the highlighted technique {string} should be marked as a manual correction",
|
|
(text: string) => {
|
|
cy.contains(".step-tech-step", text).should("have.class", "step-tech-step--manual");
|
|
},
|
|
);
|
|
|
|
Then("the correction request should have been made", () => {
|
|
// Asserts the actual span, not just that *a* request fired — a real bug
|
|
// (StepDescription.tsx's click handler reading a shared, still-mutating
|
|
// `offset` variable by reference instead of a value captured at render
|
|
// time) once sent `end` all the way to the end of the description
|
|
// instead of "Cuire"'s own tight [0, 5) span, and a request-fired-only
|
|
// assertion here didn't catch it — found only via manual testing.
|
|
cy.wait("@correction")
|
|
.its("request.body")
|
|
.should("deep.include", { start: 0, end: 5, previousTechStepId: 1 });
|
|
});
|
|
|
|
Then("the recipe {string} should not be visible in the table", (name: string) => {
|
|
cy.contains(".recipe-table__name", name).should("not.exist");
|
|
});
|
|
|
|
Then("the recipe {string} should be marked as favorite", (name: string) => {
|
|
cy.contains(".recipe-table__name", name).find(".recipe-table__fav-mark").should("exist");
|
|
});
|
|
|
|
Then("the recipe {string} should not be marked as favorite", (name: string) => {
|
|
cy.contains(".recipe-table__name", name).find(".recipe-table__fav-mark").should("not.exist");
|
|
});
|
|
|
|
When("I click the favorite star", () => {
|
|
cy.get(".favorite-star-button").click();
|
|
});
|
|
|
|
Then("the favorite request should have been made", () => {
|
|
cy.wait("@favorite");
|
|
});
|
|
|
|
Then("the favorite star should be marked as favorite", () => {
|
|
cy.get(".favorite-star-button").should("have.class", "is-favorite");
|
|
});
|
|
|
|
When("I click {string} in the recipe detail panel", (text: string) => {
|
|
cy.contains(".recipe-detail-panel__danger-button", text).click();
|
|
});
|
|
|
|
When("I confirm the deletion in the recipe detail panel", () => {
|
|
cy.contains(".recipe-detail-panel__danger-button", "Confirmer la suppression").click();
|
|
});
|
|
|
|
Then("the delete request should have been made", () => {
|
|
cy.wait("@deleteRecipe");
|
|
});
|
|
|
|
Then("the URL should match the recipes list", () => {
|
|
cy.url().should("match", /\/recettes\/?$/);
|
|
});
|