Skip to content

Commit

Permalink
Appel du back-end depuis le formulaire (#207)
Browse files Browse the repository at this point in the history
* Appel du back-end depuis le formulaire

* Ajout de la page de formulaire validé

* Correction des problèmes de validation

* Ajout de tests pour la candidature validée

* Retours de code review

* Retours supplémentaires de code review

* WIP : test d'erreur à faire fonctionner

* Fix test stub

* Ajout d'un test de changement de page
  • Loading branch information
Alezco authored Sep 9, 2024
1 parent ffa80b2 commit 8b6a101
Show file tree
Hide file tree
Showing 22 changed files with 370 additions and 62 deletions.
2 changes: 2 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function App() {
const PageCandidatureConseiller = lazy(() => import('./views/candidature-conseiller/PageCandidatureConseiller'));
const PageCandidatureStructure = lazy(() => import('./views/candidature-structure/PageCandidatureStructure'));
const PageCandidatureCoordinateur = lazy(() => import('./views/candidature-coordinateur/PageCandidatureCoordinateur'));
const PageCandidatureValidee = lazy(() => import('./views/candidature-validee/PageCandidatureValidee'));

return (
<div className="App">
Expand All @@ -49,6 +50,7 @@ function App() {
<Route path="/nouveau-formulaire-conseiller" element={<PageCandidatureConseiller />}/>
<Route path="/nouveau-formulaire-structure" element={<PageCandidatureStructure />}/>
<Route path="/nouveau-formulaire-coordinateur" element={<PageCandidatureCoordinateur />}/>
<Route path="/candidature-validee" element={<PageCandidatureValidee />}/>
<Route path="/kit-communication" element={<KitCommunication />}/>
<Route path="/donnees-personnelles" element={<DonneesPersonnelles />}/>
<Route path="/mentions-legales" element={<MentionsLegales />}/>
Expand Down
17 changes: 17 additions & 0 deletions src/components/commun/Alert.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';

export default function Alert({ children, titre }) {
return (
<div className="fr-alert fr-alert--error">
<h3 className="fr-alert__title">{titre}</h3>
<p>{children}</p>
</div>
);
}

Alert.propTypes = {
children: PropTypes.node,
titre: PropTypes.string,
};

2 changes: 1 addition & 1 deletion src/components/commun/BoutonRadio.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function BoutonRadio({ children, id, nomGroupe }) {
return (
<div className="fr-fieldset__element">
<div className="fr-radio-group">
<input type="radio" id={id} name={nomGroupe} required />
<input type="radio" id={id} name={nomGroupe} value={id} required />
<label className="fr-label" htmlFor={id}>
{children}
</label>
Expand Down
2 changes: 1 addition & 1 deletion src/components/commun/Datepicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function Datepicker({ children, id, isRequired = true, onChange,
<label className="fr-label" htmlFor={id}>
{children}
</label>
<input className="fr-input cc-datepicker" id={id} type="date" required={isRequired} onChange={onChange} min={min} />
<input className="fr-input cc-datepicker" id={id} type="date" required={isRequired} onChange={onChange} min={min} name={id} />
</div>
);
}
Expand Down
12 changes: 11 additions & 1 deletion src/components/commun/Input.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@ export default function Input({ children, id, isRequired = true, type = 'text',
<div className="fr-fieldset__element">
<div className="fr-input-group">
<label className="fr-label" htmlFor={id}>{children}</label>
<input className="fr-input" type={type} id={id} required={isRequired} pattern={pattern} onChange={onChange} list={list} min={min} />
<input
className="fr-input"
type={type}
id={id}
required={isRequired}
pattern={pattern}
onChange={onChange}
list={list}
min={min}
name={id}
/>
</div>
</div>
);
Expand Down
8 changes: 5 additions & 3 deletions src/views/candidature-conseiller/AddressChooser.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,23 @@ import { useGeoApi } from './useGeoApi';
import { debounce } from './debounce';

export default function AddressChooser() {
const { search, villes } = useGeoApi();
const { searchByName, villes } = useGeoApi();

return (
<>
<Input
id="lieuHabitation"
list="resultatsRecherche"
isRequired={false}
onChange={debounce(event => search(event.target.value))}
onChange={debounce(event => searchByName(event.target.value))}
>
Votre lieu d’habitation <span className="fr-hint-text">Saississez le nom ou le code postal de votre commune.</span>
</Input>
<datalist id="resultatsRecherche">
{villes.map(({ code, nom }) => (
<option value={`${code} ${nom}`} key={code}>{code} {nom}</option>
<option value={`${code} ${nom}`} key={code}>
{code} {nom}
</option>
))}
</datalist>
</>
Expand Down
50 changes: 43 additions & 7 deletions src/views/candidature-conseiller/CandidatureConseiller.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import SommaireConseiller from './SommaireConseiller';
import InformationsDeContact from './InformationsDeContact';
import SituationEtExperience from './SituationEtExperience';
import Disponibilite from './Disponibilite';
import Motivation from './Motivation';
import EnResume from './EnResume';
import Alert from '../../components/commun/Alert';
import { useScrollToSection } from '../../hooks/useScrollToSection';
import { useNavigate } from 'react-router-dom';
import { useApiAdmin } from './useApiAdmin';

import '@gouvfr/dsfr/dist/component/form/form.min.css';
import '@gouvfr/dsfr/dist/component/input/input.min.css';
Expand All @@ -14,25 +17,48 @@ import '@gouvfr/dsfr/dist/component/radio/radio.min.css';
import '@gouvfr/dsfr/dist/component/badge/badge.min.css';
import '@gouvfr/dsfr/dist/component/notice/notice.min.css';
import '@gouvfr/dsfr/dist/component/sidemenu/sidemenu.min.css';
import '@gouvfr/dsfr/dist/component/alert/alert.min.css';
import './CandidatureConseiller.css';

export default function CandidatureConseiller() {
const [dateDisponibilite, setDateDisponibilite] = useState('');
const [isSituationValid, setIsSituationValid] = useState(true);

const [validationError, setValidationError] = useState('');
const { buildConseillerData, creerCandidatureConseiller } = useApiAdmin();
const navigate = useNavigate();
useScrollToSection();

const validerLaCandidature = event => {
useEffect(() => {
document.title = 'Conseiller numérique - Devenir conseiller numérique';
}, []);

const estSituationRemplie = formData => {
const demandeurEmploi = formData.get('estDemandeurEmploi') === 'on';
const enEmploi = formData.get('estEnEmploi') === 'on';
const enFormation = formData.get('estEnFormation') === 'on';
const diplome = formData.get('estDiplomeMedNum') === 'on';

return demandeurEmploi || enEmploi || enFormation || diplome;
};

const validerLaCandidature = async event => {
event.preventDefault();

const formData = new FormData(event.currentTarget);
const situations = formData.get('situations');

if (situations === null) {
if (!estSituationRemplie(formData)) {
setIsSituationValid(false);
document.getElementById('situation-et-experience').scrollIntoView();
} else {
event.currentTarget.submit();
const conseillerData = await buildConseillerData(formData);
const resultatCreation = await creerCandidatureConseiller(conseillerData);
if (resultatCreation.status >= 400) {
const error = await resultatCreation.json();
setValidationError(error.message);
window.scrollTo({ top: 0, behavior: 'smooth' });
} else {
navigate('/candidature-validee');
}
}
};

Expand All @@ -45,7 +71,17 @@ export default function CandidatureConseiller() {
<div className="fr-col-12 fr-col-md-8 fr-py-12v">
<h1 className="cc-titre fr-mb-5w">Je veux devenir conseiller numérique</h1>
<p className="fr-text--sm fr-hint-text">Les champs avec <span className="cc-obligatoire">*</span> sont obligatoires.</p>
<form aria-label="Candidature conseiller" onSubmit={validerLaCandidature}>
{validationError &&
<div className="fr-pb-2w">
<Alert titre="Erreur de validation">
{validationError}
</Alert>
</div>
}
<form
aria-label="Candidature conseiller"
onSubmit={validerLaCandidature}
>
<InformationsDeContact />
<SituationEtExperience isSituationValid={isSituationValid} />
<Disponibilite setDateDisponibilite={setDateDisponibilite} />
Expand Down
128 changes: 115 additions & 13 deletions src/views/candidature-conseiller/CandidatureConseiller.test.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { render, screen, within, fireEvent } from '@testing-library/react';
import { render, screen, within, fireEvent, act } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import CandidatureConseiller from './CandidatureConseiller';
import { textMatcher, dateDujour } from '../../../test/test-utils';
import * as ReactRouterDom from 'react-router-dom';

vi.mock('react-router-dom', () => ({
useLocation: () => ({ hash: '' }),
useNavigate: vi.fn()
}));

describe('candidature conseiller', () => {
Expand Down Expand Up @@ -57,9 +59,9 @@ describe('candidature conseiller', () => {
expect(email).toHaveAttribute('type', 'email');
expect(email).toBeRequired();

const telephone = within(etapeInformationsDeContact).getByLabelText('Téléphone Format attendu : 0122334455');
const telephone = within(etapeInformationsDeContact).getByLabelText('Téléphone Format attendu : +33122334455');
expect(telephone).toHaveAttribute('type', 'tel');
expect(telephone).toHaveAttribute('pattern', '0[1-9]{9}');
expect(telephone).toHaveAttribute('pattern', '[+](33|590|596|594|262|269|687)[1-9]{9}');

const habitation = within(etapeInformationsDeContact).getByLabelText('Votre lieu d’habitation Saississez le nom ou le code postal de votre commune.');
expect(habitation).toHaveAttribute('type', 'text');
Expand Down Expand Up @@ -104,11 +106,11 @@ describe('candidature conseiller', () => {

const oui = screen.getByRole('radio', { name: 'Oui' });
expect(oui).toBeRequired();
expect(oui).toHaveAttribute('name', 'experienceProfessionnelle');
expect(oui).toHaveAttribute('name', 'aUneExperienceMedNum');

const non = screen.getByRole('radio', { name: 'Non' });
expect(non).toBeRequired();
expect(non).toHaveAttribute('name', 'experienceProfessionnelle');
expect(non).toHaveAttribute('name', 'aUneExperienceMedNum');
});

it('quand je coche "diplomé" alors un champ pour préciser le diplôme s’affiche', () => {
Expand Down Expand Up @@ -166,31 +168,31 @@ describe('candidature conseiller', () => {

const _5km = screen.getByRole('radio', { name: '5 km' });
expect(_5km).toBeRequired();
expect(_5km).toHaveAttribute('name', 'distanceDomicile');
expect(_5km).toHaveAttribute('name', 'distanceMax');

const _10km = screen.getByRole('radio', { name: '10 km' });
expect(_10km).toBeRequired();
expect(_10km).toHaveAttribute('name', 'distanceDomicile');
expect(_10km).toHaveAttribute('name', 'distanceMax');

const _15km = screen.getByRole('radio', { name: '15 km' });
expect(_15km).toBeRequired();
expect(_15km).toHaveAttribute('name', 'distanceDomicile');
expect(_15km).toHaveAttribute('name', 'distanceMax');

const _20km = screen.getByRole('radio', { name: '20 km' });
expect(_20km).toBeRequired();
expect(_20km).toHaveAttribute('name', 'distanceDomicile');
expect(_20km).toHaveAttribute('name', 'distanceMax');

const _40km = screen.getByRole('radio', { name: '40 km' });
expect(_40km).toBeRequired();
expect(_40km).toHaveAttribute('name', 'distanceDomicile');
expect(_40km).toHaveAttribute('name', 'distanceMax');

const _100km = screen.getByRole('radio', { name: '100 km' });
expect(_100km).toBeRequired();
expect(_100km).toHaveAttribute('name', 'distanceDomicile');
expect(_100km).toHaveAttribute('name', 'distanceMax');

const franceEntiere = screen.getByRole('radio', { name: 'France entière' });
expect(franceEntiere).toBeRequired();
expect(franceEntiere).toHaveAttribute('name', 'distanceDomicile');
expect(franceEntiere).toHaveAttribute('name', 'distanceMax');
});

it('quand j’affiche le formulaire alors l’étape "Votre motivation" est affiché', () => {
Expand All @@ -210,7 +212,7 @@ describe('candidature conseiller', () => {
expect(aideMotivation).toBeInTheDocument();

const descriptionMotivation = within(votreMotivation).getByLabelText('Votre message *');
expect(descriptionMotivation).toHaveAttribute('name', 'descriptionMotivation');
expect(descriptionMotivation).toHaveAttribute('name', 'motivation');
expect(descriptionMotivation).toBeRequired();
});

Expand Down Expand Up @@ -290,6 +292,14 @@ describe('candidature conseiller', () => {
// GIVEN
vi.useFakeTimers();
vi.setSystemTime(new Date(2023, 11, 12, 13));

vi.stubGlobal('fetch', vi.fn(
() => ({ status: 200, json: async () => Promise.resolve({}) }))
);

const mockNavigate = vi.fn().mockReturnValue(() => { });
vi.spyOn(ReactRouterDom, 'useNavigate').mockReturnValue(mockNavigate);

render(<CandidatureConseiller />);
const prenom = screen.getByLabelText('Prénom *');
fireEvent.change(prenom, { target: { value: 'Jean' } });
Expand Down Expand Up @@ -322,6 +332,11 @@ describe('candidature conseiller', () => {
// GIVEN
vi.useFakeTimers();
vi.setSystemTime(new Date(2023, 11, 12, 13));

vi.stubGlobal('fetch', vi.fn(
() => ({ status: 200, json: async () => Promise.resolve({}) }))
);

render(<CandidatureConseiller />);
const prenom = screen.getByLabelText('Prénom *');
fireEvent.change(prenom, { target: { value: 'Jean' } });
Expand All @@ -347,4 +362,91 @@ describe('candidature conseiller', () => {
expect(erreurCheckboxes).toBeInTheDocument();
vi.useRealTimers();
});

it('quand je remplis le formulaire, que je l’envoie et que le serveur me renvoie une erreur, alors elle s’affiche sur la page', async () => {
// GIVEN
vi.useFakeTimers();
vi.setSystemTime(new Date(2023, 11, 12, 13));

vi.stubGlobal('fetch', vi.fn(
() => ({ status: 400, json: async () => Promise.resolve({ message: 'Cette adresse mail est déjà utilisée' }) }))
);

render(<CandidatureConseiller />);
const prenom = screen.getByLabelText('Prénom *');
fireEvent.change(prenom, { target: { value: 'Jean' } });
const nom = screen.getByLabelText('Nom *');
fireEvent.change(nom, { target: { value: 'Dupont' } });
const email = screen.getByLabelText('Adresse e-mail * Format attendu : [email protected]');
fireEvent.change(email, { target: { value: '[email protected]' } });
const enEmploi = screen.getByRole('checkbox', { name: 'En emploi' });
fireEvent.click(enEmploi);
const oui = screen.getByRole('radio', { name: 'Oui' });
fireEvent.click(oui);
const date = screen.getByLabelText('Choisir une date');
fireEvent.change(date, { target: { value: dateDujour() } });
const _5km = screen.getByRole('radio', { name: '5 km' });
fireEvent.click(_5km);
const descriptionMotivation = screen.getByLabelText('Votre message *');
fireEvent.change(descriptionMotivation, { target: { value: 'je suis motivé !' } });

// WHEN
const envoyer = screen.getByRole('button', { name: 'Envoyer votre candidature' });

// eslint-disable-next-line testing-library/no-unnecessary-act
await act(() => {
fireEvent.click(envoyer);
});

// THEN
const titreErreurValidation = screen.getByRole('heading', { level: 3, name: 'Erreur de validation' });
expect(titreErreurValidation).toBeInTheDocument();
const contenuErreurValidation = screen.getByText('Cette adresse mail est déjà utilisée', { selector: 'p' });
expect(contenuErreurValidation).toBeInTheDocument();
vi.useRealTimers();
});

it('quand je remplis le formulaire avec toutes les informations valides, alors je suis redirigé vers la page de candidature validée', async () => {
// GIVEN
vi.useFakeTimers();
vi.setSystemTime(new Date(2023, 11, 12, 13));

vi.stubGlobal('fetch', vi.fn(
() => ({ status: 200, json: async () => Promise.resolve({}) }))
);

const mockNavigate = vi.fn().mockReturnValue(() => { });
vi.spyOn(ReactRouterDom, 'useNavigate').mockReturnValue(mockNavigate);

render(<CandidatureConseiller />);
const prenom = screen.getByLabelText('Prénom *');
fireEvent.change(prenom, { target: { value: 'Jean' } });
const nom = screen.getByLabelText('Nom *');
fireEvent.change(nom, { target: { value: 'Dupont' } });
const email = screen.getByLabelText('Adresse e-mail * Format attendu : [email protected]');
fireEvent.change(email, { target: { value: '[email protected]' } });
const enEmploi = screen.getByRole('checkbox', { name: 'En emploi' });
fireEvent.click(enEmploi);
const oui = screen.getByRole('radio', { name: 'Oui' });
fireEvent.click(oui);
const date = screen.getByLabelText('Choisir une date');
fireEvent.change(date, { target: { value: dateDujour() } });
const _5km = screen.getByRole('radio', { name: '5 km' });
fireEvent.click(_5km);
const descriptionMotivation = screen.getByLabelText('Votre message *');
fireEvent.change(descriptionMotivation, { target: { value: 'je suis motivé !' } });

// WHEN
const envoyer = screen.getByRole('button', { name: 'Envoyer votre candidature' });

// eslint-disable-next-line testing-library/no-unnecessary-act
await act(() => {
fireEvent.click(envoyer);
});

// THEN
expect(mockNavigate).toHaveBeenCalledWith('/candidature-validee');

vi.useRealTimers();
});
});
Loading

0 comments on commit 8b6a101

Please sign in to comment.