Les tests e2e (apps/web/cypress/e2e/) étaient de simples specs Cypress (.cy.ts), sans lien avec Cucumber alors qu'apps/api utilise déjà Gherkin pour ses propres tests BDD. Intègre @badeball/cypress-cucumber-preprocessor pour écrire les scénarios utilisateurs en Gherkin des deux côtés, même vocabulaire. - cypress.config.ts : specPattern sur *.feature, wiring du préprocesseur (esbuild bundler + plugin cucumber) - Les 11 fichiers .cy.ts sont remplacés par des paires .feature/.steps.ts (co-localisées, même nom) — conversion complète, comportement équivalent (mêmes intercepts, mêmes assertions) - cypress/support/step_definitions/common.steps.ts : steps partagés entre features (connexion, navigation, assertions génériques de texte/URL/champ) — globaux à toute la suite, réutilisables tels quels - cypress/support/profile.ts : profil du compte "connecté" courant, assemblé au fil de plusieurs Given avant le premier visit/When - README : nouvelle section "Cucumber (apps/web)" (miroir de la section existante pour apps/api), mise à jour des références aux anciens noms de fichiers .cy.ts (déjà obsolètes avant ce changement) Vérification : impossible d'exécuter Cypress dans cet environnement (crash Electron/GPU au lancement, limitation déjà documentée dans le README — reproductible sur main, indépendante de ce changement). À la place : - les 447 steps Gherkin des 11 .feature ont été vérifiés programmatiquement contre les 165 patterns de step enregistrés : 0 non résolu, 0 ambigu - les 11 .feature parsent correctement avec le parser Gherkin officiel (57 scénarios au total) - tous les .steps.ts passent `biome check` (syntaxe + style) sans erreur - CYPRESS_INSTALL_BINARY déjà géré (voir PR précédente) — le binaire est bien présent localement (`cypress verify` OK), donc le blocage est spécifiquement le sandbox GPU de cet environnement, pas l'installation La vraie exécution reste à vérifier via le job `e2e` de la CI GitHub Actions sur cette PR — c'est le chemin déjà documenté dans le README pour cet environnement précis.
184 lines
6.3 KiB
TypeScript
184 lines
6.3 KiB
TypeScript
import { Before, 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.
|
|
|
|
Before(() => {
|
|
resetProfile();
|
|
});
|
|
|
|
Given("I am not signed in", () => {
|
|
cy.intercept("GET", "**/auth/me", { statusCode: 401 });
|
|
});
|
|
|
|
Given("I am signed in as {string} {string}", (firstName: string, lastName: string) => {
|
|
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);
|
|
});
|