diff --git a/apps/web/cypress/e2e/admin-monitoring.cy.ts b/apps/web/cypress/e2e/admin-monitoring.cy.ts index fd6ba78..733e184 100644 --- a/apps/web/cypress/e2e/admin-monitoring.cy.ts +++ b/apps/web/cypress/e2e/admin-monitoring.cy.ts @@ -53,21 +53,33 @@ describe("Admin monitoring", () => { }); // The page route `/admin/monitoring` and the API path `GET /admin/monitoring` - // are identical — a plain `**/admin/monitoring` glob would also match the + // are identical — a plain `**/admin/monitoring` glob also matches the // `cy.visit()` document request (and whether that's same-origin or not - // depends on `VITE_API_URL`, which isn't set in CI). `resourceType: "fetch"` - // pins the intercept to the `AdminApiClient` XHR only. - const monitoringApi = { - method: "GET", - url: "**/admin/monitoring", - resourceType: "fetch", - } as const; + // depends on `VITE_API_URL`, which isn't set in CI). + // + // This used to be disambiguated with a declarative `resourceType: "fetch"` + // route matcher, but that stopped working (Cypress 13.17.0 on this + // machine's Chrome 152): the *declarative* `resourceType` matcher no + // longer catches this request at all — confirmed by logging every request + // Cypress hands to a `{resourceType: "fetch"}` intercept, which only ever + // saw `/admin/auth/me`, never `/admin/monitoring` — even though the very + // same request's own `req.resourceType`, read *inside* a handler matched + // by url/method alone, reliably reads back `"fetch"`. So the request *is* + // correctly typed; only the pre-filtering by that type is broken. Reading + // `req.resourceType` inside the handler and branching manually (as below) + // isn't affected by that bug and reproduced correctly in every check. + function interceptMonitoringApi(response: { statusCode: number; body: unknown }) { + return cy.intercept("GET", "**/admin/monitoring", (req) => { + if (req.resourceType === "document") { + req.continue(); + return; + } + req.reply(response); + }); + } it("renders one card per service with its status and details", () => { - cy.intercept(monitoringApi, { - statusCode: 200, - body: monitoringFixture(), - }).as("getMonitoring"); + interceptMonitoringApi({ statusCode: 200, body: monitoringFixture() }).as("getMonitoring"); cy.visit("/admin/monitoring"); cy.wait("@getMonitoring"); @@ -85,11 +97,17 @@ describe("Admin monitoring", () => { }); it("shows an error state when the request fails", () => { - cy.intercept(monitoringApi, { - statusCode: 500, - body: { code: 5000, message: "x" }, - }); + // Aliased and waited on (unlike the other tests' plain intercepts) so + // this genuinely proves the 500 came from the mock — without a live API + // server, an unmatched request fails at the network level too, which + // `MonitoringPage`'s `.catch()` can't tell apart from a real 500 (see + // the comment above `interceptMonitoringApi`, this is exactly the class + // of bug that let this test pass for the wrong reason before). + interceptMonitoringApi({ statusCode: 500, body: { code: 5000, message: "x" } }).as( + "getMonitoringError", + ); cy.visit("/admin/monitoring"); + cy.wait("@getMonitoringError"); cy.contains("Impossible de charger").should("be.visible"); }); }); diff --git a/apps/web/cypress/e2e/recipes.cy.ts b/apps/web/cypress/e2e/recipes.cy.ts index a3190df..d3a4958 100644 --- a/apps/web/cypress/e2e/recipes.cy.ts +++ b/apps/web/cypress/e2e/recipes.cy.ts @@ -69,6 +69,64 @@ const omeletteDetail = { ], }; +/** + * A 3-step recipe whose middle step already has an existing technique match + * carrying attached ingredient/utensil metadata — for the step-numbering + * regression test below, which needs a correction popover with several + * `
  • ` rows of its own open at once (see that test's own comment). + */ +const tarteDetail = { + id: 5, + name: "Tarte", + description: null, + picture: null, + portions: 4, + authorId: 1, + visibility: "PERSONAL", + allergens: [], + diets: [], + isFavorite: false, + ingredients: [], + steps: [ + { id: 1, description: "Préchauffer le four.", picture: null, order: 1, techSteps: [] }, + { + id: 2, + description: "Faire revenir les oignons avec une poêle.", + picture: null, + order: 2, + techSteps: [ + { + techStep: { id: 2, key: "panFry" }, + start: 0, + end: 13, + source: "auto", + ingredients: [ + { + ingredient: { + id: 10, + key: "onion", + icon: "VEGETABLE", + category: "freshProduce", + subcategory: "vegetables", + allergens: [], + diets: [], + }, + quantity: 2, + unit: { id: 2, key: "piece", type: "COUNT", toBaseFactor: 1 }, + start: 18, + end: 25, + source: "auto", + }, + ], + utensils: [{ utensil: { id: 20, key: "pan" }, start: 31, end: 40, source: "auto" }], + temperatures: [], + }, + ], + }, + { id: 3, description: "Servir chaud.", picture: null, order: 3, techSteps: [] }, + ], +}; + function interceptAuth() { cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); cy.intercept("GET", "**/profile/disliked-ingredients", { statusCode: 200, body: [] }); @@ -187,6 +245,54 @@ describe("Recipe catalog", () => { }); }); + // Regression: `.recipe-detail-panel__steps`'s CSS step counter + // (recipes.scss) used a bare descendant `li` selector, matching *any* + // `
  • ` nested inside a step — including ones several levels down + // inside `TechStepCorrectionPopover` (each attached ingredient/utensil/ + // temperature row, and the technique picker's own `
  • ` list). Opening + // a correction with attached metadata inflated the counter, throwing off + // every *later* step's own displayed number — exactly the "les étapes + // affichées sont numérotées n'importe comment" bug report this covers. + it("keeps every step's own number correct while a correction popover with attached metadata is open", () => { + cy.viewport(1600, 900); + cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] }); + cy.intercept("GET", "**/recipes/5", { statusCode: 200, body: tarteDetail }); + cy.intercept("GET", "**/reference/tech-steps", { + statusCode: 200, + body: [{ id: 2, key: "panFry" }], + }); + + cy.visit("/recettes/5"); + cy.get(".recipe-detail-panel__steps > li").should("have.length", 3); + + // Open the middle step's correction — pre-seeded with one ingredient + // and one utensil, so its popover renders two `.tech-step-correction-popover__row` + // `
  • `s of its own (plus the technique picker's own `
  • ` list once + // "Changer" is clicked) sitting *inside* the second step's own `
  • `. + cy.contains(".step-tech-step", "Faire revenir").click(); + cy.get(".tech-step-correction-popover__row").should("have.length", 2); + cy.contains(".tech-step-correction-popover__technique-change", "Changer").click(); + cy.get(".catalog-search-picker__list li").should("have.length.greaterThan", 0); + + // None of the popover's own nested `
  • `s join the step counter chain + // — `getComputedStyle(...).content` isn't reliable for reading a + // *resolved* counter() value across browsers (it can return the raw + // `counter(step) ". "` declaration instead), so this checks the actual + // `counter-increment` computed property instead: a real, directly + // testable signal for exactly what `> li` (recipes.scss) does and + // doesn't scope to. + cy.get(".tech-step-correction-popover__row").each(($row) => { + expect(window.getComputedStyle($row[0]).counterIncrement).to.equal("none"); + }); + cy.get(".catalog-search-picker__list li").each(($item) => { + expect(window.getComputedStyle($item[0]).counterIncrement).to.equal("none"); + }); + // The step `
  • `s themselves are still correctly in the chain. + cy.get(".recipe-detail-panel__steps > li").each(($step) => { + expect(window.getComputedStyle($step[0]).counterIncrement).to.equal("step 1"); + }); + }); + it("shows a not-found message for a selected id the API rejects", () => { cy.intercept("GET", /\/recipes\?/, { statusCode: 200, body: [] }); cy.intercept("GET", "**/recipes/999", { diff --git a/apps/web/cypress/support/step_definitions/common.steps.ts b/apps/web/cypress/support/step_definitions/common.steps.ts index c74aac9..272a2ef 100644 --- a/apps/web/cypress/support/step_definitions/common.steps.ts +++ b/apps/web/cypress/support/step_definitions/common.steps.ts @@ -180,7 +180,12 @@ Then("the ingredient {string} should be visible in the picker", (name: string) = }); Then("the recipe should include the ingredient {string}", (name: string) => { - cy.contains(".ingredient-row__name", name).should("be.visible"); + // Same reasoning as "I should see the highlighted technique" above: on the + // recipe-edit form the ingredients list can sit below the fold of + // `.app-content`'s own scroll (Nom/Description/Photo/Portions/Visibilité/ + // Régimes all come first), and a bare `.should("be.visible")` doesn't + // auto-scroll — only interaction commands like `.click()`/`.check()` do. + cy.contains(".ingredient-row__name", name).scrollIntoView().should("be.visible"); }); Then("there should be {int} ingredient rows", (count: number) => { diff --git a/apps/web/src/features/recipes/recipes.scss b/apps/web/src/features/recipes/recipes.scss index de13290..8f6b260 100644 --- a/apps/web/src/features/recipes/recipes.scss +++ b/apps/web/src/features/recipes/recipes.scss @@ -630,7 +630,17 @@ gap: var(--space-md); counter-reset: step; - li { + // `> li`, not a bare descendant `li` — this used to match *any* `
  • ` + // nested anywhere inside a step, not just the step's own direct one. + // Harmless while `StepDescription`'s editable mode only ever added + // plain text/buttons, but `TechStepCorrectionPopover` renders its own + // `
  • `s a few levels down (each attached ingredient/utensil/ + // temperature row, `.tech-step-correction-popover__row`; the technique + // picker's `.catalog-search-picker__list`, up to ~74 entries) — + // every one of those was incrementing this *same* counter, throwing + // every step's own number off as soon as any correction popover with + // attached metadata (or its technique picker) was open. + > li { counter-increment: step; &::before { @@ -639,14 +649,20 @@ color: var(--color-primary); } - img { + // Same descendant-selector fix as `> li` above, for the same reason: + // both the step's own picture and its `StepDescription`-rendered `

    ` + // are direct children of this `

  • ` — `TechStepCorrectionPopover`'s + // own `

    `s (the selection line, hints, `.field-error`) sit deeper + // inside it and must not inherit "inline, no margin" meant only for + // the step's own paragraph. + > img { display: block; max-width: 100%; border-radius: var(--radius-base); margin-bottom: var(--space-xs); } - p { + > p { display: inline; margin: 0; }