import { useState } from "react"; import "../../src/i18n/i18n"; import { TechStepCorrectionPopover } from "../../src/features/recipes/steps/TechStepCorrectionPopover"; // Mounts the popover in isolation (no StepDescription/selection plumbing // around it) — same "generic component test" posture as CheckboxOption.cy.tsx, // but this one needs `../../src/i18n/i18n` imported for its side effect // (initializes the default i18next instance `useTranslation` falls back to // with no `` in the tree — see that module's own doc // comment) since, unlike Checkbox/Radio, this component calls // `useTranslation()`. const cook = { id: 1, key: "cook" }; const simmer = { id: 3, key: "simmer" }; const butter = { id: 10, key: "butter" }; const pan = { id: 20, key: "pan" }; const gram = { id: 30, key: "gram" }; /** * A real `StepDescription` resolves `onRequestSpan` into a fresh * `resolvedMetadataSpan` via an actual browser text selection — out of * scope for a component test of the popover alone (covered by the e2e * scenario instead). This harness fakes that round-trip with a fixed * span, so tests here can exercise everything the popover itself is * responsible for once a span comes back, without needing a real * `StepDescription` in the tree. */ function Harness({ previousTechStepId = null, existingIngredients = [], existingUtensils = [], onClose = () => {}, onSubmitted = () => {}, }: Partial<{ previousTechStepId: number | null; existingIngredients: unknown[]; existingUtensils: unknown[]; onClose: () => void; onSubmitted: (result: unknown) => void; }>) { const [resolvedMetadataSpan, setResolvedMetadataSpan] = useState<{ nonce: number; kind: "ingredient" | "utensil"; range: { start: number; end: number }; text: string; } | null>(null); return (
{/* A genuinely separate sibling to click for the "outside click closes it" test — clicking blindly at a viewport coordinate would risk still landing inside the popover, which fills most of the mounted area on its own. */}
setResolvedMetadataSpan({ nonce: Date.now(), kind, range: { start: 20, end: 26 }, text: "Beurre", }) } onClose={onClose} // biome-ignore lint/suspicious/noExplicitAny: see above. onSubmitted={onSubmitted as any} />
); } describe("TechStepCorrectionPopover", () => { beforeEach(() => { cy.intercept("GET", "**/reference/tech-steps", { statusCode: 200, body: [cook, simmer] }).as( "getTechSteps", ); cy.intercept("GET", "**/reference/ingredients", { statusCode: 200, body: [butter] }).as( "getIngredients", ); cy.intercept("GET", "**/reference/units", { statusCode: 200, body: [gram] }).as("getUnits"); cy.intercept("GET", "**/reference/utensils", { statusCode: 200, body: [pan] }).as( "getUtensils", ); }); it("shows the selected text and every technique option once loaded", () => { cy.mount(); cy.wait("@getTechSteps"); cy.contains(".tech-step-correction-popover__selection", "Cuire").should("be.visible"); cy.get(".tech-step-correction-popover__list button").should("have.length", 2); }); it("offers a 'no technique here' option only when correcting an existing match", () => { cy.mount(); cy.wait("@getTechSteps"); cy.get(".tech-step-correction-popover__remove").should("not.exist"); // An existing match starts pre-selected straight into the metadata view // (see TechStepCorrectionPopover's own doc comment on why) — "Changer" // reaches the pick list, where the remove option lives. cy.mount(); cy.wait("@getTechSteps"); cy.get(".tech-step-correction-popover__remove").should("not.exist"); cy.contains("button", "Changer").click(); cy.get(".tech-step-correction-popover__remove").should("exist"); }); it("selecting a technique reveals the Ingrédients/Ustensiles sections instead of submitting immediately", () => { cy.mount(); cy.wait("@getTechSteps"); cy.contains(".tech-step-correction-popover__list button", "Mijoter").click(); cy.get(".tech-step-correction-popover__list").should("not.exist"); cy.contains("h4", "Ingrédients").should("be.visible"); cy.contains("h4", "Ustensiles").should("be.visible"); cy.contains("button", "Valider").should("be.visible"); }); it("submits the selected technique (no metadata touched) with ingredients/utensils omitted from the 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"); const onSubmitted = cy.stub().as("onSubmitted"); cy.mount(); cy.wait("@getTechSteps"); cy.contains(".tech-step-correction-popover__list button", "Mijoter").click(); cy.contains("button", "Valider").click(); cy.wait("@submitCorrection").its("request.body").should("deep.equal", { start: 0, end: 5, previousTechStepId: null, correctedTechStepId: simmer.id, }); cy.get("@onSubmitted").should("have.been.calledOnce"); }); it("adds an ingredient with quantity/unit via the span-selection flow, 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__list button", "Mijoter").click(); cy.contains("button", "+ Ajouter un ingrédient").click(); cy.wait(["@getIngredients", "@getUnits", "@getUtensils"]); 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__chip", "50 g Beurre").should("be.visible"); cy.contains("button", "Valider").click(); cy.wait("@submitCorrection") .its("request.body") .should("deep.equal", { start: 0, end: 5, previousTechStepId: null, correctedTechStepId: simmer.id, ingredients: [ { ingredientId: butter.id, quantity: 50, unitId: gram.id, start: 20, end: 26 }, ], utensils: [], }); }); it("pre-seeds existing ingredients/utensils, removable via their own chip", () => { 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. 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"); }); it("shows an error message and stays open when the submission fails", () => { cy.intercept("POST", "**/recipes/2/steps/2/corrections", { statusCode: 404, body: { code: 4051, message: "TechStep not found" }, }).as("submitCorrection"); const onClose = cy.stub().as("onClose"); cy.mount(); cy.wait("@getTechSteps"); cy.contains(".tech-step-correction-popover__list button", "Cuire").click(); cy.contains("button", "Valider").click(); cy.wait("@submitCorrection"); cy.get(".field-error").should("be.visible"); cy.get("@onClose").should("not.have.been.called"); }); it("calls onClose on an outside click", () => { const onClose = cy.stub().as("onClose"); cy.mount(); cy.wait("@getTechSteps"); cy.get('[data-testid="outside-popover"]').click(); cy.get("@onClose").should("have.been.calledOnce"); }); });