From 26453dd757af7f5ffb33c153c3d95bb0204874da Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 17 Aug 2026 10:49:19 +0200 Subject: [PATCH] =?UTF-8?q?Tests:=20couverture=20Cypress=20pour=20les=20pa?= =?UTF-8?q?ram=C3=A8tres,=20l'onboarding=20et=20la=20sidebar=20(step=208/8?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - household.cy.ts scindé en preferences.cy.ts (régime/allergies) et household-settings.cy.ts (nom, créer/rejoindre/membres/suppression/ quitter, avec et sans droits admin) - onboarding.cy.ts réordonné (régime → foyer → allergènes), avec les cas skip / créer / rejoindre à l'étape foyer - account.cy.ts: identité, suppression de compte (erreur de mot de passe, succès, annulation) - sidebar.cy.ts: menu Paramètres (3 liens) et menu compte (Mon compte, déconnexion), redirection /foyer → /parametres/foyer - auth.cy.ts/home-planning.cy.ts: adaptés au nouveau menu compte et au premier écran d'onboarding (régime, plus foyer) --- apps/web/cypress/e2e/account.cy.ts | 68 +++++++ apps/web/cypress/e2e/auth.cy.ts | 18 +- apps/web/cypress/e2e/home-planning.cy.ts | 10 +- apps/web/cypress/e2e/household-settings.cy.ts | 185 ++++++++++++++++++ apps/web/cypress/e2e/onboarding.cy.ts | 125 +++++++----- .../{household.cy.ts => preferences.cy.ts} | 42 +--- apps/web/cypress/e2e/sidebar.cy.ts | 50 +++++ 7 files changed, 394 insertions(+), 104 deletions(-) create mode 100644 apps/web/cypress/e2e/account.cy.ts create mode 100644 apps/web/cypress/e2e/household-settings.cy.ts rename apps/web/cypress/e2e/{household.cy.ts => preferences.cy.ts} (62%) create mode 100644 apps/web/cypress/e2e/sidebar.cy.ts diff --git a/apps/web/cypress/e2e/account.cy.ts b/apps/web/cypress/e2e/account.cy.ts new file mode 100644 index 0000000..65a79da --- /dev/null +++ b/apps/web/cypress/e2e/account.cy.ts @@ -0,0 +1,68 @@ +import { ErrorCode } from "@batch-cooking/shared"; + +// Mocks the API via cy.intercept — see auth.cy.ts for the rationale. + +const authenticatedProfile = { + id: 1, + firstName: "Alice", + lastName: "Martin", + email: "alice@example.com", + tokenVersion: 0, + houseId: null, + dietId: null, +}; + +describe("Account settings (/parametres/compte)", () => { + beforeEach(() => { + cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); + }); + + it("shows the signed-in profile's identity", () => { + cy.visit("/parametres/compte"); + + cy.contains("Alice").should("be.visible"); + cy.contains("Martin").should("be.visible"); + cy.contains("alice@example.com").should("be.visible"); + }); + + it("shows an error and keeps the session when the password is wrong", () => { + cy.intercept("DELETE", "**/auth/me", { + statusCode: 401, + body: { code: ErrorCode.INVALID_CREDENTIALS, message: "Invalid password" }, + }).as("deleteAccount"); + + cy.visit("/parametres/compte"); + cy.contains("button", "Supprimer mon compte").click(); + cy.get("#deleteAccountPassword").type("wrong-password"); + cy.contains("button", "Confirmer la suppression").click(); + + cy.wait("@deleteAccount"); + cy.contains("Email ou mot de passe incorrect").should("be.visible"); + cy.url().should("include", "/parametres/compte"); + }); + + it("deletes the account and returns to the login page", () => { + cy.intercept("DELETE", "**/auth/me", { statusCode: 204 }).as("deleteAccount"); + + cy.visit("/parametres/compte"); + cy.contains("button", "Supprimer mon compte").click(); + cy.get("#deleteAccountPassword").type("correct-horse-battery-staple"); + cy.contains("button", "Confirmer la suppression").click(); + + cy.wait("@deleteAccount") + .its("request.body") + .should("deep.equal", { password: "correct-horse-battery-staple" }); + cy.url().should("include", "/login"); + }); + + it("cancels the deletion without calling the API", () => { + cy.intercept("DELETE", "**/auth/me").as("deleteAccount"); + + cy.visit("/parametres/compte"); + cy.contains("button", "Supprimer mon compte").click(); + cy.contains("button", "Annuler").click(); + + cy.contains("button", "Confirmer la suppression").should("not.exist"); + cy.get("@deleteAccount.all").should("have.length", 0); + }); +}); diff --git a/apps/web/cypress/e2e/auth.cy.ts b/apps/web/cypress/e2e/auth.cy.ts index 40250ac..9be4fde 100644 --- a/apps/web/cypress/e2e/auth.cy.ts +++ b/apps/web/cypress/e2e/auth.cy.ts @@ -6,14 +6,11 @@ import { ErrorCode } from "@batch-cooking/shared"; // suites against a real database. describe("Signup", () => { - it("creates a profile and starts the onboarding wizard (household/regime/allergens)", () => { + it("creates a profile and starts the onboarding wizard (regime/household/allergens)", () => { cy.intercept("GET", "**/auth/me", { statusCode: 401 }); // The onboarding wizard's first step (see onboarding.cy.ts for the full - // walkthrough) reads the household right away to prefill its name field. - cy.intercept("GET", "**/house/current", { - statusCode: 200, - body: { id: 1, name: "Foyer de Alice" }, - }); + // walkthrough) is the regime step, which fetches the reference list. + cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [] }); cy.intercept("POST", "**/auth/signup", { statusCode: 201, body: { @@ -22,7 +19,7 @@ describe("Signup", () => { lastName: "Martin", email: "alice@example.com", tokenVersion: 0, - houseId: 1, + houseId: null, dietId: null, }, }).as("signup"); @@ -38,8 +35,8 @@ describe("Signup", () => { // Not the home page directly — signup hands off to the onboarding // wizard first (RedirectIfAuthenticated no longer applies here, it's a // RequireAuth-gated route of its own, see App.tsx). - cy.url().should("include", "/onboarding/foyer"); - cy.get("#houseName").should("have.value", "Foyer de Alice"); + cy.url().should("include", "/onboarding/regime"); + cy.contains("Étape 1 sur 3").should("be.visible"); }); it("shows a client-side validation error without calling the API", () => { @@ -157,6 +154,9 @@ describe("Already authenticated", () => { cy.intercept("POST", "**/auth/logout", { statusCode: 204 }).as("logout"); cy.visit("/"); + // "Se déconnecter" lives inside the account menu, opened by clicking + // the greeting button — see AppLayout.tsx's AccountMenu. + cy.contains("button", "Bonjour Alice").click(); cy.contains("button", "Se déconnecter").click(); cy.wait("@logout"); diff --git a/apps/web/cypress/e2e/home-planning.cy.ts b/apps/web/cypress/e2e/home-planning.cy.ts index 7f336c0..01e1d02 100644 --- a/apps/web/cypress/e2e/home-planning.cy.ts +++ b/apps/web/cypress/e2e/home-planning.cy.ts @@ -19,6 +19,8 @@ describe("Sidebar navigation", () => { cy.visit("/"); }); + // The Foyer/Compte/Préférences links — behind the sidebar's "Paramètres" + // toggle, not the main nav tested here — are covered by sidebar.cy.ts. it("highlights the current section and navigates between stub pages", () => { cy.contains("nav a", "Planning").should("have.class", "active"); @@ -32,19 +34,15 @@ describe("Sidebar navigation", () => { cy.url().should("include", "/liste-de-courses"); cy.contains("h1", "Liste de courses").should("be.visible"); - cy.contains("nav a", "Foyer & profil").click(); - cy.url().should("include", "/foyer"); - cy.contains("h1", "Foyer & profil").should("be.visible"); - cy.contains("nav a", "Planning").click(); cy.url().should("eq", `${Cypress.config().baseUrl}/`); cy.contains("h1", "Planning de la semaine").should("be.visible"); }); - it("shows the signed-in user's name and lets them log out from the sidebar", () => { + it("shows the signed-in user's name and lets them log out from the account menu", () => { cy.intercept("POST", "**/auth/logout", { statusCode: 204 }).as("logout"); - cy.contains("Bonjour Alice").should("be.visible"); + cy.contains("button", "Bonjour Alice").should("be.visible").click(); cy.contains("button", "Se déconnecter").click(); cy.wait("@logout"); diff --git a/apps/web/cypress/e2e/household-settings.cy.ts b/apps/web/cypress/e2e/household-settings.cy.ts new file mode 100644 index 0000000..eb4acd1 --- /dev/null +++ b/apps/web/cypress/e2e/household-settings.cy.ts @@ -0,0 +1,185 @@ +// Mocks the API via cy.intercept — see auth.cy.ts for the rationale. + +const adminProfile = { + id: 1, + firstName: "Alice", + lastName: "Martin", + email: "alice@example.com", + tokenVersion: 0, + houseId: null as number | null, + dietId: null, +}; + +const houseWithTwoMembers = { + id: 1, + name: "Chez Alice", + adminId: 1, + inviteCode: "ABCD2345", + members: [ + { id: 1, firstName: "Alice", lastName: "Martin" }, + { id: 2, firstName: "Bob", lastName: "Dupont" }, + ], +}; + +describe("Household settings (/parametres/foyer) — no household yet", () => { + beforeEach(() => { + cy.intercept("GET", "**/auth/me", { statusCode: 200, body: adminProfile }); + cy.intercept("GET", "**/house/current", { statusCode: 200, body: null }); + }); + + it("offers to create or join a household", () => { + cy.visit("/parametres/foyer"); + + cy.contains("Créer un foyer").should("be.visible"); + cy.contains("Rejoindre un foyer").should("be.visible"); + }); + + it("creates a household", () => { + const createdHouse = { + id: 1, + name: "Chez Alice", + adminId: 1, + inviteCode: "ABCD2345", + members: [{ id: 1, firstName: "Alice", lastName: "Martin" }], + }; + // The page reloads `GET /house/current` right after creating succeeds — + // see the "deletes the household" test above for the same pattern. + let created = false; + cy.intercept("GET", "**/house/current", (req) => { + req.reply({ statusCode: 200, body: created ? createdHouse : null }); + }); + cy.intercept("POST", "**/house", (req) => { + created = true; + req.reply({ statusCode: 201, body: createdHouse }); + }).as("createHouse"); + + cy.visit("/parametres/foyer"); + cy.get("#houseName").type("Chez Alice"); + cy.contains("button", "Créer").click(); + + cy.wait("@createHouse").its("request.body").should("deep.equal", { name: "Chez Alice" }); + cy.contains("ABCD2345").should("be.visible"); + }); + + it("joins a household by invite code", () => { + let joined = false; + cy.intercept("GET", "**/house/current", (req) => { + req.reply({ statusCode: 200, body: joined ? houseWithTwoMembers : null }); + }); + cy.intercept("POST", "**/house/join", (req) => { + joined = true; + req.reply({ statusCode: 200, body: houseWithTwoMembers }); + }).as("joinHouse"); + + cy.visit("/parametres/foyer"); + cy.get("#inviteCode").type("abcd2345"); + cy.contains("button", "Rejoindre").click(); + + cy.wait("@joinHouse").its("request.body").should("deep.equal", { inviteCode: "ABCD2345" }); + cy.contains("Bob Dupont").should("be.visible"); + }); +}); + +describe("Household settings (/parametres/foyer) — as the admin", () => { + beforeEach(() => { + cy.intercept("GET", "**/auth/me", { statusCode: 200, body: { ...adminProfile, houseId: 1 } }); + cy.intercept("GET", "**/house/current", { statusCode: 200, body: houseWithTwoMembers }); + }); + + it("shows the household's name, invite code, and members with an admin badge", () => { + cy.visit("/parametres/foyer"); + + cy.get("#houseName").should("have.value", "Chez Alice"); + cy.contains("ABCD2345").should("be.visible"); + cy.contains("Bob Dupont").should("be.visible"); + cy.contains("Alice Martin").parent().contains("Admin"); + }); + + it("autosaves the household name", () => { + cy.intercept("PATCH", "**/house/current", { + statusCode: 200, + body: { ...houseWithTwoMembers, name: "Chez les Martin" }, + }).as("renameHouse"); + + cy.visit("/parametres/foyer"); + cy.get("#houseName").clear(); + cy.get("#houseName").type("Chez les Martin"); + + cy.wait("@renameHouse").its("request.body").should("deep.equal", { name: "Chez les Martin" }); + cy.contains("Enregistré ✓").should("be.visible"); + }); + + it("removes a member", () => { + // Same reasoning as the "deletes the household" test below — the page + // reloads `GET /house/current` right after the removal succeeds. + let memberRemoved = false; + cy.intercept("GET", "**/house/current", (req) => { + const body = memberRemoved + ? { ...houseWithTwoMembers, members: [houseWithTwoMembers.members[0]] } + : houseWithTwoMembers; + req.reply({ statusCode: 200, body }); + }); + cy.intercept("DELETE", "**/house/members/2", (req) => { + memberRemoved = true; + req.reply({ + statusCode: 200, + body: { ...houseWithTwoMembers, members: [houseWithTwoMembers.members[0]] }, + }); + }).as("removeMember"); + + cy.visit("/parametres/foyer"); + cy.contains("li", "Bob Dupont").contains("button", "Retirer").click(); + + cy.wait("@removeMember"); + cy.contains("Bob Dupont").should("not.exist"); + }); + + it("deletes the household after confirming", () => { + // The page reloads `GET /house/current` right after the delete + // succeeds — this intercept needs to answer differently before/after + // that DELETE, hence the shared mutable flag rather than two static + // `cy.intercept` calls (the later one would just win for every request, + // including the initial page load). + let houseDeleted = false; + cy.intercept("GET", "**/house/current", (req) => { + req.reply({ statusCode: 200, body: houseDeleted ? null : houseWithTwoMembers }); + }); + cy.intercept("DELETE", "**/house/current", (req) => { + houseDeleted = true; + req.reply({ statusCode: 204 }); + }).as("deleteHouse"); + + cy.visit("/parametres/foyer"); + cy.contains("button", "Supprimer le foyer").click(); + cy.contains("button", "Confirmer la suppression").click(); + + cy.wait("@deleteHouse"); + cy.contains("Créer un foyer").should("be.visible"); + }); +}); + +describe("Household settings (/parametres/foyer) — as a non-admin member", () => { + beforeEach(() => { + cy.intercept("GET", "**/auth/me", { + statusCode: 200, + body: { ...adminProfile, id: 2, firstName: "Bob", lastName: "Dupont", houseId: 1 }, + }); + cy.intercept("GET", "**/house/current", { statusCode: 200, body: houseWithTwoMembers }); + }); + + it("offers to leave the household instead of deleting it", () => { + cy.visit("/parametres/foyer"); + + cy.contains("button", "Quitter le foyer").should("be.visible"); + cy.contains("button", "Supprimer le foyer").should("not.exist"); + }); + + it("leaves the household", () => { + cy.intercept("POST", "**/house/leave", { statusCode: 204 }).as("leaveHouse"); + + cy.visit("/parametres/foyer"); + cy.contains("button", "Quitter le foyer").click(); + + cy.wait("@leaveHouse"); + }); +}); diff --git a/apps/web/cypress/e2e/onboarding.cy.ts b/apps/web/cypress/e2e/onboarding.cy.ts index 39dbd0f..8906290 100644 --- a/apps/web/cypress/e2e/onboarding.cy.ts +++ b/apps/web/cypress/e2e/onboarding.cy.ts @@ -8,22 +8,27 @@ const signupResponse = { lastName: "Martin", email: "alice@example.com", tokenVersion: 0, - houseId: 1, + houseId: null as number | null, dietId: null, }; -describe("Onboarding wizard (household → regime → allergens)", () => { - it("walks through all three steps after signup and lands on the home", () => { - cy.intercept("GET", "**/auth/me", { statusCode: 401 }); - cy.intercept("POST", "**/auth/signup", { statusCode: 201, body: signupResponse }).as("signup"); - cy.intercept("GET", "**/house/current", { - statusCode: 200, - body: { id: 1, name: "Foyer de Alice" }, - }); - cy.intercept("PATCH", "**/house/current", { - statusCode: 200, - body: { id: 1, name: "Chez Alice" }, - }).as("renameHouse"); +/** Signs up and lands on the wizard's first step (regime) — shared setup for every scenario below. */ +function signupAndReachOnboarding() { + cy.intercept("GET", "**/auth/me", { statusCode: 401 }); + cy.intercept("POST", "**/auth/signup", { statusCode: 201, body: signupResponse }).as("signup"); + cy.intercept("GET", "**/planning/current", { statusCode: 200, body: null }); + + cy.visit("/signup"); + cy.get("#firstName").type("Alice"); + cy.get("#lastName").type("Martin"); + cy.get("#email").type("alice@example.com"); + cy.get("#password").type("correct-horse-battery-staple"); + cy.contains("button", "Créer mon profil").click(); + cy.wait("@signup"); +} + +describe("Onboarding wizard (regime → foyer → allergens)", () => { + it("walks through all three steps, creating a household on the way, and lands on the home", () => { cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [ @@ -35,6 +40,11 @@ describe("Onboarding wizard (household → regime → allergens)", () => { statusCode: 200, body: { ...signupResponse, dietId: 2 }, }).as("updateDiet"); + cy.intercept("GET", "**/house/current", { statusCode: 200, body: null }); + cy.intercept("POST", "**/house", { + statusCode: 201, + body: { id: 1, name: "Chez Alice", adminId: 1, inviteCode: "ABCD2345", members: [] }, + }).as("createHouse"); cy.intercept("GET", "**/reference/allergies", { statusCode: 200, body: [ @@ -45,32 +55,23 @@ describe("Onboarding wizard (household → regime → allergens)", () => { cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [1] }).as( "updateAllergies", ); - cy.intercept("GET", "**/planning/current", { statusCode: 200, body: null }); - cy.visit("/signup"); - cy.get("#firstName").type("Alice"); - cy.get("#lastName").type("Martin"); - cy.get("#email").type("alice@example.com"); - cy.get("#password").type("correct-horse-battery-staple"); - cy.contains("button", "Créer mon profil").click(); - cy.wait("@signup"); + signupAndReachOnboarding(); - // Step 1/3 — household name, prefilled with the auto-generated default. - cy.url().should("include", "/onboarding/foyer"); - cy.contains("Étape 1 sur 3").should("be.visible"); - cy.get("#houseName").should("have.value", "Foyer de Alice"); - cy.get("#houseName").clear(); - cy.get("#houseName").type("Chez Alice"); - cy.contains("button", "Continuer").click(); - cy.wait("@renameHouse").its("request.body").should("deep.equal", { name: "Chez Alice" }); - - // Step 2/3 — dietary regime. + // Step 1/3 — dietary regime. cy.url().should("include", "/onboarding/regime"); - cy.contains("Étape 2 sur 3").should("be.visible"); + cy.contains("Étape 1 sur 3").should("be.visible"); cy.get("#diet").select("Végétarien"); cy.contains("button", "Continuer").click(); cy.wait("@updateDiet").its("request.body").should("deep.equal", { dietId: 2 }); + // Step 2/3 — household, optional: creating one here. + cy.url().should("include", "/onboarding/foyer"); + cy.contains("Étape 2 sur 3").should("be.visible"); + cy.get("#houseName").type("Chez Alice"); + cy.contains("button", "Créer").click(); + cy.wait("@createHouse").its("request.body").should("deep.equal", { name: "Chez Alice" }); + // Step 3/3 — allergens (grouped into two lists) and intolerances, then finish. cy.url().should("include", "/onboarding/allergenes"); cy.contains("Étape 3 sur 3").should("be.visible"); @@ -86,46 +87,62 @@ describe("Onboarding wizard (household → regime → allergens)", () => { cy.contains("h1", "Planning de la semaine").should("be.visible"); }); - it("lets every step be skipped without changing anything", () => { - cy.intercept("GET", "**/auth/me", { statusCode: 401 }); - cy.intercept("POST", "**/auth/signup", { statusCode: 201, body: signupResponse }); - cy.intercept("GET", "**/house/current", { - statusCode: 200, - body: { id: 1, name: "Foyer de Alice" }, - }); - cy.intercept("PATCH", "**/house/current", { - statusCode: 200, - body: { id: 1, name: "Foyer de Alice" }, - }).as("renameHouse"); + it("lets the regime and allergens steps be skipped without changing anything", () => { cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [] }); cy.intercept("PATCH", "**/profile/diet", { statusCode: 200, body: signupResponse }).as( "updateDiet", ); + cy.intercept("GET", "**/house/current", { statusCode: 200, body: null }); cy.intercept("GET", "**/reference/allergies", { statusCode: 200, body: [] }); cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [] }).as( "updateAllergies", ); - cy.intercept("GET", "**/planning/current", { statusCode: 200, body: null }); - cy.visit("/signup"); - cy.get("#firstName").type("Alice"); - cy.get("#lastName").type("Martin"); - cy.get("#email").type("alice@example.com"); - cy.get("#password").type("correct-horse-battery-staple"); - cy.contains("button", "Créer mon profil").click(); - - cy.url().should("include", "/onboarding/foyer"); - cy.contains("button", "Continuer").click(); - cy.wait("@renameHouse").its("request.body").should("deep.equal", { name: "Foyer de Alice" }); + signupAndReachOnboarding(); cy.url().should("include", "/onboarding/regime"); cy.contains("button", "Continuer").click(); cy.wait("@updateDiet").its("request.body").should("deep.equal", { dietId: null }); + cy.url().should("include", "/onboarding/foyer"); + cy.contains("button", "Passer cette étape").click(); + cy.url().should("include", "/onboarding/allergenes"); cy.contains("button", "Terminer").click(); cy.wait("@updateAllergies").its("request.body").should("deep.equal", { allergyIds: [] }); cy.url().should("eq", `${Cypress.config().baseUrl}/`); }); + + it("lets the household step be completed by joining an existing household instead of creating one", () => { + cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [] }); + cy.intercept("PATCH", "**/profile/diet", { statusCode: 200, body: signupResponse }); + cy.intercept("GET", "**/house/current", { statusCode: 200, body: null }); + cy.intercept("POST", "**/house/join", { + statusCode: 200, + body: { + id: 1, + name: "Chez Bob", + adminId: 2, + inviteCode: "ABCD2345", + members: [ + { id: 1, firstName: "Alice", lastName: "Martin" }, + { id: 2, firstName: "Bob", lastName: "Dupont" }, + ], + }, + }).as("joinHouse"); + cy.intercept("GET", "**/reference/allergies", { statusCode: 200, body: [] }); + cy.intercept("PATCH", "**/profile/allergies", { statusCode: 200, body: [] }); + + signupAndReachOnboarding(); + + cy.contains("button", "Continuer").click(); + cy.url().should("include", "/onboarding/foyer"); + + cy.get("#inviteCode").type("abcd2345"); + cy.contains("button", "Rejoindre").click(); + + cy.wait("@joinHouse").its("request.body").should("deep.equal", { inviteCode: "ABCD2345" }); + cy.url().should("include", "/onboarding/allergenes"); + }); }); diff --git a/apps/web/cypress/e2e/household.cy.ts b/apps/web/cypress/e2e/preferences.cy.ts similarity index 62% rename from apps/web/cypress/e2e/household.cy.ts rename to apps/web/cypress/e2e/preferences.cy.ts index 9255797..698a219 100644 --- a/apps/web/cypress/e2e/household.cy.ts +++ b/apps/web/cypress/e2e/preferences.cy.ts @@ -10,13 +10,9 @@ const authenticatedProfile = { dietId: 2, }; -describe("Household & profile settings (/foyer) — hot saving", () => { +describe("Dietary preferences (/parametres/preferences) — hot saving", () => { beforeEach(() => { cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); - cy.intercept("GET", "**/house/current", { - statusCode: 200, - body: { id: 1, name: "Chez Alice" }, - }); cy.intercept("GET", "**/reference/diets", { statusCode: 200, body: [ @@ -34,10 +30,9 @@ describe("Household & profile settings (/foyer) — hot saving", () => { cy.intercept("GET", "**/profile/allergies", { statusCode: 200, body: [2] }); }); - it("loads the current household name, regime, and shows allergies/intolerances as two groups", () => { - cy.visit("/foyer"); + it("loads the current regime, and shows allergies/intolerances as two groups", () => { + cy.visit("/parametres/preferences"); - cy.get("#houseName").should("have.value", "Chez Alice"); cy.get("#diet").should("have.value", "2"); cy.contains("legend", "Allergies").should("be.visible"); cy.contains("legend", "Intolérances").should("be.visible"); @@ -46,41 +41,17 @@ describe("Household & profile settings (/foyer) — hot saving", () => { }); it("has no explicit save button anywhere on the page", () => { - cy.visit("/foyer"); + cy.visit("/parametres/preferences"); cy.contains("button", "Enregistrer").should("not.exist"); }); - it("autosaves the household name a short pause after typing, no button click", () => { - cy.intercept("PATCH", "**/house/current", { - statusCode: 200, - body: { id: 1, name: "Chez les Martin" }, - }).as("renameHouse"); - - cy.visit("/foyer"); - cy.get("#houseName").clear(); - cy.get("#houseName").type("Chez les Martin"); - - cy.wait("@renameHouse").its("request.body").should("deep.equal", { name: "Chez les Martin" }); - cy.contains("Enregistré ✓").should("be.visible"); - }); - - it("does not autosave an empty household name — shows a validation message instead", () => { - cy.intercept("PATCH", "**/house/current").as("renameHouse"); - - cy.visit("/foyer"); - cy.get("#houseName").clear(); - - cy.contains("Le nom du foyer est requis").should("be.visible"); - cy.get("@renameHouse.all").should("have.length", 0); - }); - it("autosaves the regime as soon as it's selected", () => { cy.intercept("PATCH", "**/profile/diet", { statusCode: 200, body: { ...authenticatedProfile, dietId: 1 }, }).as("updateDiet"); - cy.visit("/foyer"); + cy.visit("/parametres/preferences"); cy.get("#diet").select("Omnivore"); cy.wait("@updateDiet").its("request.body").should("deep.equal", { dietId: 1 }); @@ -91,11 +62,12 @@ describe("Household & profile settings (/foyer) — hot saving", () => { "updateAllergies", ); - cy.visit("/foyer"); + cy.visit("/parametres/preferences"); cy.contains("label", "Arachides").find("input[type=checkbox]").check(); cy.wait("@updateAllergies") .its("request.body") .should("deep.equal", { allergyIds: [2, 1] }); + cy.contains("Enregistré ✓").should("be.visible"); }); }); diff --git a/apps/web/cypress/e2e/sidebar.cy.ts b/apps/web/cypress/e2e/sidebar.cy.ts new file mode 100644 index 0000000..07295d2 --- /dev/null +++ b/apps/web/cypress/e2e/sidebar.cy.ts @@ -0,0 +1,50 @@ +// Mocks the API via cy.intercept — see auth.cy.ts for the rationale. + +const authenticatedProfile = { + id: 1, + firstName: "Alice", + lastName: "Martin", + email: "alice@example.com", + tokenVersion: 0, + houseId: null, + dietId: null, +}; + +describe("Sidebar — settings menu and account menu", () => { + beforeEach(() => { + cy.intercept("GET", "**/auth/me", { statusCode: 200, body: authenticatedProfile }); + cy.intercept("GET", "**/planning/current", { statusCode: 200, body: null }); + }); + + it("no longer lists Foyer in the main nav", () => { + cy.visit("/"); + cy.get(".app-sidebar__nav a").should("not.contain", "Foyer"); + }); + + it("reveals the three settings pages behind the Paramètres toggle", () => { + cy.visit("/"); + + cy.contains("a", "Compte").should("not.exist"); + cy.contains("button", "Paramètres").click(); + + cy.contains("a", "Compte").should("have.attr", "href", "/parametres/compte"); + cy.contains("a", "Préférences").should("have.attr", "href", "/parametres/preferences"); + cy.contains("a", "Foyer").should("have.attr", "href", "/parametres/foyer"); + }); + + it("opens the account menu from the greeting and links to Mon compte", () => { + cy.visit("/"); + + cy.contains("a", "Mon compte").should("not.exist"); + cy.contains("button", "Bonjour Alice").click(); + cy.contains("a", "Mon compte").click(); + + cy.url().should("include", "/parametres/compte"); + }); + + it("redirects the old /foyer path to /parametres/foyer", () => { + cy.intercept("GET", "**/house/current", { statusCode: 200, body: null }); + cy.visit("/foyer"); + cy.url().should("include", "/parametres/foyer"); + }); +});