Ouvrir le popover de correction depuis un highlight existant tombait sur la liste de choix (avec l'option "Aucune technique ici"), pas sur la vue metadonnees — pour voir/editer les ingredients/ustensiles deja rattaches a une technique correctement detectee, il fallait recliquer cette meme technique dans la liste, sans aucun indice que c'est ce qu'il fallait faire (rien ne la distingue des autres dans cette liste). Resultat cote utilisateur : la fonctionnalite de correction/edition de metadonnees etait techniquement presente mais invisible en pratique. TechStepCorrectionPopover demarre desormais selectionne sur `previousTechStepId` quand il est defini (un clic sur un highlight existant) — droit dans la vue Ingredients/Ustensiles, deja pre-remplie. "Changer" reste disponible pour rejoindre la liste complete (relabelliser ou supprimer la correspondance). Tests Cypress (component + e2e) mis a jour pour ce nouveau point d'entree par defaut. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
244 lines
9.2 KiB
TypeScript
244 lines
9.2 KiB
TypeScript
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 `<I18nextProvider>` 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 (
|
|
<div>
|
|
{/* 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. */}
|
|
<div data-testid="outside-popover" style={{ height: 20 }} />
|
|
<TechStepCorrectionPopover
|
|
recipeId={2}
|
|
stepId={2}
|
|
selectedText="Cuire"
|
|
range={{ start: 0, end: 5 }}
|
|
previousTechStepId={previousTechStepId}
|
|
// biome-ignore lint/suspicious/noExplicitAny: test harness stands in for real StepTechStepIngredientView/UtensilView props — precise typing isn't the point here.
|
|
existingIngredients={existingIngredients as any}
|
|
// biome-ignore lint/suspicious/noExplicitAny: see above.
|
|
existingUtensils={existingUtensils as any}
|
|
resolvedMetadataSpan={resolvedMetadataSpan}
|
|
onRequestSpan={(kind) =>
|
|
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}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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(<Harness />);
|
|
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(<Harness previousTechStepId={null} />);
|
|
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(<Harness previousTechStepId={cook.id} />);
|
|
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(<Harness />);
|
|
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(<Harness onSubmitted={onSubmitted} />);
|
|
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(<Harness />);
|
|
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(
|
|
<Harness
|
|
previousTechStepId={cook.id}
|
|
existingIngredients={[
|
|
{ ingredient: butter, quantity: 50, unit: gram, start: 0, end: 6, source: "auto" },
|
|
]}
|
|
existingUtensils={[{ utensil: pan, start: 14, end: 23, source: "auto" }]}
|
|
/>,
|
|
);
|
|
// 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(<Harness onClose={onClose} />);
|
|
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(<Harness onClose={onClose} />);
|
|
cy.wait("@getTechSteps");
|
|
|
|
cy.get('[data-testid="outside-popover"]').click();
|
|
|
|
cy.get("@onClose").should("have.been.calledOnce");
|
|
});
|
|
});
|