Skip to content

Commit

Permalink
Feat: Creation formulaire candidature structure coordo (#198)
Browse files Browse the repository at this point in the history
* Refacto formulaire conseiller

* :feat intialisation des tests formulaire candidature structure

* test formulaire partie 'information contact' + 'votre besoin CN'

* Retours de code review

* Ajout d'un scroll vers la bonne section du formulaire

* partie Votre motivation + test

* :feat ajout premier bloc formulaire informations contact structure

* Refacto des informations de structure

* partie engagement + test

* :feat ajout premier bloc formulaire informations contact structure

* Refacto des informations de structure

* Correction des tests

* Ajout du formulaire coordinateur

---------

Co-authored-by: dienamo <[email protected]>
Co-authored-by: Ornella Ourfi <[email protected]>
  • Loading branch information
3 people authored Aug 29, 2024
1 parent a2dcd52 commit 30be7ba
Show file tree
Hide file tree
Showing 17 changed files with 540 additions and 70 deletions.
2 changes: 2 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function App() {
const CarteCoordinateur = lazy(() => import('./views/coordination-territoriale/CarteCoordinateur'));
const PageCandidatureConseiller = lazy(() => import('./views/candidature-conseiller/PageCandidatureConseiller'));
const PageCandidatureStructure = lazy(() => import('./views/candidature-structure/PageCandidatureStructure'));
const PageCandidatureCoordinateur = lazy(() => import('./views/candidature-coordinateur/PageCandidatureCoordinateur'));

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

export default function Checkbox({ children, id, onCheck, checked, required = true }) {
export default function Checkbox({ children, id, name, onCheck, required = true }) {
return (
<div className="fr-fieldset__element">
<div className="fr-checkbox-group">
<input id={id} type="checkbox" onChange={onCheck} checked={checked} required={required}/>
<input id={id} type="checkbox" name={name} onChange={onCheck} required={required} />
<label className="fr-label" htmlFor={id}>
{children}
</label>
Expand All @@ -17,7 +17,7 @@ export default function Checkbox({ children, id, onCheck, checked, required = tr
Checkbox.propTypes = {
children: PropTypes.node,
id: PropTypes.string,
name: PropTypes.string,
onCheck: PropTypes.func,
checked: PropTypes.bool,
required: PropTypes.bool,
required: PropTypes.bool
};
30 changes: 15 additions & 15 deletions src/views/candidature-conseiller/CandidatureConseiller.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,22 @@ import '@gouvfr/dsfr/dist/component/sidemenu/sidemenu.min.css';
import './CandidatureConseiller.css';

export default function CandidatureConseiller() {
const [dateDisponibilite, setDateDisponibilite] = useState();
const [dateDisponibilite, setDateDisponibilite] = useState('');
const [isSituationValid, setIsSituationValid] = useState(true);
const [situationChecks, setSituationChecks] = useState(
new Array(situations.length).fill(false)
);

useScrollToSection();

const valider = () => {
setIsSituationValid(situationChecks.some(checked => checked));
if (!isSituationValid) {
document.getElementById('situationEtExperience').scrollIntoView();
const validerLaCandidature = event => {
event.preventDefault();

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

if (situations === null) {
setIsSituationValid(false);
document.getElementById('situation-et-experience').scrollIntoView();
} else {
event.currentTarget.submit();
}
};

Expand All @@ -42,17 +46,13 @@ 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">
<form aria-label="Candidature conseiller" onSubmit={validerLaCandidature}>
<InformationsDeContact />
<SituationEtExperience
situationChecks={situationChecks}
setSituationChecks={setSituationChecks}
isSituationValid={isSituationValid}
/>
<SituationEtExperience isSituationValid={isSituationValid} />
<Disponibilite setDateDisponibilite={setDateDisponibilite} />
<Motivation />
<EnResume dateDisponibilite={dateDisponibilite} />
<button className="fr-btn cc-envoyer" type="submit" onClick={valider}>
<button className="fr-btn cc-envoyer" type="submit">
Envoyer votre candidature
</button>
</form>
Expand Down
93 changes: 59 additions & 34 deletions src/views/candidature-conseiller/CandidatureConseiller.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,32 @@ vi.mock('react-router-dom', () => ({
}));

describe('candidature conseiller', () => {
describe('étant un candidat', () => {
it('quand j’affiche le formulaire alors le titre et le menu s’affichent', () => {
// WHEN
render(<CandidatureConseiller />);
it('quand j’affiche le formulaire alors le titre et le menu s’affichent', () => {
// WHEN
render(<CandidatureConseiller />);

// THEN
const titre = screen.getByRole('heading', { level: 1, name: 'Je veux devenir conseiller numérique' });
expect(titre).toBeInTheDocument();
// THEN
const titre = screen.getByRole('heading', { level: 1, name: 'Je veux devenir conseiller numérique' });
expect(titre).toBeInTheDocument();

const champsObligatoires = screen.getByText(textMatcher('Les champs avec * sont obligatoires.'), { selector: 'p' });
expect(champsObligatoires).toBeInTheDocument();
const champsObligatoires = screen.getByText(textMatcher('Les champs avec * sont obligatoires.'), { selector: 'p' });
expect(champsObligatoires).toBeInTheDocument();

const navigation = screen.getByRole('navigation', { name: 'Sommaire' });
const menu = within(navigation).getByRole('list');
const menuItems = within(menu).getAllByRole('listitem');
const navigation = screen.getByRole('navigation', { name: 'Sommaire' });
const menu = within(navigation).getByRole('list');
const menuItems = within(menu).getAllByRole('listitem');

const informationsDeContact = within(menuItems[0]).getByRole('link', { name: 'Vos informations de contact' });
expect(informationsDeContact).toHaveAttribute('href', '#informations-de-contact');
const informationsDeContact = within(menuItems[0]).getByRole('link', { name: 'Vos informations de contact' });
expect(informationsDeContact).toHaveAttribute('href', '#informations-de-contact');

const situationEtExperience = within(menuItems[1]).getByRole('link', { name: 'Votre situation et expérience' });
expect(situationEtExperience).toHaveAttribute('href', '#situation-et-experience');
const situationEtExperience = within(menuItems[1]).getByRole('link', { name: 'Votre situation et expérience' });
expect(situationEtExperience).toHaveAttribute('href', '#situation-et-experience');

const votreDisponibilite = within(menuItems[2]).getByRole('link', { name: 'Votre disponibilité' });
expect(votreDisponibilite).toHaveAttribute('href', '#votre-disponibilite');
const votreDisponibilite = within(menuItems[2]).getByRole('link', { name: 'Votre disponibilité' });
expect(votreDisponibilite).toHaveAttribute('href', '#votre-disponibilite');

const votreMotivation = within(menuItems[3]).getByRole('link', { name: 'Votre motivation' });
expect(votreMotivation).toHaveAttribute('href', '#votre-motivation');
});
const votreMotivation = within(menuItems[3]).getByRole('link', { name: 'Votre motivation' });
expect(votreMotivation).toHaveAttribute('href', '#votre-motivation');
});

it('quand j’affiche le formulaire alors l’étape "Vos informations de contact" est affiché', () => {
Expand Down Expand Up @@ -113,7 +111,7 @@ describe('candidature conseiller', () => {
expect(non).toHaveAttribute('name', 'experienceProfessionnelle');
});

it('quand je coche "diplomé", un champ pour préciser le diplôme s’affiche', () => {
it('quand je coche "diplomé" alors un champ pour préciser le diplôme s’affiche', () => {
// GIVEN
render(<CandidatureConseiller />);

Expand Down Expand Up @@ -221,7 +219,7 @@ describe('candidature conseiller', () => {
render(<CandidatureConseiller />);

// THEN
const enResume = screen.getByText(textMatcher('En résumé'), { selector: 'p' });
const enResume = screen.getByText('En résumé', { selector: 'p' });
expect(enResume).toBeInTheDocument();

const titreResume = screen.getByText(
Expand All @@ -242,11 +240,10 @@ describe('candidature conseiller', () => {
it('quand je modifie la date de disponibilité, alors elle s’affiche dans l’encart "En résumé"', () => {
// GIVEN
render(<CandidatureConseiller />);
const dateDisponibilite = '2023-12-12';

// WHEN
const date = screen.getByLabelText('Choisir une date');
fireEvent.change(date, { target: { value: dateDisponibilite } });
const dateDisponibilite = screen.getByLabelText('Choisir une date');
fireEvent.change(dateDisponibilite, { target: { value: '2023-12-12' } });

// THEN
const titreResume = screen.getByText(
Expand All @@ -270,46 +267,74 @@ describe('candidature conseiller', () => {
},
];

vi.spyOn(global, 'fetch').mockResolvedValue({
json: () => Promise.resolve(geoApiResponse)
vi.spyOn(global, 'fetch').mockResolvedValueOnce({
json: async () => Promise.resolve(geoApiResponse)
});

// WHEN
const adresse = screen.getByLabelText('Votre lieu d’habitation Saississez le nom ou le code postal de votre commune.');
fireEvent.change(adresse, { target: { value: 'par' } });

// THEN
const paris = await screen.findByText(textMatcher('75001 Paris'), { selector: 'option' });
const parisot = await screen.findByText(textMatcher('82137 Parisot'), { selector: 'option' });
const paris = await screen.findByRole('option', { name: '75001 Paris', hidden: true });
const parisot = await screen.findByRole('option', { name: '82137 Parisot', hidden: true });
expect(paris).toBeInTheDocument();
expect(parisot).toBeInTheDocument();
});

it('quand je coche au moins une case de situation et que je valide le formulaire alors il n’y a pas d’erreur de validation', () => {
// GIVEN
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: '2023-12-12' } });
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', { type: 'submit' });
const envoyer = screen.getByRole('button', { name: 'Envoyer votre candidature' });
fireEvent.click(envoyer);

// THEN
const erreurCheckboxes = screen.queryByText(textMatcher('Vous devez cocher au moins une case'), { selector: 'p' });
const erreurCheckboxes = screen.queryByText('Vous devez cocher au moins une case', { selector: 'p' });
expect(erreurCheckboxes).not.toBeInTheDocument();
});

it('quand je ne coche pas de case de situation et que je valide le formulaire alors il y a une erreur de validation', () => {
// GIVEN
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 oui = screen.getByRole('radio', { name: 'Oui' });
fireEvent.click(oui);
const date = screen.getByLabelText('Choisir une date');
fireEvent.change(date, { target: { value: '2023-12-12' } });
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', { type: 'submit' });
const envoyer = screen.getByRole('button', { name: 'Envoyer votre candidature' });
fireEvent.click(envoyer);

// THEN
const erreurCheckboxes = screen.getByText(textMatcher('Vous devez cocher au moins une case'), { selector: 'p' });
const erreurCheckboxes = screen.getByText('Vous devez cocher au moins une case', { selector: 'p' });
expect(erreurCheckboxes).toBeInTheDocument();
});
});
2 changes: 1 addition & 1 deletion src/views/candidature-conseiller/EnResume.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import PropTypes from 'prop-types';

export default function EnResume({ dateDisponibilite }) {
const formatDate = () => {
if (!dateDisponibilite) {
if (dateDisponibilite === '') {
return '[Renseignez votre date de disponibilité]';
}
return new Date(dateDisponibilite).toLocaleDateString();
Expand Down
14 changes: 3 additions & 11 deletions src/views/candidature-conseiller/SituationEtExperience.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,10 @@ import Input from '../../components/commun/Input';
import { situations } from './situations';
import PropTypes from 'prop-types';

export default function SituationEtExperience({ situationChecks, setSituationChecks, isSituationValid }) {
export default function SituationEtExperience({ isSituationValid }) {
const [isDiplomeSelected, setIsDiplomeSelected] = useState(false);

const handleCheck = event => {
const indexCaseCochee = situations.findIndex(situation => situation.id === event.target.id);
const updatedCheckedState = situationChecks.map((item, index) =>
index === indexCaseCochee ? !item : item
);
setSituationChecks(updatedCheckedState);

setIsDiplomeSelected(event.target.id === 'diplome' && event.target.checked);
};

Expand All @@ -25,8 +19,8 @@ export default function SituationEtExperience({ situationChecks, setSituationChe
<p className="fr-mb-3w cc-bold">
Êtes-vous actuellement dans l’une des situations suivantes ? <span className="cc-obligatoire">*</span>
</p>
{situations.map(({ id, libelle }, index) =>
<Checkbox id={id} key={id} onCheck={handleCheck} checked={situationChecks[index]} required={false}>
{situations.map(({ id, libelle }) =>
<Checkbox id={id} key={id} name="situations" onCheck={handleCheck} required={false}>
{libelle}
</Checkbox>
)}
Expand Down Expand Up @@ -58,7 +52,5 @@ export default function SituationEtExperience({ situationChecks, setSituationChe
}

SituationEtExperience.propTypes = {
situationChecks: PropTypes.array,
setSituationChecks: PropTypes.func,
isSituationValid: PropTypes.bool
};
34 changes: 34 additions & 0 deletions src/views/candidature-coordinateur/BesoinEnCoordinateur.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import BoutonRadio from '../../components/commun/BoutonRadio';
import Datepicker from '../../components/commun/Datepicker';

export default function BesoinEnCoordinateur() {
return (
<fieldset className="fr-border cc-section fr-p-3w fr-mb-3w" id="votre-besoin-en-coordinateur">
<legend className="fr-h5">Votre besoin en coordinateur</legend>
<hr />
<p className="fr-mb-3w cc-bold">
Avez-vous déjà identifié un candidat pour le poste de coordinateur de conseiller numérique ?<span className="cc-obligatoire">*</span>
</p>
<p className="fr-text--sm fr-hint-text">Si oui, merci d’inviter ce candidat à s’inscrire sur la plateforme Conseiller numérique</p>
<BoutonRadio id="oui" nomGroupe="identificationCandidat">
Oui
</BoutonRadio>
<BoutonRadio id="non" nomGroupe="identificationCandidat">
Non
</BoutonRadio>
<p className="fr-mt-4w fr-mb-3w cc-bold">Le coordinateur<span className="cc-obligatoire">*</span></p>
<BoutonRadio id="coordination" nomGroupe="coordinateur">
Effectuera uniquement des missions de coordination
</BoutonRadio>
<BoutonRadio id="publics" nomGroupe="coordinateur">
Accompagnera également des publics
</BoutonRadio>
<hr />
<p className="fr-mb-3w cc-bold">À partir de quand êtes vous prêt à accueillir votre coordinateur ?<span className="cc-obligatoire">*</span></p>
<Datepicker id="choisir-date">
Choisir une date
</Datepicker>
</fieldset >
);
}
45 changes: 45 additions & 0 deletions src/views/candidature-coordinateur/CandidatureCoordinateur.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import SommaireCoordinateur from './SommaireCoordinateur';
import InformationsDeContact from '../candidature-structure/InformationsDeContact';
import InformationsDeStructure from '../candidature-structure/InformationsDeStructure';
import BesoinEnCoordinateur from './BesoinEnCoordinateur';
import Motivation from './Motivation';
import Engagement from './Engagement';
import { useScrollToSection } from '../../hooks/useScrollToSection';

import '@gouvfr/dsfr/dist/component/form/form.min.css';
import '@gouvfr/dsfr/dist/component/input/input.min.css';
import '@gouvfr/dsfr/dist/component/checkbox/checkbox.min.css';
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 '../candidature-conseiller/CandidatureConseiller.css';

export default function CandidatureCoordinateur() {
useScrollToSection();

return (
<div className="fr-container fr-mt-5w fr-mb-5w">
<div className="fr-grid-row">
<div className="fr-col-12 fr-col-md-4">
<SommaireCoordinateur />
</div>
<div className="fr-col-12 fr-col-md-8 fr-py-12v">
<h1 className="cc-titre fr-mb-5w">Je souhaite engager un coordinateur pour mes conseillers numériques</h1>
<p className="fr-text--sm fr-hint-text">Les champs avec <span className="cc-obligatoire">*</span> sont obligatoires.</p>
<form aria-label="Candidature coordinateur" >
<InformationsDeStructure />
<InformationsDeContact />
<BesoinEnCoordinateur />
<Motivation />
<Engagement />
<button className="fr-btn cc-envoyer fr-mt-3w" type="submit">
Envoyer votre candidature
</button>
</form>
</div>
</div>
</div>
);
}
Loading

0 comments on commit 30be7ba

Please sign in to comment.