batchCooking/apps/web/cypress/e2e/admin-tours.cy.ts
Nicolas b39105ac27
All checks were successful
CI / lint (push) Successful in 2m10s
CI / build (push) Successful in 2m19s
CI / intent-service-test (push) Successful in 4m41s
CI / e2e (push) Successful in 7m19s
CI / test (push) Successful in 16m20s
feat(web): page admin de CRUD des didacticiels (#44)
L'UI par-dessus l'API `/admin/api/tours*`. Nouvelle entrée
`/admin/didacticiels` dans `AdminLayout` (i18n `admin.nav.tours`), route
sous `RequireAdmin`+`AdminLayout` dans `App.tsx`.

`ToursPage.tsx` : liste des scénarios à gauche (nom, état activé/désactivé,
nombre d'étapes) ; à droite l'éditeur du scénario sélectionné —
- renommer, activer/désactiver ;
- éditer la liste d'étapes (route, cible via `<select>` alimenté par
  `GET /tours/targets`, titre, contenu), réordonner (↑/↓), ajouter,
  retirer ;
- « Enregistrer » → un seul `PATCH /:id` qui remplace tout (name/enabled +
  étapes rénumérotées `order` = index), erreurs API affichées telles quelles ;
- « Supprimer le scénario » (confirmation `window.confirm`) ;
- « Nouveau scénario » → formulaire clé + nom → `POST /`.

`admin-client.ts` gagne `getTourScenarios` / `getTourTargets` /
`createTourScenario` / `updateTourScenario` / `deleteTourScenario`.

Tests Cypress (`admin-tours.cy.ts`, 4) : liste (activés + désactivés,
nombre d'étapes) ; édition nom + réordonnancement d'étapes → un seul PATCH
avec la liste complète réordonnée ; création → POST `{key,name}` ;
suppression après confirmation → DELETE. 4/4 en local ; `pnpm build` +
`pnpm lint` verts.

Closes #44
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 06:53:38 +02:00

129 lines
4.3 KiB
TypeScript

// Mocks the admin API via cy.intercept — no live backend. Covers the
// `/admin/didacticiels` CRUD page (issue #44): list, edit-and-save a
// scenario's steps in one block PATCH, create a scenario, delete one.
const adminBody = {
id: 1,
email: "ops@example.com",
name: "Ops",
createdAt: "2026-08-01T00:00:00.000Z",
lastLoginAt: "2026-08-28T09:00:00.000Z",
};
const TARGETS = [
{ key: "nav-planning", description: "Lien « Planning » de la barre latérale" },
{ key: "planning-grid", description: "La grille du planning de la semaine" },
{ key: "planning-cook-button", description: "Bouton « Cuisiner » de l'en-tête du planning" },
];
function scenarios() {
return [
{
id: 1,
key: "general",
name: "Tour d'orientation",
enabled: true,
steps: [
{
order: 0,
route: "/",
targetKey: "nav-planning",
title: "Bienvenue",
content: "Le planning.",
},
{
order: 1,
route: null,
targetKey: "planning-grid",
title: "La grille",
content: "Sept jours.",
},
],
},
{ id: 2, key: "techStepMetadata", name: "Corriger les techniques", enabled: false, steps: [] },
];
}
describe("Admin — didacticiels CRUD", () => {
beforeEach(() => {
cy.viewport(1400, 900);
cy.intercept("GET", "**/admin/api/auth/me", { statusCode: 200, body: adminBody });
cy.intercept("GET", "**/admin/api/tours", { statusCode: 200, body: scenarios() }).as("list");
cy.intercept("GET", "**/admin/api/tours/targets", { statusCode: 200, body: TARGETS });
});
it("lists every scenario, enabled and disabled, with its step count", () => {
cy.visit("/admin/didacticiels");
cy.wait("@list");
cy.contains(".tours-page__list button", "Tour d'orientation").should(
"contain.text",
"2 étapes",
);
cy.contains(".tours-page__list button", "Corriger les techniques").should(
"contain.text",
"désactivé",
);
});
it("edits a scenario's name and steps and saves the whole list in one PATCH", () => {
cy.intercept("PATCH", "**/admin/api/tours/1", (req) => {
req.reply({ statusCode: 200, body: { ...scenarios()[0], name: req.body.name } });
}).as("save");
cy.visit("/admin/didacticiels");
cy.wait("@list");
cy.contains(".tours-page__list button", "Tour d'orientation").click();
cy.get('[data-testid="scenario-editor"]').within(() => {
cy.contains("label", "Nom").find("input").clear().type("Tour d'accueil");
// Reorder: push step 1 down so "La grille" comes first.
cy.get(".tours-page__step").first().contains("button", "↓").click();
cy.contains("button", "Enregistrer").click();
});
cy.wait("@save")
.its("request.body")
.should((body) => {
expect(body.name).to.equal("Tour d'accueil");
expect(body.enabled).to.equal(true);
expect(body.steps).to.have.length(2);
// Reordered: the grid step is now first, nav-planning second.
expect(body.steps[0].targetKey).to.equal("planning-grid");
expect(body.steps[1].targetKey).to.equal("nav-planning");
});
});
it("creates a new scenario", () => {
cy.intercept("POST", "**/admin/api/tours", (req) => {
req.reply({
statusCode: 201,
body: { id: 3, key: req.body.key, name: req.body.name, enabled: true, steps: [] },
});
}).as("create");
cy.visit("/admin/didacticiels");
cy.wait("@list");
cy.contains("button", "Nouveau scénario").click();
cy.contains("label", "Clé").find("input").type("shoppingList");
cy.contains("label", "Nom").find("input").type("Tour de la liste de courses");
cy.contains("button", "Créer").click();
cy.wait("@create")
.its("request.body")
.should("deep.equal", { key: "shoppingList", name: "Tour de la liste de courses" });
});
it("deletes a scenario after confirmation", () => {
cy.intercept("DELETE", "**/admin/api/tours/1", { statusCode: 204 }).as("del");
cy.on("window:confirm", () => true);
cy.visit("/admin/didacticiels");
cy.wait("@list");
cy.contains(".tours-page__list button", "Tour d'orientation").click();
cy.get('[data-testid="scenario-editor"]').contains("button", "Supprimer le scénario").click();
cy.wait("@del");
});
});