Johan Korger
Lead Front-end
Lead Front-end
Lead Front-end
Chaque projet a contribué à l’amélioration du starter
(Jest, MSW, Cucumber, Eslint, Typescript, Toolkit, OIDC, …)
(Header, Footer, Router, Layout, etc …)
Pouvoir démarrer un projet rapidement dans les environnements AXA avec une stack à jour
Sources disponiblent sur Github
Personnalisable dans le tsconfig.js
Implémentation de Slash Design System
Eslint, Prettier, Husky, Sonar, Lint Staged
Eslint A11y, Axe Core, Jest Axe, A11yMenu
Authentification avec React OIDC
Gestion des formulaires avec React Hook Form
Système de routes avec React Router
Gestion des requêtes avec React Query
Vitest, Jest Cucumber, Testing Library, Jest Axe, Gherkins
Le projet est Open Source, vous pouvez donc forker ou cloner le projet
Si vous rencontrez des bugs, vous pouvez créer une issue sur le repository.
Si vous souhaitez contribuer au projet, vous pouvez en faire la demande pour être ajouter à l'équipe.
L'ensemble du projet a été développé avec Typescript
Si vous souhaitez adapter sa configuration, vous pouvez modifier le fichier tsconfig.
Permet de tester le DOM et les interactions utilisateur
Pour personnaliser, modifiez le fichier vite.config.ts
Pour les tests d'intégration, un dossier /features contient les scénarios Gherkin
Pour modifier les réponses d'api lors des tests : src/shared/testsUtils/msw.ts
React Query permet de faciliter la synchronisation entre le front et le back et offre des fonctionnalités qui facilitent la vie du développeur et améliorent l’expérience utilisateur.
React Hook Form permet de gérer l'état et la validation des formulaires de manière performante et simple.
React Router est une librairie de routing basée sur la déclaration de routes
(contrairement à NextJS qui se base sur le file system)
Nous allons maintenant passer à la pratique.
Veuillez utiliser la navigation vers le bas pour suivre les étapes.
Vous pouvez cloner le projet de la démo depuis l'url ci-dessous :
https://github.com/samuel-gomez/react-starter-vitejs.git
Il faut ensuite nettoyer les éléments de la démo
npm run clean
Vous pouvez maintenant effectuer l'installation
npm i
Vous pouvez lancer le projet en saisissant la commande suivante.
npm start
Le projet va démarrer à l'adresse : http://localhost:3000
Commençons par créer une nouvelle page
Dans le dossier /pages, créez un nouveau dossier /People
le but sera de récupérer les données d'une API et de les afficher dans un tableau
export { default } from './People';
src/pages/People/People.tsx
import { TITLE_BAR, TITLE } from './constants';
const People = () => (
<p>Ma page people</p>
);
export default People;
export const TITLE_BAR = 'Liste des gens';
export const TITLE = 'Tableau des gens';
export const ROUTE_URL_PEOPLE = 'people';
src/pages/People/People.tsx
on importe le Composant Layouton importe les constantesOn définit le type du Composant PeopleOn met à jour les props du Composant PeopleComposant Layout avec les propriétés propsTitleOn ajoute un titreimport Layout, { type TLayoutPage } from 'Layout'; import { TITLE_BAR, TITLE } from './constants'; export type TPeople = TLayoutPage; const People = ({ titleBar = TITLE_BAR, title = TITLE }: TPeople) => ( <Layout propsTitle={{ title: titleBar, backHome: true }}> <h2 className="af-title--content">{title}</h2> <p>Ma page people</p> </Layout> ); export default People;
src/App/Routes/Routes.tsxon importe la route depuis le Composant PeopleOn réexporte pour centraliser les routesimport { ROUTE_URL_UNAUTHORIZE as UNAUTHORIZE } from 'pages/Unauthorize/constants'; import { ROUTE_URL_PEOPLE as PEOPLE } from 'pages/People/constants'; const ROUTE_URLS = { HOME, PEOPLE, ... }; export default ROUTE_URLS;
on importe le Composant PeopleOn ajoute la route dans le Routerconst PageNotFound = lazy(() => import('pages/NotFound')); const PagePeople = lazy(() => import('pages/People')); ... <Routes> <Route element={<RouteSecureCmpt />}> <Route index path={ROUTE_URLS.HOME} element={<Home />} /> </Route> <Route index path={ROUTE_URLS.PEOPLE} element={<PagePeople />} /> ...
On ajoute le lien dans le menuimport ROUTE_URL from 'App/Routes/constants'; export const CLASS_BODY_MENU_OPEN = 'af-menu-open'; const MENU_ITEMS = [ { label: 'Accueil', url: ROUTE_URL.HOME, }, { label: 'People', url: `/${ROUTE_URL.PEOPLE}`, }, ]; export default MENU_ITEMS;
Feature: Page People
En tant que profil autorisé, je souhaite pouvoir afficher la page People
@RG1
Scenario Outline: Affichage de la page People
Given Je suis un utilisateur connu et connecté avec le profil "<profil>"
When J'accède à la page People
Then un titre "Tableau des gens" est visible
Examples:
| profil |
| Admin |
| User |
On importe Jest CucumberOn importe "configure" de Testing Library via le customRenderOn inclut les éléments cachés pour l'accessibilité (doc)On importe le fichier People.featureOn pose le test à videimport { defineFeature, loadFeature } from 'jest-cucumber'; import { configure } from 'shared/testsUtils/customRender'; configure({ defaultHidden: true }); const feature = loadFeature('features/People/People.feature'); defineFeature(feature, test => { test('Affichage de la page People', ({ given, and, when, then }) => {}); });
npm t People.spec.tsx
Récupérez le code généré dans le terminal defineFeature(feature, test => {
test('Affichage de la page People', ({ given, when, then }) => {
given(/^Je suis un utilisateur connu et connecté avec le profil (.*)$/, arg0 => {});
when("J'accède à la page People", () => {});
then(/^un titre "(.*)" est visible$/, arg0 => {});
});
});
On importe "render" et "screen" de Testing LibraryOn importe les scénarios typeOn importe notre composant de pageOn mocke le role depuis les valeurs du scénarioOn rend la page avec le rôle définiOn vérifie le nom de l'utilisateurOn vérifie la présence du titre de niveau 2... import { configure, render, screen } from 'shared/testsUtils/customRender'; import { JeSuisUnUtilisateurConnuEtConnecteAvecleProfil, UnTitreEstVisible } from 'shared/testsUtils/sharedScenarios'; import People from '..'; ... defineFeature(feature, test => { let role: string; test('Affichage de la page People', ({ given, when, then }) => { JeSuisUnUtilisateurConnuEtConnecteAvecleProfil(given, (roleMock: string) => { role = roleMock; }); when("J'accède à la page People", async () => { render(<People />, {}, { role }); expect(await screen.findByText('Samuel Gomez')).toBeInTheDocument(); }); UnTitreEstVisible(then,2); }); });
On définie les données reçues via l'api que l'on va fournir à MSWOn vérifie la présence du tableauOn vérifie les entêtes du tableauOn vérifie le contenu du tableauGiven Je suis un utilisateur connu et connecté avec le profil "<profil>" And la page reçoit les données suivantes | _id | firstname | lastname | birthDate | photo | entity | manager | managerId | | 1 | Samuel | Gomez | 1983-10-20T00:00:00 | https://randomuser.me/portraits/men/34.jpg | BIOSPAN | Sophie | 4 | | 2 | John | Doe | 1978-10-20T00:00:00 | https://randomuser.me/portraits/men/34.jpg | PEARLESSA | Sophie | 4 | | 3 | Guillaume | Chervet | 1985-10-20T00:00:00 | https://randomuser.me/portraits/men/34.jpg | CIRCUM | Sophie | 4 | | 4 | Sophie | Danneels | 1992-10-20T00:00:00 | https://randomuser.me/portraits/women/85.jpg | TRIPSCH | | | When J'accède à la page People Then un titre "Tableau des gens" est visible And la page contient un tableau répertoriant la liste des gens And le tableau présente des entêtes de colonnes dans l’ordre suivant : "Prénom", "Nom", "Date de naissance", "Entité" And le tableau contient 4 lignes avec 4 colonnes dans l'ordre suivant : | firstname | lastname | birthdate | entity | | Samuel | Gomez | 20/10/1983 | BIOSPAN | | John | Doe | 20/10/1978 | PEARLESSA | | Guillaume | Chervet | 20/10/1985 | CIRCUM | | Sophie | Danneels | 20/10/1992 | TRIPSCH |
npm t People.spec.tsx
src/pages/People/__tests__/People.spec.tsx
test('Affichage de la page People', ({ given, when, then, and }) => {
JeSuisUnUtilisateurConnuEtConnecteAvecleProfil(given, (roleMock: string) => {
role = roleMock;
});
and('la page reçoit les données suivantes', table => {});
when("J'accède à la page People", async () => {
render(<People />, {}, { role });
expect(await screen.findByText('Samuel Gomez')).toBeInTheDocument();
});
UnTitreEstVisible(then,2);
and('la page contient un tableau répertoriant la liste des gens', () => {});
and(/^le tableau présente des entêtes de colonnes dans l’ordre suivant : "(.*)", "(.*)", "(.*)", "(.*)"$/, (arg0, arg1, arg2, arg3) => {});
and(/^le tableau contient (\d+) lignes avec (\d+) colonnes dans l'ordre suivant :$/, (arg0, arg1, table) => {});
});
On importe les scénarios typeOn importe serverUseGet qui permet de mocker les appels réseaux en GET via MSWOn passe les données du gherkin à la méthode serverUseGet (le typage sera fait après)Ensuite, on remplace les scénarios générés par les scénarios type... import { JeSuisUnUtilisateurConnuEtConnecteAvecleProfil, LaPageContientUnTableau, LeTableauContientLesLignesCorrespondantAuxDonneesRecues, LeTableauPresenteDesEntetesDeColonnesDansLOrdreSuivant, UnTitreEstVisible, } from 'shared/testsUtils/sharedScenarios'; import { serverUseGet } from 'shared/testsUtils/msw'; import People from '..'; ... const tableItemsType = 'membres'; and('la page reçoit les données suivantes', responseBody => { serverUseGet<TPeopleData[]>({ route: 'people', responseBody }); // le typage sera fait juste après }); ... LaPageContientUnTableau(and, 'la page contient un tableau répertoriant la liste des gens', tableItemsType); LeTableauPresenteDesEntetesDeColonnesDansLOrdreSuivant( and, /^le tableau présente des entêtes de colonnes dans l’ordre suivant : "(.*)", "(.*)", "(.*)", "(.*)"$/, tableItemsType, ); LeTableauContientLesLignesCorrespondantAuxDonneesRecues( and, /^le tableau contient (\d+) lignes avec (\d+) colonnes dans l'ordre suivant :$/, tableItemsType, );
apiUrl est sous forme d'objet pour permettre d'avoir plusieurs sources d'API... { "apiUrl": { "base": "https://react-starter-api.vercel.app/api/" }, "baseUrl": "",
on créé une constante pour le nom du serviceon créé une constante pour le endpointon créé une constante qui contiendra les infos du header pour le tableauexport const TITLE_BAR = 'Liste des gens'; export const TITLE = 'Tableau des gens'; export const ROUTE_URL_PEOPLE = 'people'; export const SERVICE_NAME = 'people'; export const ENDPOINT = 'people'; export const TABLE_HEADERS_PEOPLE = [ { label: 'Prénom', id: 'firstname', key: 'firstname' }, { label: 'Nom', id: 'lastname', key: 'lastname' }, { label: 'Date de naissance', id: 'birthDate', key: 'birthDate' }, { label: 'Entité', id: 'entity', key: 'entity' }, ];
export type TPeopleData = Record<string, string>;
export type TPeopleDataResponse = {
responseBody: TPeopleData[];
};
On importe useQuery de React Query, setAnomalyEmptyItems, Tanomaly et nos constantesimport { useQuery } from '@tanstack/react-query'; import Layout, { type TLayoutPage } from 'Layout'; import { setAnomalyEmptyItems } from 'shared/helpers'; import { type Tanomaly } from 'shared/types'; import { ENDPOINT, SERVICE_NAME, TITLE, TITLE_BAR } from './constants'; export const usePeople = () => { const { data, isLoading, error, refetch } = useQuery({ queryKey: [ENDPOINT], select: ({ responseBody }: TPeopleDataResponse) => ({ anomaly: setAnomalyEmptyItems(responseBody), [SERVICE_NAME]: computeInfos(responseBody), }), }); return { ...data, anomaly: (error || data?.anomaly) as Tanomaly | null, isLoading, refetch, }; }; export type TReturnUsePeople = ReturnType<typeof usePeople>;
On créé un hook custom pour effectuer l'appel avec React Queryimport { useQuery } from '@tanstack/react-query'; import Layout, { type TLayoutPage } from 'Layout'; import { setAnomalyEmptyItems } from 'shared/helpers'; import { type Tanomaly } from 'shared/types'; import { ENDPOINT, SERVICE_NAME, TITLE, TITLE_BAR } from './constants'; export const usePeople = () => { const { data, isLoading, error, refetch } = useQuery({ queryKey: [ENDPOINT], select: ({ responseBody }: TPeopleDataResponse) => ({ anomaly: setAnomalyEmptyItems(responseBody), [SERVICE_NAME]: computeInfos(responseBody), }), }); return { ...data, anomaly: (error || data?.anomaly) as Tanomaly | null, isLoading, refetch, }; }; export type TReturnUsePeople = ReturnType<typeof usePeople>;
On passe la route au useQuery, il nous retourne les données, l'état du fetch, l'état de l'erreur, et une méthode refetchimport { useQuery } from '@tanstack/react-query'; import Layout, { type TLayoutPage } from 'Layout'; import { setAnomalyEmptyItems } from 'shared/helpers'; import { type Tanomaly } from 'shared/types'; import { ENDPOINT, SERVICE_NAME, TITLE, TITLE_BAR } from './constants'; export const usePeople = () => { const { data, isLoading, error, refetch } = useQuery({ queryKey: [ENDPOINT], select: ({ responseBody }: TPeopleDataResponse) => ({ anomaly: setAnomalyEmptyItems(responseBody), [SERVICE_NAME]: computeInfos(responseBody), }), }); return { ...data, anomaly: (error || data?.anomaly) as Tanomaly | null, isLoading, refetch, }; }; export type TReturnUsePeople = ReturnType<typeof usePeople>;
En second paramètre, sur la propriété select, on peut passer une fonction qui s'exécutera après avoir reçu les données pour effectuer des traitementsimport { useQuery } from '@tanstack/react-query'; import Layout, { type TLayoutPage } from 'Layout'; import { setAnomalyEmptyItems } from 'shared/helpers'; import { type Tanomaly } from 'shared/types'; import { ENDPOINT, SERVICE_NAME, TITLE, TITLE_BAR } from './constants'; export const usePeople = () => { const { data, isLoading, error, refetch } = useQuery({ queryKey: [ENDPOINT], select: ({ responseBody }: TPeopleDataResponse) => ({ anomaly: setAnomalyEmptyItems(responseBody), [SERVICE_NAME]: computeInfos(responseBody), }), }); return { ...data, anomaly: (error || data?.anomaly) as Tanomaly | null, isLoading, refetch, }; }; export type TReturnUsePeople = ReturnType<typeof usePeople>;
ici, on renvoie un objet contenant l'anomalie en cas de réponse à vide et les données formatées en cas de données non vides
setAnomalyEmptyItems est une fonction utilitaire qui va renvoyer un objet dans l'anomalie qui affichera une alertimport { useQuery } from '@tanstack/react-query'; import Layout, { type TLayoutPage } from 'Layout'; import { setAnomalyEmptyItems } from 'shared/helpers'; import { type Tanomaly } from 'shared/types'; import { ENDPOINT, SERVICE_NAME, TITLE, TITLE_BAR } from './constants'; export const usePeople = () => { const { data, isLoading, error, refetch } = useQuery({ queryKey: [ENDPOINT], select: ({ responseBody }: TPeopleDataResponse) => ({ anomaly: setAnomalyEmptyItems(responseBody), [SERVICE_NAME]: computeInfos(responseBody), }), }); return { ...data, anomaly: (error || data?.anomaly) as Tanomaly | null, isLoading, refetch, }; }; export type TReturnUsePeople = ReturnType<typeof usePeople>;
computeInfos est une fonction que l'on va créer pour respecter le format de données attendu par le tableau
Enfin, on retourne les valeurs utiles à la vueimport { useQuery } from '@tanstack/react-query'; import Layout, { type TLayoutPage } from 'Layout'; import { setAnomalyEmptyItems } from 'shared/helpers'; import { type Tanomaly } from 'shared/types'; import { ENDPOINT, SERVICE_NAME, TITLE, TITLE_BAR } from './constants'; export const usePeople = () => { const { data, isLoading, error, refetch } = useQuery({ queryKey: [ENDPOINT], select: ({ responseBody }: TPeopleDataResponse) => ({ anomaly: setAnomalyEmptyItems(responseBody), [SERVICE_NAME]: computeInfos(responseBody), }), }); return { ...data, anomaly: (error || data?.anomaly) as Tanomaly | null, isLoading, refetch, }; }; export type TReturnUsePeople = ReturnType<typeof usePeople>;
On modifie les imports... import { setAnomalyEmptyItems, setDate } from 'shared/helpers'; import { setDisplay } from 'shared/components/Table'; ... export const computeInfos = (data: TPeopleData[]) => data?.map(({ _id, firstname, lastname, birthDate, entity }) => ({ key: _id, cols: { ...setDisplay({ firstname }), ...setDisplay({ lastname }), ...setDisplay({ birthDate: setDate({ date: birthDate }) }), ...setDisplay({ entity }), }, }));
La fonction reçoit les données brutes... import { setAnomalyEmptyItems, setDate } from 'shared/helpers'; import { setDisplay } from 'shared/components/Table'; ... export const computeInfos = (data: TPeopleData[]) => data?.map(({ _id, firstname, lastname, birthDate, entity }) => ({ key: _id, cols: { ...setDisplay({ firstname }), ...setDisplay({ lastname }), ...setDisplay({ birthDate: setDate({ date: birthDate }) }), ...setDisplay({ entity }), }, }));
On boucle sur les données... import { setAnomalyEmptyItems, setDate } from 'shared/helpers'; import { setDisplay } from 'shared/components/Table'; ... export const computeInfos = (data: TPeopleData[]) => data?.map(({ _id, firstname, lastname, birthDate, entity }) => ({ key: _id, cols: { ...setDisplay({ firstname }), ...setDisplay({ lastname }), ...setDisplay({ birthDate: setDate({ date: birthDate }) }), ...setDisplay({ entity }), }, }));
Pour chaque item, on renvoie les données formatées... import { setAnomalyEmptyItems, setDate } from 'shared/helpers'; import { setDisplay } from 'shared/components/Table'; ... export const computeInfos = (data: TPeopleData[]) => data?.map(({ _id, firstname, lastname, birthDate, entity }) => ({ key: _id, cols: { ...setDisplay({ firstname }), ...setDisplay({ lastname }), ...setDisplay({ birthDate: setDate({ date: birthDate }) }), ...setDisplay({ entity }), }, }));
La méthode setDisplay formate les données pour chaque cellule du tableau... import { setAnomalyEmptyItems, setDate } from 'shared/helpers'; import { setDisplay } from 'shared/components/Table'; ... export const computeInfos = (data: TPeopleData[]) => data?.map(({ _id, firstname, lastname, birthDate, entity }) => ({ key: _id, cols: { ...setDisplay({ firstname }), ...setDisplay({ lastname }), ...setDisplay({ birthDate: setDate({ date: birthDate }) }), ...setDisplay({ entity }), }, }));
cols: {
...
entity: {
classModifier: 'actions',
children: <Badge classModifier="info">{entity}</Badge>
},
On importe la fonction setLoaderMode... import { setLoaderMode } from 'shared/components/Loader'; ... const PeopleContainer = () => { const { anomaly, isLoading, people, refetch } = usePeople(); return <People people={people} loaderMode={setLoaderMode({ isLoading })} refetch={refetch} anomaly={anomaly} />; }; export default PeopleContainer;
On déclare un nouveau composant... import { setLoaderMode } from 'shared/components/Loader'; ... const PeopleContainer = () => { const { anomaly, isLoading, people, refetch } = usePeople(); return <People people={people} loaderMode={setLoaderMode({ isLoading })} refetch={refetch} anomaly={anomaly} />; }; export default PeopleContainer;
On récupère les données via notre hook custom... import { setLoaderMode } from 'shared/components/Loader'; ... const PeopleContainer = () => { const { anomaly, isLoading, people, refetch } = usePeople(); return <People people={people} loaderMode={setLoaderMode({ isLoading })} refetch={refetch} anomaly={anomaly} />; }; export default PeopleContainer;
On les passe à la vue... import { setLoaderMode } from 'shared/components/Loader'; ... const PeopleContainer = () => { const { anomaly, isLoading, people, refetch } = usePeople(); return <People people={people} loaderMode={setLoaderMode({ isLoading })} refetch={refetch} anomaly={anomaly} />; }; export default PeopleContainer;
On modifie l'export par défaut... import { setLoaderMode } from 'shared/components/Loader'; ... const PeopleContainer = () => { const { anomaly, isLoading, people, refetch } = usePeople(); return <People people={people} loaderMode={setLoaderMode({ isLoading })} refetch={refetch} anomaly={anomaly} />; }; export default PeopleContainer;
On ajoute les imports nécessaires... import Loader, { TLoader, setLoaderMode } from 'shared/components/Loader'; import Resilience from 'shared/components/Resilience'; import Table, { setDisplay } from 'shared/components/Table'; import { ENDPOINT, SERVICE_NAME, TABLE_HEADERS_PEOPLE, TITLE, TITLE_BAR } from './constants'; ... export type TPeople = TLayoutPage & Pick<TReturnUsePeople, 'people' | 'anomaly' | 'refetch'> & { loaderMode: TLoader['mode']; headers?: typeof TABLE_HEADERS_PEOPLE; }; const People = ({ titleBar = TITLE_BAR, title = TITLE, people, headers = TABLE_HEADERS_PEOPLE, refetch, loaderMode, anomaly }: TPeople) => ( <Layout propsTitle={{ title: titleBar, backHome: true }}> <h2 className="af-title--content">{title}</h2> <Loader mode={loaderMode}> <Resilience anomaly={anomaly} refetch={refetch as React.MouseEventHandler<HTMLButtonElement>}> <Table title="Titre de mon tableau" items={people} headers={headers} itemsType="membres" /> </Resilience> </Loader> </Layout> );
On modifie le type de People... import Loader, { TLoader, setLoaderMode } from 'shared/components/Loader'; import Resilience from 'shared/components/Resilience'; import Table, { setDisplay } from 'shared/components/Table'; import { ENDPOINT, SERVICE_NAME, TABLE_HEADERS_PEOPLE, TITLE, TITLE_BAR } from './constants'; ... export type TPeople = TLayoutPage & Pick<TReturnUsePeople, 'people' | 'anomaly' | 'refetch'> & { loaderMode: TLoader['mode']; headers?: typeof TABLE_HEADERS_PEOPLE; }; const People = ({ titleBar = TITLE_BAR, title = TITLE, people, headers = TABLE_HEADERS_PEOPLE, refetch, loaderMode, anomaly }: TPeople) => ( <Layout propsTitle={{ title: titleBar, backHome: true }}> <h2 className="af-title--content">{title}</h2> <Loader mode={loaderMode}> <Resilience anomaly={anomaly} refetch={refetch as React.MouseEventHandler<HTMLButtonElement>}> <Table title="Titre de mon tableau" items={people} headers={headers} itemsType="membres" /> </Resilience> </Loader> </Layout> );
On modifie les props d'entrée... import Loader, { TLoader, setLoaderMode } from 'shared/components/Loader'; import Resilience from 'shared/components/Resilience'; import Table, { setDisplay } from 'shared/components/Table'; import { ENDPOINT, SERVICE_NAME, TABLE_HEADERS_PEOPLE, TITLE, TITLE_BAR } from './constants'; ... export type TPeople = TLayoutPage & Pick<TReturnUsePeople, 'people' | 'anomaly' | 'refetch'> & { loaderMode: TLoader['mode']; headers?: typeof TABLE_HEADERS_PEOPLE; }; const People = ({ titleBar = TITLE_BAR, title = TITLE, people, headers = TABLE_HEADERS_PEOPLE, refetch, loaderMode, anomaly }: TPeople) => ( <Layout propsTitle={{ title: titleBar, backHome: true }}> <h2 className="af-title--content">{title}</h2> <Loader mode={loaderMode}> <Resilience anomaly={anomaly} refetch={refetch as React.MouseEventHandler<HTMLButtonElement>}> <Table title="Titre de mon tableau" items={people} headers={headers} itemsType="membres" /> </Resilience> </Loader> </Layout> );
On ajoute le Loader qui affichera un
loader en fonction de l'état de chargement... import Loader, { TLoader, setLoaderMode } from 'shared/components/Loader'; import Resilience from 'shared/components/Resilience'; import Table, { setDisplay } from 'shared/components/Table'; import { ENDPOINT, SERVICE_NAME, TABLE_HEADERS_PEOPLE, TITLE, TITLE_BAR } from './constants'; ... export type TPeople = TLayoutPage & Pick<TReturnUsePeople, 'people' | 'anomaly' | 'refetch'> & { loaderMode: TLoader['mode']; headers?: typeof TABLE_HEADERS_PEOPLE; }; const People = ({ titleBar = TITLE_BAR, title = TITLE, people, headers = TABLE_HEADERS_PEOPLE, refetch, loaderMode, anomaly }: TPeople) => ( <Layout propsTitle={{ title: titleBar, backHome: true }}> <h2 className="af-title--content">{title}</h2> <Loader mode={loaderMode}> <Resilience anomaly={anomaly} refetch={refetch as React.MouseEventHandler<HTMLButtonElement>}> <Table title="Titre de mon tableau" items={people} headers={headers} itemsType="membres" /> </Resilience> </Loader> </Layout> );
On ajoute le composant Resilience
qui affichera un fallback en cas d'anomalie... import Loader, { TLoader, setLoaderMode } from 'shared/components/Loader'; import Resilience from 'shared/components/Resilience'; import Table, { setDisplay } from 'shared/components/Table'; import { ENDPOINT, SERVICE_NAME, TABLE_HEADERS_PEOPLE, TITLE, TITLE_BAR } from './constants'; ... export type TPeople = TLayoutPage & Pick<TReturnUsePeople, 'people' | 'anomaly' | 'refetch'> & { loaderMode: TLoader['mode']; headers?: typeof TABLE_HEADERS_PEOPLE; }; const People = ({ titleBar = TITLE_BAR, title = TITLE, people, headers = TABLE_HEADERS_PEOPLE, refetch, loaderMode, anomaly }: TPeople) => ( <Layout propsTitle={{ title: titleBar, backHome: true }}> <h2 className="af-title--content">{title}</h2> <Loader mode={loaderMode}> <Resilience anomaly={anomaly} refetch={refetch as React.MouseEventHandler<HTMLButtonElement>}> <Table title="Titre de mon tableau" items={people} headers={headers} itemsType="membres" /> </Resilience> </Loader> </Layout> );
On ajoute le composant Table
qui affichera le tableau si tout est ok... import Loader, { TLoader, setLoaderMode } from 'shared/components/Loader'; import Resilience from 'shared/components/Resilience'; import Table, { setDisplay } from 'shared/components/Table'; import { ENDPOINT, SERVICE_NAME, TABLE_HEADERS_PEOPLE, TITLE, TITLE_BAR } from './constants'; ... export type TPeople = TLayoutPage & Pick<TReturnUsePeople, 'people' | 'anomaly' | 'refetch'> & { loaderMode: TLoader['mode']; headers?: typeof TABLE_HEADERS_PEOPLE; }; const People = ({ titleBar = TITLE_BAR, title = TITLE, people, headers = TABLE_HEADERS_PEOPLE, refetch, loaderMode, anomaly }: TPeople) => ( <Layout propsTitle={{ title: titleBar, backHome: true }}> <h2 className="af-title--content">{title}</h2> <Loader mode={loaderMode}> <Resilience anomaly={anomaly} refetch={refetch as React.MouseEventHandler<HTMLButtonElement>}> <Table title="Titre de mon tableau" items={people} headers={headers} itemsType="membres" /> </Resilience> </Loader> </Layout> );
npm run cover
On importe waitFor et customRenderHook.import { customRenderHook, waitFor } from 'shared/testsUtils'; import { computeInfos, usePeople } from '../People';
customRenderHook permet de modifier les données renvoyée par React Query
const peopleMock = [ { _id: '99999', firstname: 'Samuel', lastname: 'Gomez', birthDate: '1985-10-20T13:44:20.540000', entity: 'AXA', }, ];
const expectedData = [ { cols: { birthDate: { label: '20/10/1985', }, firstname: { label: 'Samuel', }, lastname: { label: 'Gomez', }, entity: { label: 'AXA', }, }, key: '99999', }, ];
const expectedEmptyAnomaly = { label: 'Info : Aucune donnée trouvée', type: 'info', iconName: 'exclamation-sign', };
Cas avec des donnéesdescribe('computeInfos', () => { it('Should computed people when computeInfos have been called with people', () => { const computedPeople = computeInfos(peopleMock); expect(computedPeople).toMatchObject(expectedData); }); it('Should empty array when computeInfos have been called with empty people', () => { const computedPeople = computeInfos([]); expect(computedPeople).toEqual([]); }); });
Cas données videsdescribe('computeInfos', () => { it('Should computed people when computeInfos have been called with people', () => { const computedPeople = computeInfos(peopleMock); expect(computedPeople).toMatchObject(expectedData); }); it('Should empty array when computeInfos have been called with empty people', () => { const computedPeople = computeInfos([]); expect(computedPeople).toEqual([]); }); });
Cas données videsdescribe('usePeople', () => { it.each` queryData | people | isLoading | anomaly ${{ responseBody: [] }} | ${[]} | ${false} | ${expectedEmptyAnomaly} ${{ responseBody: peopleMock }} | ${expectedData} | ${false} | ${null} `( 'Should return isLoading: $isLoading, anomaly: $anomaly, people: $people when usePeople is rendered with queryData: $queryData', async ({ queryData, anomaly, isLoading, people }) => { const { result } = customRenderHook({ queryData })(() => usePeople(), {}); await waitFor(() => expect(result.current).toMatchObject({ anomaly, isLoading, people, refetch: result.current.refetch, }), ); }, ); });
Cas avec des donnéesdescribe('usePeople', () => { it.each` queryData | people | isLoading | anomaly ${{ responseBody: [] }} | ${[]} | ${false} | ${expectedEmptyAnomaly} ${{ responseBody: peopleMock }} | ${expectedData} | ${false} | ${null} `( 'Should return isLoading: $isLoading, anomaly: $anomaly, people: $people when usePeople is rendered with queryData: $queryData', async ({ queryData, anomaly, isLoading, people }) => { const { result } = customRenderHook({ queryData })(() => usePeople(), {}); await waitFor(() => expect(result.current).toMatchObject({ anomaly, isLoading, people, refetch: result.current.refetch, }), ); }, ); });
On rend notre hook en passant le mock que doit renvoyer React Querydescribe('usePeople', () => { it.each` queryData | people | isLoading | anomaly ${{ responseBody: [] }} | ${[]} | ${false} | ${expectedEmptyAnomaly} ${{ responseBody: peopleMock }} | ${expectedData} | ${false} | ${null} `( 'Should return isLoading: $isLoading, anomaly: $anomaly, people: $people when usePeople is rendered with queryData: $queryData', async ({ queryData, anomaly, isLoading, people }) => { const { result } = customRenderHook({ queryData })(() => usePeople(), {}); await waitFor(() => expect(result.current).toMatchObject({ anomaly, isLoading, people, refetch: result.current.refetch, }), ); }, ); });
describe('usePeople', () => {
it.each`
queryData | people | isLoading | anomaly
${{ responseBody: [] }} | ${[]} | ${false} | ${expectedEmptyAnomaly}
${{ responseBody: peopleMock }} | ${expectedData} | ${false} | ${null}
`(
'Should return isLoading: $isLoading, anomaly: $anomaly, people: $people when usePeople is rendered with queryData: $queryData',
async ({ queryData, anomaly, isLoading, people }) => {
const { result } = customRenderHook({ queryData })(() => usePeople(), {});
await waitFor(() =>
expect(result.current).toMatchObject({
anomaly,
isLoading,
people,
refetch: result.current.refetch,
}),
);
},
);
});
Afficher le détail du contrat au clic du bouton
<Button id="uniqueid" className="af-btn--circle" >
<i role="img" aria-label="eye-open" className="glyphicon glyphicon-eye-open" />
</Button>