victoria-ynov 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # Module Test - Formulaire d'inscription React
2
+
3
+ Application React de formulaire d'inscription avec validation en temps reel des champs (date de naissance, nom, prenom, ville, code postal, email). Les donnees sont envoyees a une API REST via Axios et le compteur d'utilisateurs est recupere au chargement depuis JSONPlaceholder.
4
+
5
+ ## Pre-requis
6
+
7
+ - Node.js (v21 ou superieur)
8
+ - npm
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ git clone <url-du-repo>
14
+ cd my-app
15
+ npm install
16
+ ```
17
+
18
+ ## Lancer l'application
19
+
20
+ ```bash
21
+ npm start
22
+ ```
23
+
24
+ L'application s'ouvre sur [http://localhost:3000](http://localhost:3000).
25
+
26
+ ## Lancer les tests unitaires et d'integration
27
+
28
+ ```bash
29
+ npm test
30
+ ```
31
+
32
+ Les tests s'executent avec coverage. Le rapport de couverture est genere dans le dossier `coverage/`.
33
+
34
+ ### Strategie de mock (Jest)
35
+
36
+ Les tests d'integration utilisent `jest.mock('axios')` pour simuler les reponses de l'API sans connexion reseau reelle :
37
+
38
+ ```js
39
+ jest.mock('axios');
40
+
41
+ // Simuler un succes GET
42
+ axios.get.mockResolvedValue({ data: mockUsers });
43
+
44
+ // Simuler une erreur metier (400)
45
+ const error400 = new Error('Bad Request');
46
+ error400.response = { status: 400, data: { message: 'Cet email est deja utilise.' } };
47
+ axios.post.mockRejectedValue(error400);
48
+
49
+ // Simuler un crash serveur (500)
50
+ const error500 = new Error('Internal Server Error');
51
+ error500.response = { status: 500 };
52
+ axios.post.mockRejectedValue(error500);
53
+ ```
54
+
55
+ Cela permet de tester les trois scenarios API sans dependre de JSONPlaceholder :
56
+
57
+ | Scenario | Mock | Comportement attendu |
58
+ | -------- | ---- | -------------------- |
59
+ | Succes (201) | `mockResolvedValue` | Toast vert, compteur incremente |
60
+ | Erreur metier (400) | `mockRejectedValue` status 400 | Toast rouge avec message du back |
61
+ | Crash serveur (500) | `mockRejectedValue` status 500 | Toast rouge d'alerte, app stable |
62
+
63
+ ## Lancer les tests End-to-End (Cypress)
64
+
65
+ Les tests E2E s'executent dans un vrai navigateur contre l'application en cours d'execution.
66
+
67
+ **1. Lancer l'application** (dans un terminal)
68
+
69
+ ```bash
70
+ npm start
71
+ ```
72
+
73
+ **2. Ouvrir Cypress** (dans un autre terminal)
74
+
75
+ ```bash
76
+ npm run cypress
77
+ ```
78
+
79
+ Cypress ouvre une interface graphique. Cliquer sur `navigation.cy.js` pour lancer les tests E2E.
80
+
81
+ ### Strategie de mock (Cypress)
82
+
83
+ Les tests E2E utilisent `cy.intercept` pour bouchonner les routes API et permettre aux tests de passer sans backend reel. Chaque requete HTTP est interceptee avant d'atteindre le reseau et remplacee par une reponse fictive :
84
+
85
+ ```js
86
+ // Intercepter le GET /users (dans beforeEach : actif pour tous les tests)
87
+ cy.intercept('GET', 'https://jsonplaceholder.typicode.com/users', {
88
+ statusCode: 200,
89
+ body: mockUsers,
90
+ }).as('getUsers');
91
+
92
+ // Intercepter le POST /users avec une erreur 400
93
+ cy.intercept('POST', 'https://jsonplaceholder.typicode.com/users', {
94
+ statusCode: 400,
95
+ body: { message: 'Cet email est deja utilise.' },
96
+ }).as('postUser');
97
+ ```
98
+
99
+ **Scenarios couverts :**
100
+
101
+ - **Nominal (201)** : inscription complete, toast vert, compteur passe de 10 a 11.
102
+ - **Erreur metier (400)** : le message specifique du back s'affiche dans un toast rouge, compteur inchange.
103
+ - **Crash serveur (500)** : toast d'alerte generique, l'application ne plante pas, compteur inchange.
104
+
105
+ ## Structure du projet
106
+
107
+ ```text
108
+ my-app/
109
+ ├── src/
110
+ │ ├── pages/
111
+ │ │ ├── Home.js - Page d'accueil (compteur d'utilisateurs)
112
+ │ │ └── Register.js - Page formulaire d'inscription avec validation
113
+ │ ├── tests/
114
+ │ │ ├── home.test.js - Tests unitaires du composant Home
115
+ │ │ ├── register.test.js - Tests d'integration du formulaire (scenario chaotique)
116
+ │ │ ├── app.test.js - Tests d'integration API avec jest.mock('axios')
117
+ │ │ ├── api.test.js - Tests unitaires des fonctions Axios (countUsers, getAllUsers, postUser)
118
+ │ │ ├── module.test.js - Tests unitaires de calculateAge
119
+ │ │ └── validator.test.js - Tests unitaires des validateurs
120
+ │ ├── App.js - Composant racine avec routeur et etat global
121
+ │ ├── api.js - Fonctions Axios (countUsers, getAllUsers, postUser)
122
+ │ ├── module.js - Fonction de calcul d'age
123
+ │ └── validator.js - Fonctions de validation (age, email, CP, identite, ville)
124
+ ├── cypress/
125
+ │ └── e2e/
126
+ │ └── navigation.cy.js - Tests E2E avec cy.intercept (201, 400, 500)
127
+ └── TEST_PLAN.md - Plan de test et documentation des cas testes
128
+ ```
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ presets: [
3
+ '@babel/preset-env',
4
+ ['@babel/preset-react', { runtime: 'automatic' }],
5
+ ],
6
+ };
package/dist/App.js ADDED
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = require("react");
8
+ var _reactRouterDom = require("react-router-dom");
9
+ var _Home = _interopRequireDefault(require("./pages/Home"));
10
+ var _Register = _interopRequireDefault(require("./pages/Register"));
11
+ var _api = require("./api");
12
+ var _jsxRuntime = require("react/jsx-runtime");
13
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
+ /**
15
+ * Composant racine de l'application.
16
+ * Gère l'état global du compteur d'utilisateurs et le routage entre les pages.
17
+ * Récupère le nombre d'utilisateurs depuis l'API au chargement.
18
+ *
19
+ * @component
20
+ * @returns {JSX.Element} L'application avec le routeur
21
+ */function App() {
22
+ const [usersCount, setUsersCount] = (0, _react.useState)(0);
23
+ const [apiError, setApiError] = (0, _react.useState)('');
24
+ (0, _react.useEffect)(() => {
25
+ const fetchCount = async () => {
26
+ try {
27
+ const count = await (0, _api.countUsers)();
28
+ setUsersCount(count);
29
+ } catch (error) {
30
+ setApiError('Impossible de récupérer les utilisateurs.');
31
+ }
32
+ };
33
+ fetchCount();
34
+ }, []);
35
+
36
+ /**
37
+ * Envoie un nouvel utilisateur à l'API et incrémente le compteur.
38
+ * @param {Object} user - L'utilisateur à ajouter
39
+ * @returns {Promise<void>}
40
+ */
41
+ const addUser = async user => {
42
+ await (0, _api.postUser)(user);
43
+ setUsersCount(prev => prev + 1);
44
+ };
45
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactRouterDom.BrowserRouter, {
46
+ basename: process.env.NODE_ENV === 'production' ? '/Module-test' : '',
47
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactRouterDom.Routes, {
48
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactRouterDom.Route, {
49
+ path: "/",
50
+ element: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Home.default, {
51
+ usersCount: usersCount,
52
+ apiError: apiError
53
+ })
54
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactRouterDom.Route, {
55
+ path: "/register",
56
+ element: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Register.default, {
57
+ addUser: addUser
58
+ })
59
+ })]
60
+ })
61
+ });
62
+ }
63
+ var _default = exports.default = App;
package/dist/api.js ADDED
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.postUser = exports.getAllUsers = exports.countUsers = void 0;
7
+ var _axios = _interopRequireDefault(require("axios"));
8
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
9
+ const API = process.env.REACT_APP_SERVER_URL || 'https://jsonplaceholder.typicode.com';
10
+
11
+ /**
12
+ * Récupère le nombre d'utilisateurs enregistrés via l'API.
13
+ *
14
+ * @returns {Promise<number>} Le nombre d'utilisateurs
15
+ * @throws {Error} Si la requête échoue
16
+ */
17
+ const countUsers = async () => {
18
+ try {
19
+ const response = await _axios.default.get("".concat(API, "/users"));
20
+ return response.data.length;
21
+ } catch (error) {
22
+ throw error;
23
+ }
24
+ };
25
+
26
+ /**
27
+ * Récupère la liste complète des utilisateurs depuis l'API.
28
+ *
29
+ * @returns {Promise<Array>} La liste des utilisateurs
30
+ * @throws {Error} Si la requête échoue
31
+ */
32
+ exports.countUsers = countUsers;
33
+ const getAllUsers = async () => {
34
+ try {
35
+ const response = await _axios.default.get("".concat(API, "/users"));
36
+ return response.data;
37
+ } catch (error) {
38
+ throw error;
39
+ }
40
+ };
41
+
42
+ /**
43
+ * Envoie une requête POST à l'API pour créer un nouvel utilisateur avec les données fournies.
44
+ *
45
+ * @param {Object} user - L'utilisateur à créer (doit contenir au moins une propriété "name")
46
+ * @returns {Promise<Object>} L'utilisateur créé tel que retourné par l'API
47
+ * @throws {Error} Si la requête échoue
48
+ */
49
+ exports.getAllUsers = getAllUsers;
50
+ const postUser = async user => {
51
+ try {
52
+ const response = await _axios.default.post("".concat(API, "/users"), user);
53
+ return response.data;
54
+ } catch (error) {
55
+ throw error;
56
+ }
57
+ };
58
+ exports.postUser = postUser;
package/dist/index.css ADDED
@@ -0,0 +1,13 @@
1
+ body {
2
+ margin: 0;
3
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5
+ sans-serif;
6
+ -webkit-font-smoothing: antialiased;
7
+ -moz-osx-font-smoothing: grayscale;
8
+ }
9
+
10
+ code {
11
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12
+ monospace;
13
+ }
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = _interopRequireDefault(require("react"));
8
+ var _client = _interopRequireDefault(require("react-dom/client"));
9
+ require("./index.css");
10
+ var _App = _interopRequireDefault(require("./App"));
11
+ var _reportWebVitals = _interopRequireDefault(require("./reportWebVitals"));
12
+ var _jsxRuntime = require("react/jsx-runtime");
13
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
14
+ const root = _client.default.createRoot(document.getElementById('root'));
15
+ root.render(/*#__PURE__*/(0, _jsxRuntime.jsx)(_react.default.StrictMode, {
16
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_App.default, {})
17
+ }));
18
+
19
+ // If you want to start measuring performance in your app, pass a function
20
+ // to log results (for example: reportWebVitals(console.log))
21
+ // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
22
+ (0, _reportWebVitals.default)();
23
+ var _default = exports.default = _App.default;
package/dist/logo.svg ADDED
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
package/dist/module.js ADDED
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.calculateAge = calculateAge;
7
+ /**
8
+ * Calule l'âge d'une personne à partir de sa date de naissance.
9
+ *
10
+ * @param {Object} p Un objet représentant une personne, implémentant un champ birth de type Date
11
+ * @returns {number} L'age de la personne en années
12
+ * @throws {Error} "missing param p" - si aucun argument n'est envoyé
13
+ * @throws {Error} "p is not an object" - si p n'est pas un objet
14
+ * @throws {Error} "missing birth field" - si p.birth est manquant
15
+ * @throws {Error} "birth must be a valid Date" - si p.birth n'est pas une date valide
16
+ * @throws {Error} "birth cannot be in the future" - si p.birth est une date future
17
+ * @throws {Error} "age cannot exceed 150 years" - si l'âge calculé est supérieur à 150 ans
18
+ */
19
+
20
+ function calculateAge(p) {
21
+ // Aucun argument n’a été envoyé
22
+ if (!p) throw new Error("missing param p");
23
+
24
+ // Le format envoyé n'est pas un objet
25
+ if (typeof p !== 'object') throw new Error("p is not an object");
26
+
27
+ // L'objet ne contient pas le champ birth
28
+ if (!p.birth) throw new Error("missing birth field");
29
+
30
+ // Le champ birth n'est pas une date et la date envoyée est fausse
31
+ if (!(p.birth instanceof Date) || isNaN(p.birth.getTime())) throw new Error("birth must be a valid Date");
32
+
33
+ // La date de naissance ne peut pas être dans le futur
34
+ if (p.birth > new Date()) throw new Error("birth cannot be in the future");
35
+
36
+ // La personne ne peut pas avoir plus de 150 ans
37
+ let dateDiff = new Date(Date.now() - p.birth.getTime());
38
+ let age = Math.abs(dateDiff.getUTCFullYear() - 1970);
39
+ if (age > 150) throw new Error("age cannot exceed 150 years");
40
+ return age;
41
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _reactRouterDom = require("react-router-dom");
8
+ var _jsxRuntime = require("react/jsx-runtime");
9
+ /**
10
+ * Page d'accueil
11
+ * Présente l'application et propose un lien vers le formulaire d'inscription.
12
+ * Affiche le nombre d'utilisateurs inscrits récupéré depuis l'API.
13
+ *
14
+ * @param {Object} props
15
+ * @param {number} props.usersCount - Le nombre d'utilisateurs inscrits
16
+ * @param {string} [props.apiError] - Message d'erreur à afficher si le chargement a échoué
17
+ * @returns {JSX.Element} La page d'accueil
18
+ */function Home(_ref) {
19
+ let {
20
+ usersCount,
21
+ apiError
22
+ } = _ref;
23
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
24
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("h1", {
25
+ children: "Bienvenue"
26
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)("p", {
27
+ children: "Cliquez sur le bouton ci-dessous pour acc\xE9der au formulaire d'inscription."
28
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactRouterDom.Link, {
29
+ to: "/register",
30
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)("button", {
31
+ children: "Acc\xE9der au formulaire"
32
+ })
33
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)("h1", {
34
+ children: "Utilisateurs inscrits"
35
+ }), apiError ? /*#__PURE__*/(0, _jsxRuntime.jsx)("p", {
36
+ "data-testid": "api-error",
37
+ style: {
38
+ color: 'red'
39
+ },
40
+ children: apiError
41
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)("p", {
42
+ "data-testid": "user-count",
43
+ children: [usersCount, " utilisateur", usersCount > 1 ? 's' : '', " inscrit", usersCount > 1 ? 's' : '']
44
+ })]
45
+ });
46
+ }
47
+ var _default = exports.default = Home;
@@ -0,0 +1,38 @@
1
+ .App {
2
+ text-align: center;
3
+ }
4
+
5
+ .App-logo {
6
+ height: 40vmin;
7
+ pointer-events: none;
8
+ }
9
+
10
+ @media (prefers-reduced-motion: no-preference) {
11
+ .App-logo {
12
+ animation: App-logo-spin infinite 20s linear;
13
+ }
14
+ }
15
+
16
+ .App-header {
17
+ background-color: #282c34;
18
+ min-height: 100vh;
19
+ display: flex;
20
+ flex-direction: column;
21
+ align-items: center;
22
+ justify-content: center;
23
+ font-size: calc(10px + 2vmin);
24
+ color: white;
25
+ }
26
+
27
+ .App-link {
28
+ color: #61dafb;
29
+ }
30
+
31
+ @keyframes App-logo-spin {
32
+ from {
33
+ transform: rotate(0deg);
34
+ }
35
+ to {
36
+ transform: rotate(360deg);
37
+ }
38
+ }