Consomme le socle backend de #42/#44 (`GET /reference/tours`, `GET/POST /profile/tours`). Deux tours : orientation générale (auto sur `/`) et correction des métadonnées de recette (auto à l'ouverture du premier popover de correction). - Dépendance `driver.js` (~5 ko, MIT, zéro dépendance). - `features/tour/tour-engine.ts` : wrapper vanilla autour de `driver.js` — gère ce que la lib ne fait pas seule : la navigation React Router entre étapes situées sur des routes différentes, et un `onFinish` unique (`"completed"` seulement si on atteint la fin, `"skipped"` pour la croix / Échap / clic sur l'overlay). - `features/tour/TourContext.tsx` : `TourProvider` (charge scénarios + progression, keyed sur `user?.id` comme `ThemeContext`) et `useTour()` (`startTour(key)` / `isTourCompleted(key)` / `tours` / `isReady`). `isReady` est **calculé au render** contre l'id utilisateur chargé — pas stocké — pour qu'un changement de `user` le repasse à `false` immédiatement, avant qu'un effet enfant (l'auto-déclenchement) ne tourne sur des données de complétion périmées. - `main.tsx` : `<TourProvider>` sous `ThemeProvider`. - Attributs `data-tour-*` : liens de nav (`AppLayout`), grille / bouton `+` du 1er créneau / bouton Cuisiner (`PlanningPage`), page liste de courses (`ShoppingListPage`), description d'étape + section technique du popover (`StepDescription` / `TechStepCorrectionPopover`). - Déclenchement auto : tour `general` sur `/` (`PlanningPage`, ref-gardé), tour `techStepMetadata` à `activeCorrection` non-null (`StepDescription`). - Section « Revoir un tutoriel » dans `/parametres/preferences-utilisateur` — un `map` sur `tours`, un bouton « Revoir » par scénario. - `tour.scss` re-skinne le popover `driver.js` sur les tokens du thème. - i18n `tour.*` (chrome uniquement : boutons du popover, section « Revoir » ; le contenu des étapes vient de la base). Tests Cypress (`tour.feature`, 5 scénarios) : auto-démarrage + élément surligné pour un nouveau venu ; « Terminé » → complétion enregistrée `skipped:false` ; croix → `skipped:true` ; un tour déjà vu ne se relance pas ; « Revoir » le relance depuis les paramètres. Vérifié 5/5 en local. `pnpm build` + `pnpm lint` verts. Refs #42 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
79 lines
2.9 KiB
TypeScript
79 lines
2.9 KiB
TypeScript
import { Given, Then, When } from "@badeball/cypress-cucumber-preprocessor";
|
|
|
|
// Guided-tour steps (issue #42). Mocks the API via `cy.intercept`, like the
|
|
// rest of the suite. `I am signed in as` / `the planning request returns
|
|
// nothing` / `I visit` come from the shared common.steps.ts.
|
|
|
|
const GENERAL_STEPS = [
|
|
{ order: 0, route: "/", targetKey: "nav-planning", title: "Votre semaine, ici", content: "..." },
|
|
{ order: 1, route: null, targetKey: "nav-recipes", title: "Vos recettes", content: "..." },
|
|
{
|
|
order: 2,
|
|
route: null,
|
|
targetKey: "nav-shopping-list",
|
|
title: "La liste de courses",
|
|
content: "...",
|
|
},
|
|
];
|
|
|
|
function interceptTours(steps: typeof GENERAL_STEPS) {
|
|
cy.intercept("GET", "**/reference/tours", {
|
|
statusCode: 200,
|
|
body: [{ key: "general", name: "Tour d'orientation", steps }],
|
|
});
|
|
}
|
|
|
|
Given("the guided tour has not been seen yet", () => {
|
|
interceptTours(GENERAL_STEPS);
|
|
cy.intercept("GET", "**/profile/tours", { statusCode: 200, body: { completedTourKeys: [] } });
|
|
cy.intercept("POST", "**/profile/tours/general/complete", { statusCode: 204 }).as("complete");
|
|
});
|
|
|
|
Given("the guided tour has not been seen yet, with a single step", () => {
|
|
interceptTours([GENERAL_STEPS[0]]);
|
|
cy.intercept("GET", "**/profile/tours", { statusCode: 200, body: { completedTourKeys: [] } });
|
|
cy.intercept("POST", "**/profile/tours/general/complete", { statusCode: 204 }).as("complete");
|
|
});
|
|
|
|
Given("the guided tour has already been seen", () => {
|
|
interceptTours(GENERAL_STEPS);
|
|
cy.intercept("GET", "**/profile/tours", {
|
|
statusCode: 200,
|
|
body: { completedTourKeys: ["general"] },
|
|
});
|
|
cy.intercept("POST", "**/profile/tours/general/complete", { statusCode: 204 }).as("complete");
|
|
});
|
|
|
|
Then("the tour popover shows {string}", (title: string) => {
|
|
cy.get(".driver-popover.tour-popover .driver-popover-title").should("contain.text", title);
|
|
});
|
|
|
|
Then("the sidebar {string} link is highlighted by the tour", (label: string) => {
|
|
cy.contains(".app-sidebar__nav a", label).should("have.class", "driver-active-element");
|
|
});
|
|
|
|
When("I click the tour's next button", () => {
|
|
cy.get(".driver-popover.tour-popover .driver-popover-next-btn").click();
|
|
});
|
|
|
|
When("I dismiss the tour", () => {
|
|
cy.get(".driver-popover.tour-popover .driver-popover-close-btn").click();
|
|
});
|
|
|
|
Then("the tour completion was recorded as finished", () => {
|
|
cy.wait("@complete").its("request.body").should("deep.equal", { skipped: false });
|
|
});
|
|
|
|
Then("the tour completion was recorded as skipped", () => {
|
|
cy.wait("@complete").its("request.body").should("deep.equal", { skipped: true });
|
|
});
|
|
|
|
Then("no tour popover is shown", () => {
|
|
// Give the auto-trigger effect a chance to (not) fire.
|
|
cy.wait(300);
|
|
cy.get(".driver-popover").should("not.exist");
|
|
});
|
|
|
|
When("I click {string} next to {string}", (button: string, tourName: string) => {
|
|
cy.contains(".tour-replay-list__item", tourName).contains("button", button).click();
|
|
});
|