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 +128 -0
- package/babel.config.js +6 -0
- package/dist/App.js +63 -0
- package/dist/api.js +58 -0
- package/dist/index.css +13 -0
- package/dist/index.js +23 -0
- package/dist/logo.svg +1 -0
- package/dist/module.js +41 -0
- package/dist/pages/Home.js +47 -0
- package/dist/pages/Register.css +38 -0
- package/dist/pages/Register.js +288 -0
- package/dist/reportWebVitals.js +26 -0
- package/dist/setupTests.js +14 -0
- package/dist/tests/api.test.js +74 -0
- package/dist/tests/app.test.js +180 -0
- package/dist/tests/home.test.js +71 -0
- package/dist/tests/module.test.js +94 -0
- package/dist/tests/register.test.js +139 -0
- package/dist/tests/validator.test.js +415 -0
- package/dist/validator.js +170 -0
- package/package.json +56 -0
- package/src/App.js +52 -0
- package/src/api.js +49 -0
- package/src/index.css +13 -0
- package/src/index.js +19 -0
- package/src/logo.svg +1 -0
- package/src/module.js +43 -0
- package/src/pages/Home.js +30 -0
- package/src/pages/Register.css +38 -0
- package/src/pages/Register.js +202 -0
- package/src/reportWebVitals.js +13 -0
- package/src/validator.js +199 -0
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
|
+
```
|
package/babel.config.js
ADDED
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
|
+
}
|