batchCooking/apps/web/cypress/support/step_definitions/common.steps.ts
Nicolas a7d1103b4a fix(web): remplace le hook Before() par un reset dans le premier step
Même famille de bug que les 2 commits précédents : dès qu'un
Before()/After() Cucumber est enregistré, le browser runtime du
preprocessor lit `messages.HookType.BEFORE_TEST_CASE`/`AFTER_TEST_CASE`
pour le rapporter — absent de @cucumber/messages@17.1.1 (voir
l'override dans package.json). Confirmé par le run CI précédent :
chaque scénario plantait sur "Cannot read properties of undefined
(reading 'BEFORE_TEST_CASE')", y compris ceux n'ayant a priori rien à
voir avec le profil (le seul Before() du repo vit dans
common.steps.ts, partagé par toutes les features).

Le seul hook du repo ne servait qu'à réinitialiser le profil "connecté"
courant entre scénarios. Remplacé par un reset explicite au tout début
du step "I am signed in as ...", le point d'entrée par lequel passe
systématiquement toute construction de profil — équivalent
fonctionnellement, sans avoir besoin d'enregistrer de hook du tout.
2026-08-19 12:07:33 +02:00

191 lines
6.9 KiB
TypeScript

import { Given, Then, When } from "@badeball/cypress-cucumber-preprocessor";
import { buildProfile, currentProfile, resetProfile, setCurrentProfile } from "../profile";
// Steps shared across every feature — signing in/out, navigation, and
// generic UI assertions/interactions phrased the same way regardless of
// which page they happen to run against. Anything specific to one feature
// (its own API responses, its own DOM structure) lives in that feature's
// own `<name>.steps.ts` instead — same split as apps/api's
// step-definitions/ (shared "profile already exists" vs. feature-specific
// steps).
//
// Mocks the API via `cy.intercept` — this job doesn't run a live backend
// (see .github/workflows/ci.yml); apps/api's own Mocha/Cucumber suites
// cover real API behavior against a real database.
Given("I am not signed in", () => {
cy.intercept("GET", "**/auth/me", { statusCode: 401 });
});
// No `Before()` hook for this reset (deliberately) — registering any
// Cucumber hook makes the preprocessor's browser runtime read
// `messages.HookType.{BEFORE,AFTER}_TEST_CASE` to report it, and that enum
// doesn't exist on the older, CommonJS-only `@cucumber/messages` this repo
// is pinned to (see `pnpm.overrides` in package.json, and this branch's
// commit history for why) — every scenario crashed on "Cannot read
// properties of undefined (reading 'BEFORE_TEST_CASE')" the moment this
// file registered one. Resetting right here instead, at the one step every
// profile-building chain always starts with, is equivalent for our
// purposes without needing a hook at all.
Given("I am signed in as {string} {string}", (firstName: string, lastName: string) => {
resetProfile();
setCurrentProfile(
buildProfile({
firstName,
lastName,
email: `${firstName.toLowerCase()}@example.com`,
}),
);
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: currentProfile });
});
// Composable with the step above — re-registers the same intercept with an
// updated profile field. Order between these doesn't matter as long as they
// all run before the scenario's `visit`/`When` step: Cypress resolves
// multiple `cy.intercept` calls on the same route by giving the
// most-recently-registered one priority, so the final, fully-assembled
// profile is always what the app actually receives.
Given("my household id is {int}", (houseId: number) => {
if (!currentProfile) {
throw new Error('"my household id is" must follow "I am signed in as ..."');
}
setCurrentProfile({ ...currentProfile, houseId });
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: currentProfile });
});
Given("my diet id is {int}", (dietId: number) => {
if (!currentProfile) {
throw new Error('"my diet id is" must follow "I am signed in as ..."');
}
setCurrentProfile({ ...currentProfile, dietId });
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: currentProfile });
});
Given("my user id is {int}", (id: number) => {
if (!currentProfile) {
throw new Error('"my user id is" must follow "I am signed in as ..."');
}
setCurrentProfile({ ...currentProfile, id });
cy.intercept("GET", "**/auth/me", { statusCode: 200, body: currentProfile });
});
When("I visit {string}", (path: string) => {
cy.visit(path);
});
Then("the URL should include {string}", (fragment: string) => {
cy.url().should("include", fragment);
});
Then("the URL should not include {string}", (fragment: string) => {
cy.url().should("not.include", fragment);
});
Then("the URL should be the home page", () => {
cy.url().should("eq", `${Cypress.config().baseUrl}/`);
});
Then("I should see {string}", (text: string) => {
cy.contains(text).should("be.visible");
});
Then("I should see the heading {string}", (text: string) => {
cy.contains("h1", text).should("be.visible");
});
Then("I should not see {string}", (text: string) => {
cy.contains(text).should("not.exist");
});
When("I click the button {string}", (text: string) => {
cy.contains("button", text).click();
});
When("I click the link {string}", (text: string) => {
cy.contains("a", text).click();
});
When("I fill in the {string} field with {string}", (fieldId: string, value: string) => {
cy.get(`#${fieldId}`).type(value);
});
When("I clear the {string} field", (fieldId: string) => {
cy.get(`#${fieldId}`).clear();
});
Then("the {string} field should have the value {string}", (fieldId: string, value: string) => {
cy.get(`#${fieldId}`).should("have.value", value);
});
When("I select {string} from the {string} field", (value: string, fieldId: string) => {
cy.get(`#${fieldId}`).select(value);
});
Then("I should see the section {string}", (legend: string) => {
cy.contains("legend", legend).should("be.visible");
});
Then("the checkbox {string} should be checked", (label: string) => {
cy.contains("label", label).find("input[type=checkbox]").should("be.checked");
});
Then("the checkbox {string} should not be checked", (label: string) => {
cy.contains("label", label).find("input[type=checkbox]").should("not.be.checked");
});
When("I check the checkbox {string}", (label: string) => {
cy.contains("label", label).find("input[type=checkbox]").check();
});
Then("the radio {string} should be checked", (label: string) => {
cy.contains("label", label).find("input[type=radio]").should("be.checked");
});
Then("the radio {string} should not be checked", (label: string) => {
cy.contains("label", label).find("input[type=radio]").should("not.be.checked");
});
When("I click the radio {string}", (label: string) => {
cy.contains("label", label).click();
});
Then("the page should have no theme override", () => {
cy.get("html").should("not.have.attr", "data-theme");
});
Then("the page theme should be {string}", (theme: string) => {
cy.get("html").should("have.attr", "data-theme", theme);
});
// Freezes `Date` so "today"/"this week" assertions are deterministic
// instead of depending on the day the suite happens to run.
Given("today is frozen at {string}", (iso: string) => {
cy.clock(new Date(iso), ["Date"]);
});
Given("the viewport is {int} by {int}", (width: number, height: number) => {
cy.viewport(width, height);
});
Then("the {string} button should not be disabled", (text: string) => {
cy.contains("button", text).should("not.be.disabled");
});
When("I open the account menu", () => {
cy.get(".app-sidebar__account-toggle").click();
});
// Not "**/planning*" — that glob also matches the Vite dev request for
// planning-page.scss. Used by any feature that lands on the home page
// (Planning) but isn't itself testing the planning grid's content.
Given("the planning request returns nothing", () => {
cy.intercept("GET", /\/planning\?/, { statusCode: 200, body: null });
});
Given("the household request returns no household", () => {
cy.intercept("GET", "**/house/current", { statusCode: 200, body: null });
});
Then("the link {string} should point to {string}", (text: string, href: string) => {
cy.contains("a", text).should("have.attr", "href", href);
});