Compare commits
No commits in common. "ab1cf421d900c8e18ada1f9c2311224a0a54f8a6" and "713d6c520d6de7033bff499c301ce053beff8717" have entirely different histories.
ab1cf421d9
...
713d6c520d
3 changed files with 12 additions and 78 deletions
|
|
@ -108,11 +108,7 @@ Given("recipe 7 exists with an egg omelette", () => {
|
||||||
diets: [],
|
diets: [],
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
ingredients: [{ ingredient: egg, quantity: 3, unit: pieceUnit }],
|
ingredients: [{ ingredient: egg, quantity: 3, unit: pieceUnit }],
|
||||||
// `techSteps: []` matches the real `StepView` shape (packages/shared) —
|
steps: [{ id: 1, description: "Battre les œufs.", picture: null, order: 0 }],
|
||||||
// every other recipe-step mock in the suite sends it, and its absence
|
|
||||||
// here let `/recettes/7` (rendered after this recipe saves) hit
|
|
||||||
// `StepDescription` with `techSteps === undefined`.
|
|
||||||
steps: [{ id: 1, description: "Battre les œufs.", picture: null, order: 0, techSteps: [] }],
|
|
||||||
};
|
};
|
||||||
cy.intercept("GET", "**/recipes/7", { statusCode: 200, body: existingRecipe });
|
cy.intercept("GET", "**/recipes/7", { statusCode: 200, body: existingRecipe });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -166,29 +166,6 @@ const brownStepRecipe = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* A recipe whose step object is missing `techSteps` entirely (not `[]`) —
|
|
||||||
* a partial/older payload shape. `StepDescription` must render its
|
|
||||||
* description as plain text rather than crashing on `liveTechSteps.flatMap`.
|
|
||||||
* Regression: an intermittent `recipe-form.feature` failure where saving an
|
|
||||||
* edited recipe navigates to `/recettes/:id`, whose detail loaded before
|
|
||||||
* `techSteps` was populated.
|
|
||||||
*/
|
|
||||||
const techStepsMissingDetail = {
|
|
||||||
id: 7,
|
|
||||||
name: "Omelette",
|
|
||||||
description: null,
|
|
||||||
picture: null,
|
|
||||||
portions: 2,
|
|
||||||
authorId: 1,
|
|
||||||
visibility: "PERSONAL",
|
|
||||||
allergens: [],
|
|
||||||
diets: [],
|
|
||||||
isFavorite: false,
|
|
||||||
ingredients: [],
|
|
||||||
steps: [{ id: 1, description: "Battre les œufs.", picture: null, order: 1 }],
|
|
||||||
};
|
|
||||||
|
|
||||||
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: [] });
|
||||||
|
|
@ -307,27 +284,6 @@ describe("Recipe catalog", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Regression: a step payload missing `techSteps` entirely (see
|
|
||||||
// `techStepsMissingDetail`) made `StepDescription` throw
|
|
||||||
// `Cannot read properties of undefined (reading 'flatMap')`, surfacing as
|
|
||||||
// an uncaught application error. An uncaught exception fails the spec on
|
|
||||||
// its own, so simply asserting the description renders is enough — no
|
|
||||||
// extra `cy.on("uncaught:exception")` needed.
|
|
||||||
it("renders a step whose techSteps field is absent from the payload, as plain text", () => {
|
|
||||||
cy.viewport(1600, 900);
|
|
||||||
cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] });
|
|
||||||
cy.intercept("GET", "**/recipes/7", { statusCode: 200, body: techStepsMissingDetail }).as(
|
|
||||||
"getRecipe",
|
|
||||||
);
|
|
||||||
|
|
||||||
cy.visit("/recettes/7");
|
|
||||||
cy.wait("@getRecipe");
|
|
||||||
|
|
||||||
cy.get(".recipe-detail-panel__steps").within(() => {
|
|
||||||
cy.contains("Battre les œufs.").should("be.visible");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Regression: `.recipe-detail-panel__steps`'s CSS step counter
|
// Regression: `.recipe-detail-panel__steps`'s CSS step counter
|
||||||
// (recipes.scss) used a bare descendant `li` selector, matching *any*
|
// (recipes.scss) used a bare descendant `li` selector, matching *any*
|
||||||
// `<li>` nested inside a step — including ones several levels down
|
// `<li>` nested inside a step — including ones several levels down
|
||||||
|
|
|
||||||
|
|
@ -71,15 +71,14 @@ function findTechStepForTemperature(
|
||||||
* no click handlers) — this mode is purely additive, not a rewrite of the
|
* no click handlers) — this mode is purely additive, not a rewrite of the
|
||||||
* read-only rendering.
|
* read-only rendering.
|
||||||
*
|
*
|
||||||
* Maintains its own local copy of `techSteps` (seeded from the prop — or
|
* Maintains its own local copy of `techSteps` (seeded from the prop, then
|
||||||
* `[]` if it's momentarily absent, see the prop's own note — then replaced
|
* replaced with whatever `POST .../corrections` returns on a successful
|
||||||
* with whatever `POST .../corrections` returns on a successful submit — see
|
* submit — see `SubmitTechStepCorrectionResult`'s doc comment,
|
||||||
* `SubmitTechStepCorrectionResult`'s doc comment, `packages/shared`) so a
|
* `packages/shared`) so a correction's effect (a new/relabeled/removed
|
||||||
* correction's effect (a new/relabeled/removed highlight) appears
|
* highlight) appears immediately, without needing the parent to re-fetch
|
||||||
* immediately, without needing the parent to re-fetch the whole recipe.
|
* the whole recipe. Resynced whenever the `techSteps` prop itself changes
|
||||||
* Resynced whenever the `techSteps` prop itself changes (e.g. the parent
|
* (e.g. the parent reloaded the recipe for an unrelated reason) so this
|
||||||
* reloaded the recipe for an unrelated reason) so this never keeps showing
|
* never keeps showing stale local state past that.
|
||||||
* stale local state past that.
|
|
||||||
*/
|
*/
|
||||||
export function StepDescription({
|
export function StepDescription({
|
||||||
description,
|
description,
|
||||||
|
|
@ -89,32 +88,15 @@ export function StepDescription({
|
||||||
stepId,
|
stepId,
|
||||||
}: {
|
}: {
|
||||||
description: string;
|
description: string;
|
||||||
/**
|
techSteps: StepTechStepView[];
|
||||||
* The step's detected technique sequence. `StepView.techSteps`
|
|
||||||
* (`packages/shared`) always sends an array — `[]` when nothing matched —
|
|
||||||
* but this is optional so a partial/not-yet-resolved payload degrades to
|
|
||||||
* "render the description as plain text" instead of crashing on
|
|
||||||
* `.flatMap`, the same "tolerate missing data, don't crash" posture this
|
|
||||||
* component already takes for a match with no `contextStart`/`contextEnd`
|
|
||||||
* or a temperature with no owning occurrence. Seen `undefined` in
|
|
||||||
* practice on the post-save navigation of the "edit an existing recipe"
|
|
||||||
* flow, before the reloaded recipe's detail had resolved
|
|
||||||
* (`recipe-form.feature`).
|
|
||||||
*/
|
|
||||||
techSteps?: StepTechStepView[];
|
|
||||||
/** Requires `recipeId`/`stepId` when `true` — omit (or leave `false`) for a read-only view with nothing real to correct against yet (e.g. `RecipeDetailPanel`'s `"loaded-draft"` unsaved-preview branch). */
|
/** Requires `recipeId`/`stepId` when `true` — omit (or leave `false`) for a read-only view with nothing real to correct against yet (e.g. `RecipeDetailPanel`'s `"loaded-draft"` unsaved-preview branch). */
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
recipeId?: number;
|
recipeId?: number;
|
||||||
stepId?: number;
|
stepId?: number;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// `?? []` here, not just at each read site, so every consumer below
|
const [liveTechSteps, setLiveTechSteps] = useState(techSteps);
|
||||||
// (`.flatMap` for `temperatures`, `splitDescriptionSegments`,
|
useEffect(() => setLiveTechSteps(techSteps), [techSteps]);
|
||||||
// `findTechStepForTemperature`, the `activeStepTechStep` lookup) always
|
|
||||||
// has a real array — see the `techSteps` prop's own note for when it
|
|
||||||
// arrives absent.
|
|
||||||
const [liveTechSteps, setLiveTechSteps] = useState(techSteps ?? []);
|
|
||||||
useEffect(() => setLiveTechSteps(techSteps ?? []), [techSteps]);
|
|
||||||
|
|
||||||
// Temperatures are attached per technique-clause server-side; for the
|
// Temperatures are attached per technique-clause server-side; for the
|
||||||
// read-only highlight we only care *where* in the description they are,
|
// read-only highlight we only care *where* in the description they are,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue