diff --git a/apps/web/cypress/component/TechStepCorrectionPopover.cy.tsx b/apps/web/cypress/component/TechStepCorrectionPopover.cy.tsx index 2eaf71c..ee7a14f 100644 --- a/apps/web/cypress/component/TechStepCorrectionPopover.cy.tsx +++ b/apps/web/cypress/component/TechStepCorrectionPopover.cy.tsx @@ -16,6 +16,9 @@ const butter = { id: 10, key: "butter" }; const pan = { id: 20, key: "pan" }; const gram = { id: 30, key: "gram" }; +/** Long enough that `range={start:0,end:5}` ("Cuire") and a resolved metadata span at `[20,26)` ("Beurre") both fall within it — see {@link Harness}'s fixed `onRequestSpan` stub. */ +const DESCRIPTION = "Cuire un peu de beurre."; + /** * A real `StepDescription` resolves `onRequestSpan` into a fresh * `resolvedMetadataSpan` via an actual browser text selection — out of @@ -29,18 +32,20 @@ function Harness({ previousTechStepId = null, existingIngredients = [], existingUtensils = [], + existingTemperatures = [], onClose = () => {}, onSubmitted = () => {}, }: Partial<{ previousTechStepId: number | null; existingIngredients: unknown[]; existingUtensils: unknown[]; + existingTemperatures: unknown[]; onClose: () => void; onSubmitted: (result: unknown) => void; }>) { const [resolvedMetadataSpan, setResolvedMetadataSpan] = useState<{ nonce: number; - kind: "ingredient" | "utensil"; + kind: "ingredient" | "utensil" | "temperature"; range: { start: number; end: number }; text: string; } | null>(null); @@ -52,13 +57,16 @@ function Harness({ setResolvedMetadataSpan({ @@ -90,34 +98,40 @@ describe("TechStepCorrectionPopover", () => { ); }); - it("shows the selected text, the technique catalog (searchable) and the metadata sections all together", () => { + it("shows the selected text, the technique picker (searchable) and all three metadata sections together", () => { cy.mount(); cy.wait("@getTechSteps"); cy.contains(".tech-step-correction-popover__selection", "Cuire").should("be.visible"); // Merged editor (see TechStepCorrectionPopover's own doc comment) — no - // separate "pick, then metadata reveals itself" step, both render at - // once, and the technique catalog goes through the same searchable - // `CatalogSearchPicker` as the ingredient/utensil sub-flows (a plain - // unfiltered list of the real ~74-entry catalog isn't browsable). + // separate "pick, then metadata reveals itself" step, all four + // sections render at once, and the technique catalog goes through the + // same searchable `CatalogSearchPicker` as the ingredient/utensil + // sub-flows (a plain unfiltered list of the real ~74-entry catalog + // isn't browsable) — visible by default since nothing's picked yet. cy.get( ".tech-step-correction-popover__technique-section .catalog-search-picker__list button", ).should("have.length", 2); - cy.contains("h4", "Ingrédients").should("be.visible"); - cy.contains("h4", "Ustensiles").should("be.visible"); + cy.contains(".tech-step-correction-popover__section-title", "Ingrédients").should("be.visible"); + cy.contains(".tech-step-correction-popover__section-title", "Ustensiles").should("be.visible"); + cy.contains(".tech-step-correction-popover__section-title", "Température").should("be.visible"); cy.contains("button", "Valider").should("be.visible"); }); it("offers a 'no technique here' option, and marks the current pick, only when correcting an existing match", () => { cy.mount(); cy.wait("@getTechSteps"); - cy.get(".tech-step-correction-popover__remove").should("not.exist"); + cy.get(".tech-step-correction-popover__remove-match").should("not.exist"); cy.contains(".tech-step-correction-popover__chosen-technique", "Aucune technique sélectionnée"); cy.mount(); cy.wait("@getTechSteps"); - cy.get(".tech-step-correction-popover__remove").should("exist"); + cy.get(".tech-step-correction-popover__remove-match").should("exist"); cy.contains(".tech-step-correction-popover__chosen-technique", "Cuire"); + // The picker itself hides behind "Changer" once something's already + // picked (see TechStepCorrectionPopover's own doc comment) — reopened + // here just to confirm it still marks the current pick correctly. + cy.contains("button", "Changer").click(); cy.contains(".catalog-search-picker__list button", "Cuire").should( "have.class", "catalog-search-picker__item--selected", @@ -149,7 +163,7 @@ describe("TechStepCorrectionPopover", () => { cy.contains("button", "Valider").should("not.be.disabled"); }); - it("submits the selected technique (no metadata touched) with ingredients/utensils omitted from the request", () => { + it("submits the selected technique (no metadata touched) with ingredients/utensils/temperatures omitted from the request", () => { cy.intercept("POST", "**/recipes/2/steps/2/corrections", { statusCode: 201, body: { @@ -200,14 +214,16 @@ describe("TechStepCorrectionPopover", () => { ".tech-step-correction-popover__technique-section .catalog-search-picker__list button", "Mijoter", ).click(); - cy.contains("button", "+ Ajouter un ingrédient").click(); + cy.contains("button", "Ajouter un ingrédient").click(); cy.contains(".catalog-search-picker button", "Beurre").click(); cy.get('input[type="number"]').type("50"); cy.get("select").select(String(gram.id)); - cy.contains("button", "Ajouter").click(); + cy.contains(".tech-step-correction-popover__add-form button", "Ajouter").click(); - cy.contains(".tech-step-correction-popover__chip", "50 g Beurre").should("be.visible"); + cy.contains(".tech-step-correction-popover__row", "Beurre") + .should("contain.text", "Beurre") + .and("contain.text", "50 g"); cy.contains("button", "Valider").click(); cy.wait("@submitCorrection") @@ -221,28 +237,85 @@ describe("TechStepCorrectionPopover", () => { { ingredientId: butter.id, quantity: 50, unitId: gram.id, start: 20, end: 26 }, ], utensils: [], + temperatures: [], }); }); - it("pre-seeds existing ingredients/utensils, removable via their own chip", () => { + it("adds a temperature via the span-selection flow and its 3-way mode switch, included in the submitted request", () => { + cy.intercept("POST", "**/recipes/2/steps/2/corrections", { + statusCode: 201, + body: { + id: 1, + start: 0, + end: 5, + previousTechStep: null, + correctedTechStep: simmer, + createdAt: new Date().toISOString(), + }, + }).as("submitCorrection"); + cy.mount(); + cy.wait("@getTechSteps"); + + cy.contains( + ".tech-step-correction-popover__technique-section .catalog-search-picker__list button", + "Mijoter", + ).click(); + cy.contains("button", "Ajouter une température").click(); + + // Defaults to the qualitative mode — switching to °C swaps the input + // shown, never both at once (mutually exclusive, see + // `manualStepTechStepTemperatureInputSchema`'s own doc comment, + // packages/shared). + cy.contains(".tech-step-correction-popover__mode-switch button", "°C").click(); + cy.get('.tech-step-correction-popover__value-line input[type="number"]').type("180"); + cy.contains(".tech-step-correction-popover__add-form button", "Ajouter").click(); + + cy.contains(".tech-step-correction-popover__row", "180") + .should("contain.text", "°C") + .and("contain.text", "Beurre"); // the (stubbed) source quote + cy.contains("button", "Valider").click(); + + cy.wait("@submitCorrection") + .its("request.body") + .should("deep.equal", { + start: 0, + end: 5, + previousTechStepId: null, + correctedTechStepId: simmer.id, + ingredients: [], + utensils: [], + temperatures: [{ celsius: 180, gasMark: null, qualitative: null, start: 20, end: 26 }], + }); + }); + + it("pre-seeds existing ingredients/utensils/temperatures, each removable via its own row", () => { cy.mount( , ); // An existing match starts pre-selected on itself (see - // TechStepCorrectionPopover's own doc comment) — the metadata sections, - // pre-seeded from `existingIngredients`/`existingUtensils`, are visible - // immediately, no need to re-pick "Cuire" from a list first. + // TechStepCorrectionPopover's own doc comment) — the metadata + // sections, pre-seeded from `existingIngredients`/`existingUtensils`/ + // `existingTemperatures`, are visible immediately, no need to re-pick + // "Cuire" from a list first. cy.wait("@getTechSteps"); cy.wait(["@getIngredients", "@getUnits", "@getUtensils"]); - cy.contains(".tech-step-correction-popover__chip", "50 g Beurre").find("button").click(); - cy.contains(".tech-step-correction-popover__chip", "Beurre").should("not.exist"); + cy.contains(".tech-step-correction-popover__row", "Beurre").should("be.visible"); + cy.contains(".tech-step-correction-popover__row", "Feu doux").should("be.visible"); + + cy.contains(".tech-step-correction-popover__row", "Beurre") + .find(".tech-step-correction-popover__row-remove") + .click(); + cy.contains(".tech-step-correction-popover__row", "Beurre").should("not.exist"); }); it("shows an error message and stays open when the submission fails", () => { diff --git a/apps/web/cypress/e2e/recipes.feature b/apps/web/cypress/e2e/recipes.feature index 29914e2..8ccdeb8 100644 --- a/apps/web/cypress/e2e/recipes.feature +++ b/apps/web/cypress/e2e/recipes.feature @@ -45,6 +45,21 @@ Feature: Managing a recipe from the catalog When I focus the highlighted technique "Cuire" Then the tooltip should show "Mijoter (correction manuelle)" + Scenario: Clicking a highlighted temperature reopens its technique's correction popover + # Same master-detail room requirement as recipes.cy.ts's own equivalent + # test — Cypress's default 1000×660 doesn't leave the detail panel + # (with the popover open below its step) enough height to show + # everything without its own scroll. + Given the viewport is 1600 by 900 + And the recipe catalog contains "Gratin" + And recipe 4's detail is available + And the tech steps reference list includes preheat + When I visit "/recettes/4" + Then I should see the highlighted temperature "feu moyen" + When I click the highlighted temperature "feu moyen" + Then I should see the technique correction options + And the chosen technique should be "Préchauffer" + Scenario: Deletes a recipe after a two-step confirmation, then clears the selection Given the recipe catalog contains "Omelette" And recipe 2's detail is available diff --git a/apps/web/cypress/e2e/recipes.ts b/apps/web/cypress/e2e/recipes.ts index 206e4fb..fdf7eea 100644 --- a/apps/web/cypress/e2e/recipes.ts +++ b/apps/web/cypress/e2e/recipes.ts @@ -48,6 +48,50 @@ const omeletteDetail = { ], }; +/** + * 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: [] }); }); @@ -68,6 +112,23 @@ 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 @@ -96,6 +157,7 @@ Given('correcting step 2\'s "Cuire" match will succeed', () => { source: "manual", ingredients: [], utensils: [], + temperatures: [], }, ], }, @@ -110,6 +172,23 @@ 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 @@ -118,8 +197,13 @@ Then("I should see the technique correction options", () => { // 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. +// 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, diff --git a/apps/web/src/features/recipes/recipes.scss b/apps/web/src/features/recipes/recipes.scss index 11543d9..de13290 100644 --- a/apps/web/src/features/recipes/recipes.scss +++ b/apps/web/src/features/recipes/recipes.scss @@ -673,7 +673,10 @@ text-decoration: underline dotted var(--color-primary); text-decoration-thickness: 1px; text-underline-offset: 0.15em; - cursor: help; + // A real clickable trigger (opens `TechStepCorrectionPopover`), not just + // a hover-for-a-tooltip target — `pointer`, not `help`, same reasoning + // now shared by `.step-temperature` below. + cursor: pointer; &:hover, &:focus-visible { @@ -702,17 +705,31 @@ // `--color-primary`, so a temperature reads as its own kind of metadata at // a glance. `--low`/`--medium`/`--high` only vary the underline weight — // the qualitative scale is a nuance, not three different colors. +// +// Renders as a ` ); } @@ -263,11 +329,13 @@ export function StepDescription({