Compare commits
No commits in common. "f01ac58fecb2c57fa30350de977cebc3f6aab87d" and "ec26f32dd8ca27f62d19d300f87caa551be07c7d" have entirely different histories.
f01ac58fec
...
ec26f32dd8
3 changed files with 5 additions and 121 deletions
|
|
@ -127,45 +127,6 @@ const tarteDetail = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* A one-step recipe whose single technique match carries no attached
|
|
||||||
* metadata yet — for the issue #36 regression test below, which needs a
|
|
||||||
* clean correction popover and a description word ("oignons") that appears
|
|
||||||
* *only* in the step text, nowhere in the popover itself.
|
|
||||||
*/
|
|
||||||
const brownStepRecipe = {
|
|
||||||
id: 6,
|
|
||||||
name: "Soupe",
|
|
||||||
description: null,
|
|
||||||
picture: null,
|
|
||||||
portions: 4,
|
|
||||||
authorId: 1,
|
|
||||||
visibility: "PERSONAL",
|
|
||||||
allergens: [],
|
|
||||||
diets: [],
|
|
||||||
isFavorite: false,
|
|
||||||
ingredients: [],
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
description: "Faire revenir les oignons.",
|
|
||||||
picture: null,
|
|
||||||
order: 1,
|
|
||||||
techSteps: [
|
|
||||||
{
|
|
||||||
techStep: { id: 3, key: "brown" },
|
|
||||||
start: 0,
|
|
||||||
end: 13,
|
|
||||||
source: "auto",
|
|
||||||
ingredients: [],
|
|
||||||
utensils: [],
|
|
||||||
temperatures: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
function interceptAuth() {
|
function interceptAuth() {
|
||||||
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile });
|
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile });
|
||||||
cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] });
|
cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] });
|
||||||
|
|
@ -332,63 +293,6 @@ describe("Recipe catalog", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Regression (issue #36): while the correction popover's "add an
|
|
||||||
// ingredient/utensil/temperature" flow waits for a text selection, the
|
|
||||||
// mousedown that *starts* that drag lands on the step description — a DOM
|
|
||||||
// sibling of the popover — and used to count as an outside-click,
|
|
||||||
// closing the popover before mouseup; the selection then opened a fresh
|
|
||||||
// blank popover for the selected word as a brand-new technique. The
|
|
||||||
// description container is now exempt from the popover's outside-click.
|
|
||||||
it("keeps the same correction popover open while selecting a span for an ingredient", () => {
|
|
||||||
cy.viewport(1600, 900);
|
|
||||||
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] });
|
|
||||||
cy.intercept("GET", "**/recipes/6", { statusCode: 200, body: brownStepRecipe });
|
|
||||||
cy.intercept("GET", "**/reference/tech-steps", {
|
|
||||||
statusCode: 200,
|
|
||||||
body: [{ id: 3, key: "brown" }],
|
|
||||||
});
|
|
||||||
cy.intercept("GET", "**/reference/ingredients", { statusCode: 200, body: [] });
|
|
||||||
cy.intercept("GET", "**/reference/units", { statusCode: 200, body: [] });
|
|
||||||
cy.intercept("GET", "**/reference/utensils", { statusCode: 200, body: [] });
|
|
||||||
|
|
||||||
cy.visit("/recettes/6");
|
|
||||||
|
|
||||||
// Open the technique correction, then ask to attach an ingredient — the
|
|
||||||
// popover now shows the "select the span in the description" hint.
|
|
||||||
cy.contains(".step-tech-step", "Faire revenir").click();
|
|
||||||
cy.get(".tech-step-correction-popover").should("have.length", 1);
|
|
||||||
cy.get(".tech-step-correction-popover__selection").should("contain.text", "Faire revenir");
|
|
||||||
cy.contains(".tech-step-correction-popover__add-row", "Ajouter un ingrédient").click();
|
|
||||||
cy.get(".tech-step-correction-popover__hint").should("be.visible");
|
|
||||||
|
|
||||||
// Reproduce the real mousedown → drag → mouseup over the description.
|
|
||||||
// The mousedown is the event that used to close the popover.
|
|
||||||
cy.contains(".recipe-detail-panel__steps p", "Faire revenir").as("stepParagraph");
|
|
||||||
cy.get("@stepParagraph").trigger("mousedown");
|
|
||||||
cy.window().then((win) => {
|
|
||||||
const paragraph = win.document.querySelector(".recipe-detail-panel__steps p") as HTMLElement;
|
|
||||||
const span = [...paragraph.querySelectorAll<HTMLElement>("[data-offset]")].find((el) =>
|
|
||||||
el.textContent?.includes("oignons"),
|
|
||||||
);
|
|
||||||
const textNode = span?.firstChild as Text;
|
|
||||||
const local = textNode.textContent?.indexOf("oignons") ?? 0;
|
|
||||||
const range = win.document.createRange();
|
|
||||||
range.setStart(textNode, local);
|
|
||||||
range.setEnd(textNode, local + "oignons".length);
|
|
||||||
const selection = win.getSelection();
|
|
||||||
selection?.removeAllRanges();
|
|
||||||
selection?.addRange(range);
|
|
||||||
});
|
|
||||||
cy.get("@stepParagraph").trigger("mouseup");
|
|
||||||
|
|
||||||
// Still the *same* popover — its heading still quotes the technique span
|
|
||||||
// "Faire revenir", not "oignons" — now showing the ingredient sub-form
|
|
||||||
// for the freshly selected span, not a blank new-technique popover.
|
|
||||||
cy.get(".tech-step-correction-popover").should("have.length", 1);
|
|
||||||
cy.get(".tech-step-correction-popover__selection").should("contain.text", "Faire revenir");
|
|
||||||
cy.get(".tech-step-correction-popover__add-form").should("be.visible");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows a not-found message for a selected id the API rejects", () => {
|
it("shows a not-found message for a selected id the API rejects", () => {
|
||||||
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] });
|
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] });
|
||||||
cy.intercept("GET", "**/recipes/999", {
|
cy.intercept("GET", "**/recipes/999", {
|
||||||
|
|
|
||||||
|
|
@ -337,7 +337,6 @@ export function StepDescription({
|
||||||
existingUtensils={activeStepTechStep?.utensils ?? []}
|
existingUtensils={activeStepTechStep?.utensils ?? []}
|
||||||
existingTemperatures={activeStepTechStep?.temperatures ?? []}
|
existingTemperatures={activeStepTechStep?.temperatures ?? []}
|
||||||
resolvedMetadataSpan={resolvedMetadataSpan}
|
resolvedMetadataSpan={resolvedMetadataSpan}
|
||||||
descriptionContainerRef={containerRef}
|
|
||||||
onRequestSpan={setPendingSpanRequest}
|
onRequestSpan={setPendingSpanRequest}
|
||||||
onClose={closeActiveCorrection}
|
onClose={closeActiveCorrection}
|
||||||
onSubmitted={handleSubmitted}
|
onSubmitted={handleSubmitted}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
type UnitView,
|
type UnitView,
|
||||||
type UtensilView,
|
type UtensilView,
|
||||||
} from "@batch-cooking/shared";
|
} from "@batch-cooking/shared";
|
||||||
import { type ReactNode, type RefObject, useEffect, useRef, useState } from "react";
|
import { type ReactNode, useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ApiError, apiClient } from "../../../api/client";
|
import { ApiError, apiClient } from "../../../api/client";
|
||||||
import { errorMessageService } from "../../../services/error-message.service";
|
import { errorMessageService } from "../../../services/error-message.service";
|
||||||
|
|
@ -304,7 +304,6 @@ export function TechStepCorrectionPopover({
|
||||||
existingUtensils,
|
existingUtensils,
|
||||||
existingTemperatures,
|
existingTemperatures,
|
||||||
resolvedMetadataSpan,
|
resolvedMetadataSpan,
|
||||||
descriptionContainerRef,
|
|
||||||
onRequestSpan,
|
onRequestSpan,
|
||||||
onClose,
|
onClose,
|
||||||
onSubmitted,
|
onSubmitted,
|
||||||
|
|
@ -335,20 +334,6 @@ export function TechStepCorrectionPopover({
|
||||||
range: TextSelectionRange;
|
range: TextSelectionRange;
|
||||||
text: string;
|
text: string;
|
||||||
} | null;
|
} | null;
|
||||||
/**
|
|
||||||
* The step description's own container (`StepDescription`'s existing
|
|
||||||
* `containerRef`). A `mousedown` inside it is *not* treated as an
|
|
||||||
* outside-click here: while the "attach an ingredient/utensil/
|
|
||||||
* temperature" flow is waiting for a selection, the drag that makes that
|
|
||||||
* selection starts with a `mousedown` on the description — which is a DOM
|
|
||||||
* sibling of this popover, so without this exemption it closed the
|
|
||||||
* popover before the `mouseup` even landed, and the selection then opened
|
|
||||||
* a brand-new technique correction instead (issue #36). `StepDescription`
|
|
||||||
* already drives every meaningful interaction inside it
|
|
||||||
* (`setActiveCorrection`/`setResolvedMetadataSpan`) explicitly, so
|
|
||||||
* dropping the close here loses nothing.
|
|
||||||
*/
|
|
||||||
descriptionContainerRef?: RefObject<HTMLElement | null>;
|
|
||||||
/** Tells `StepDescription` "the next text selection in the description is for an ingredient/utensil/temperature mention, not a new technique correction" — see this component's own doc comment. */
|
/** Tells `StepDescription` "the next text selection in the description is for an ingredient/utensil/temperature mention, not a new technique correction" — see this component's own doc comment. */
|
||||||
onRequestSpan: (kind: MetadataKind) => void;
|
onRequestSpan: (kind: MetadataKind) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|
@ -474,17 +459,13 @@ export function TechStepCorrectionPopover({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClickOutside(e: MouseEvent) {
|
function handleClickOutside(e: MouseEvent) {
|
||||||
const target = e.target as Node;
|
if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) {
|
||||||
if (popoverRef.current?.contains(target)) return;
|
|
||||||
// A mousedown in the description isn't an "outside" click — it's how
|
|
||||||
// the viewer selects the span for an ingredient/utensil/temperature
|
|
||||||
// mention. Let StepDescription handle it (see `descriptionContainerRef`).
|
|
||||||
if (descriptionContainerRef?.current?.contains(target)) return;
|
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
}, [onClose, descriptionContainerRef]);
|
}, [onClose]);
|
||||||
|
|
||||||
async function removeMatch() {
|
async function removeMatch() {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue