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>
129 lines
4.3 KiB
TypeScript
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");
|
|
});
|
|
});
|