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" }], }, ], }; /** * A separate, self-contained fixture for the "clicking a temperature * highlight" scenario — kept apart from `omeletteDetail` above rather than * adding a temperature to its "Cuire" occurrence, so this doesn't entangle * with that fixture's own "Corrects a detected technique" scenario (which * relabels the same occurrence without touching metadata, and must keep * meaning "nothing here changed"). */ const gratinDetail = { id: 4, name: "Gratin", description: null, picture: null, portions: 4, authorId: 1, visibility: "PERSONAL", allergens: [], diets: [], isFavorite: false, ingredients: [], steps: [ { id: 10, // "Préchauffez" = [0, 11), "à feu moyen" = [20, 31) — counted char by // char (accented/multi-byte characters here are each still one JS // string index, same convention `start`/`end` use everywhere else). description: "Préchauffez le four à feu moyen.", picture: null, order: 1, techSteps: [ { techStep: { id: 9, key: "preheat" }, start: 0, end: 11, source: "auto", temperatures: [ { celsius: null, gasMark: null, qualitative: "medium", start: 20, end: 31 }, ], }, ], }, ], }; 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"); }); Given("recipe 4's detail is available", () => { cy.intercept("GET", "**/recipes/4", { statusCode: 200, body: gratinDetail }); }); // TechStepCorrectionPopover fetches the full technique catalog on mount // (to resolve the chosen technique's own label) — a dedicated fixture // naming just `preheat` (`gratinDetail`'s own technique), kept separate // from "the tech steps reference list has options" above (that one's // `cook`/`simmer` pair is purpose-built for the *other* scenario's // relabel, not a generic catalog stand-in). Given("the tech steps reference list includes preheat", () => { cy.intercept("GET", "**/reference/tech-steps", { statusCode: 200, body: [{ id: 9, key: "preheat" }], }); }); // 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: [], temperatures: [], }, ], }, }).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"); }); Then("I should see the highlighted temperature {string}", (text: string) => { // Same "off the fold, needs an explicit scroll" reasoning as // common.steps.ts's "I should see the highlighted technique" — a bare // `.should("be.visible")` doesn't auto-scroll. cy.contains(".step-temperature", text).scrollIntoView().should("be.visible"); }); // Temperature is corrected through its owning technique's own popover, not // on its own — see `StepDescription.tsx`'s `findTechStepForTemperature`. When("I click the highlighted temperature {string}", (text: string) => { cy.contains(".step-temperature", text).click(); }); Then("the chosen technique should be {string}", (label: string) => { cy.contains(".tech-step-correction-popover__chosen-technique", label).should("be.visible"); }); // Picking a technique only *selects* it — it takes a separate "Valider" // click to actually submit (room was made for attaching ingredient/utensil // metadata alongside it, see `TechStepCorrectionPopover.tsx`'s own doc // comment on its merged editor) — folded into this one step since nothing // in this scenario cares about that intermediate state on its own. The // technique catalog is picked via the same searchable `CatalogSearchPicker` // the ingredient/utensil sub-flows use, scoped to // `__technique-section` since that same search-and-pick component is // reused inside this popover for more than just techniques. This step is // only ever used to *relabel* an existing match (this feature's one // scenario using it) — the picker hides behind "Changer" in that case // (nothing to decide once a technique's already confirmed correct, see // TechStepCorrectionPopover's own doc comment), so it's reopened first. When("I choose {string} as the correct technique", (label: string) => { cy.contains(".tech-step-correction-popover__technique-change", "Changer").click(); cy.contains( ".tech-step-correction-popover__technique-section .catalog-search-picker__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\/?$/); });