- ApiClient.getCurrentPlanning() — GET /planning/current.
- HomePage.tsx: replaces the old greeting card (now redundant with
AppLayout's sidebar) with the household's current planning — loading /
error / empty ("aucun planning pour cette semaine") / loaded (table of
weekDay/meal/recipe) states, modeled as a discriminated union so an
impossible combination (e.g. loading with data) can't be represented.
No invented weekday/meal grid — the API's `weekDay`/`meal` are free-form
strings (no enum exists yet in the schema), so this renders the items
as returned rather than assuming a specific vocabulary.
- locales/fr/translation.json: home.* replaced (title/loading/error/
empty/table.*), old greeting/logout keys removed (superseded by
layout.greeting/layout.logout from the AppLayout commit).
- cypress/e2e/auth.cy.ts: updated the now-stale "Bonjour Alice Martin"
assertions (greeting moved to the sidebar, first name only) and added
GET /planning/current intercepts so these specs don't depend on a real
backend. Manually verified end-to-end against a real API in the browser
preview (empty state + a seeded planning) — Cypress itself can't run
headless Chromium in this sandboxed dev environment (confirmed
pre-existing on main, unrelated to this change); CI runs the real
suite.
Verified with `pnpm --filter web build` after each of steps 2-4 to keep
every commit in this sequence independently buildable.
157 lines
4.9 KiB
TypeScript
157 lines
4.9 KiB
TypeScript
import { ErrorCode } from "@batch-cooking/shared";
|
|
|
|
// Mocks the API via cy.intercept — this job doesn't run a live backend (see
|
|
// .github/workflows/ci.yml), and it keeps these specs focused on frontend
|
|
// behavior. Backend behavior itself is covered by apps/api's Mocha/Cucumber
|
|
// suites against a real database.
|
|
|
|
describe("Signup", () => {
|
|
it("creates a profile and lands on the home page", () => {
|
|
cy.intercept("GET", "**/auth/me", { statusCode: 401 });
|
|
cy.intercept("GET", "**/planning/current", { statusCode: 200, body: null });
|
|
cy.intercept("POST", "**/auth/signup", {
|
|
statusCode: 201,
|
|
body: {
|
|
id: 1,
|
|
firstName: "Alice",
|
|
lastName: "Martin",
|
|
email: "alice@example.com",
|
|
tokenVersion: 0,
|
|
houseId: 1,
|
|
dietId: null,
|
|
},
|
|
}).as("signup");
|
|
|
|
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");
|
|
cy.url().should("not.include", "/signup");
|
|
cy.contains("Bonjour Alice").should("be.visible");
|
|
});
|
|
|
|
it("shows a client-side validation error without calling the API", () => {
|
|
cy.intercept("GET", "**/auth/me", { statusCode: 401 });
|
|
cy.intercept("POST", "**/auth/signup").as("signup");
|
|
|
|
cy.visit("/signup");
|
|
cy.get("#firstName").type("A");
|
|
cy.get("#lastName").type("B");
|
|
cy.get("#email").type("a@example.com");
|
|
cy.get("#password").type("short");
|
|
cy.contains("button", "Créer mon profil").click();
|
|
|
|
cy.contains("8 caractères minimum").should("be.visible");
|
|
cy.get("@signup.all").should("have.length", 0);
|
|
});
|
|
|
|
it("shows the API's error when the email is already taken", () => {
|
|
cy.intercept("GET", "**/auth/me", { statusCode: 401 });
|
|
cy.intercept("POST", "**/auth/signup", {
|
|
statusCode: 409,
|
|
body: { code: ErrorCode.EMAIL_ALREADY_IN_USE, message: "Email already in use" },
|
|
}).as("signup");
|
|
|
|
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");
|
|
cy.contains("Cet email est déjà utilisé").should("be.visible");
|
|
});
|
|
});
|
|
|
|
describe("Login", () => {
|
|
it("logs in and lands on the home page", () => {
|
|
cy.intercept("GET", "**/auth/me", { statusCode: 401 });
|
|
cy.intercept("GET", "**/planning/current", { statusCode: 200, body: null });
|
|
cy.intercept("POST", "**/auth/login", {
|
|
statusCode: 200,
|
|
body: {
|
|
id: 1,
|
|
firstName: "Alice",
|
|
lastName: "Martin",
|
|
email: "alice@example.com",
|
|
tokenVersion: 0,
|
|
houseId: 1,
|
|
dietId: null,
|
|
},
|
|
}).as("login");
|
|
|
|
cy.visit("/login");
|
|
cy.get("#email").type("alice@example.com");
|
|
cy.get("#password").type("correct-horse-battery-staple");
|
|
cy.contains("button", "Se connecter").click();
|
|
|
|
cy.wait("@login");
|
|
cy.contains("Bonjour Alice").should("be.visible");
|
|
});
|
|
|
|
it("shows an error on invalid credentials", () => {
|
|
cy.intercept("GET", "**/auth/me", { statusCode: 401 });
|
|
cy.intercept("POST", "**/auth/login", {
|
|
statusCode: 401,
|
|
body: { code: ErrorCode.INVALID_CREDENTIALS, message: "Invalid email or password" },
|
|
}).as("login");
|
|
|
|
cy.visit("/login");
|
|
cy.get("#email").type("alice@example.com");
|
|
cy.get("#password").type("wrong-password");
|
|
cy.contains("button", "Se connecter").click();
|
|
|
|
cy.wait("@login");
|
|
cy.contains("Email ou mot de passe incorrect").should("be.visible");
|
|
});
|
|
});
|
|
|
|
describe("Already authenticated", () => {
|
|
it("redirects away from /login to the home page", () => {
|
|
cy.intercept("GET", "**/auth/me", {
|
|
statusCode: 200,
|
|
body: {
|
|
id: 1,
|
|
firstName: "Alice",
|
|
lastName: "Martin",
|
|
email: "alice@example.com",
|
|
tokenVersion: 0,
|
|
houseId: 1,
|
|
dietId: null,
|
|
},
|
|
});
|
|
cy.intercept("GET", "**/planning/current", { statusCode: 200, body: null });
|
|
|
|
cy.visit("/login");
|
|
cy.url().should("not.include", "/login");
|
|
cy.contains("Bonjour Alice").should("be.visible");
|
|
});
|
|
|
|
it("logs out and returns to the login page", () => {
|
|
cy.intercept("GET", "**/auth/me", {
|
|
statusCode: 200,
|
|
body: {
|
|
id: 1,
|
|
firstName: "Alice",
|
|
lastName: "Martin",
|
|
email: "alice@example.com",
|
|
tokenVersion: 0,
|
|
houseId: 1,
|
|
dietId: null,
|
|
},
|
|
});
|
|
cy.intercept("GET", "**/planning/current", { statusCode: 200, body: null });
|
|
cy.intercept("POST", "**/auth/logout", { statusCode: 204 }).as("logout");
|
|
|
|
cy.visit("/");
|
|
cy.contains("button", "Se déconnecter").click();
|
|
|
|
cy.wait("@logout");
|
|
cy.url().should("include", "/login");
|
|
});
|
|
});
|