Les corrections utilisateur (via TechStepCorrectionPopover) sont
désormais écrites directement dans StepTechStep, avec une colonne
`source` ("auto" | "manual") qui les distingue des matches du
classifieur NLP :
- Migration `step_tech_step_source` ajoutant `source` (défaut "auto")
- `applyManualCorrection`/`renumberStepTechSteps` dans
recipe-tech-step-correction.service.ts : une correction met à jour
ou crée l'entrée StepTechStep concernée (source "manual"), la
réponse de l'endpoint inclut désormais le techSteps à jour du step
(SubmitTechStepCorrectionResult), pas seulement l'audit de
correction
- backfill-tech-steps.ts préserve les entrées "manual" existantes :
seules les entrées "auto" sont recalculées, et un nouveau match
auto chevauchant une correction manuelle est ignoré plutôt
qu'inséré en doublon — vérifié en base réelle (une correction
manuelle survit intacte à un backfill complet)
- Le front distingue visuellement les deux (StepDescription.tsx,
recipes.scss : `.step-tech-step--manual`, couleur Turmeric au lieu
de Basil), avec un tooltip "(correction manuelle)" et un indicateur
de découvrabilité de la fonctionnalité dans RecipeDetailPanel
Corrige aussi deux bugs trouvés en testant en conditions réelles :
- StepDescription.tsx : le clic sur un highlight existant lisait la
variable `offset` (mutable, partagée par la boucle) au lieu d'une
valeur capturée, envoyant un `end` erroné (fin de la description
entière au lieu du span du mot cliqué)
- backfill-tech-steps.ts : le garde `import.meta.url ===
file://${process.argv[1]}` ne matche jamais sur Windows (chemins à
antislash), le script ne faisait donc rien en exécution directe ;
remplacé par `pathToFileURL(process.argv[1]).href`
335 tests apps/api passants, 40/40 composants Cypress, 75/76 e2e
Cypress (1 flake pré-existant sans rapport, non touché ici).
165 lines
5.5 KiB
TypeScript
165 lines
5.5 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" }],
|
|
},
|
|
}).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");
|
|
});
|
|
|
|
When("I choose {string} as the correct technique", (label: string) => {
|
|
cy.contains(".tech-step-correction-popover__list button", label).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\/?$/);
|
|
});
|