From 06f3d5b8a692459e08719d5399f0481da0484d8c Mon Sep 17 00:00:00 2001 From: Fabien Mercier Date: Sun, 11 Aug 2024 12:20:40 +0200 Subject: [PATCH 1/2] On ne peut pas valider le formulaire de candidature d'un conseiller si on n'a pas rempli au moins une situation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le test n'était pas correct car il testait que la phrase s'affiche mais le formulaire se validait quand même. --- src/components/commun/Checkbox.jsx | 5 +- .../CandidatureConseiller.jsx | 17 +++-- .../CandidatureConseiller.test.jsx | 72 +++++++++++++------ .../SituationEtExperience.jsx | 2 +- vitest.setup.js | 4 ++ 5 files changed, 69 insertions(+), 31 deletions(-) diff --git a/src/components/commun/Checkbox.jsx b/src/components/commun/Checkbox.jsx index aeff91b..3c63c8f 100644 --- a/src/components/commun/Checkbox.jsx +++ b/src/components/commun/Checkbox.jsx @@ -1,11 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -export default function Checkbox({ children, id, onCheck, checked }) { +export default function Checkbox({ children, id, name, onCheck, checked }) { return (
- + @@ -17,6 +17,7 @@ export default function Checkbox({ children, id, onCheck, checked }) { Checkbox.propTypes = { children: PropTypes.node, id: PropTypes.string, + name: PropTypes.string, onCheck: PropTypes.func, checked: PropTypes.bool, }; diff --git a/src/views/candidature-conseiller/CandidatureConseiller.jsx b/src/views/candidature-conseiller/CandidatureConseiller.jsx index 7491ab8..dd6b723 100644 --- a/src/views/candidature-conseiller/CandidatureConseiller.jsx +++ b/src/views/candidature-conseiller/CandidatureConseiller.jsx @@ -23,10 +23,17 @@ export default function CandidatureConseiller() { new Array(situations.length).fill(false) ); - const valider = () => { - setIsSituationValid(situationChecks.some(checked => checked)); - if (!isSituationValid) { + const validerLaCandidature = event => { + event.preventDefault(); + + const formData = new FormData(event.currentTarget); + const situations = formData.get('situations'); + + if (situations === null) { + setIsSituationValid(false); document.getElementById('situationEtExperience').scrollIntoView(); + } else { + event.currentTarget.submit(); } }; @@ -39,7 +46,7 @@ export default function CandidatureConseiller() {

Je veux devenir conseiller numérique

Les champs avec * sont obligatoires.

-
+ - diff --git a/src/views/candidature-conseiller/CandidatureConseiller.test.jsx b/src/views/candidature-conseiller/CandidatureConseiller.test.jsx index 1adeecf..c432fe9 100644 --- a/src/views/candidature-conseiller/CandidatureConseiller.test.jsx +++ b/src/views/candidature-conseiller/CandidatureConseiller.test.jsx @@ -4,34 +4,32 @@ import CandidatureConseiller from './CandidatureConseiller'; import { textMatcher } from './test-utils'; describe('candidature conseiller', () => { - describe('étant un candidat', () => { - it('quand j’affiche le formulaire alors le titre et le menu s’affichent', () => { - // WHEN - render(); + it('quand j’affiche le formulaire alors le titre et le menu s’affichent', () => { + // WHEN + render(); - // 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', '#informationsDeContact'); + const informationsDeContact = within(menuItems[0]).getByRole('link', { name: 'Vos informations de contact' }); + expect(informationsDeContact).toHaveAttribute('href', '#informationsDeContact'); - const situationEtExperience = within(menuItems[1]).getByRole('link', { name: 'Votre situation et expérience' }); - expect(situationEtExperience).toHaveAttribute('href', '#situationEtExperience'); + const situationEtExperience = within(menuItems[1]).getByRole('link', { name: 'Votre situation et expérience' }); + expect(situationEtExperience).toHaveAttribute('href', '#situationEtExperience'); - const votreDisponibilite = within(menuItems[2]).getByRole('link', { name: 'Votre disponibilité' }); - expect(votreDisponibilite).toHaveAttribute('href', '#votreDisponibilite'); + const votreDisponibilite = within(menuItems[2]).getByRole('link', { name: 'Votre disponibilité' }); + expect(votreDisponibilite).toHaveAttribute('href', '#votreDisponibilite'); - const votreMotivation = within(menuItems[3]).getByRole('link', { name: 'Votre motivation' }); - expect(votreMotivation).toHaveAttribute('href', '#votreMotivation'); - }); + const votreMotivation = within(menuItems[3]).getByRole('link', { name: 'Votre motivation' }); + expect(votreMotivation).toHaveAttribute('href', '#votreMotivation'); }); it('quand j’affiche le formulaire alors l’étape "Vos informations de contact" est affiché', () => { @@ -283,11 +281,25 @@ describe('candidature conseiller', () => { 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(); + 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 : nom@domaine.fr'); + fireEvent.change(email, { target: { value: 'jean.dupont@example.com' } }); 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 @@ -298,9 +310,23 @@ describe('candidature conseiller', () => { 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(); + 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 : nom@domaine.fr'); + fireEvent.change(email, { target: { value: 'jean.dupont@example.com' } }); + 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 diff --git a/src/views/candidature-conseiller/SituationEtExperience.jsx b/src/views/candidature-conseiller/SituationEtExperience.jsx index bd214bd..bfbc7bd 100644 --- a/src/views/candidature-conseiller/SituationEtExperience.jsx +++ b/src/views/candidature-conseiller/SituationEtExperience.jsx @@ -26,7 +26,7 @@ export default function SituationEtExperience({ situationChecks, setSituationChe Êtes-vous actuellement dans l’une des situations suivantes ? *

{situations.map(({ id, libelle }, index) => - + {libelle} )} diff --git a/vitest.setup.js b/vitest.setup.js index e856011..730c7d2 100644 --- a/vitest.setup.js +++ b/vitest.setup.js @@ -1 +1,5 @@ +import { vi } from 'vitest'; import 'vitest-dom/extend-expect'; + +// scrollIntoView is not implemented in jsdom +window.HTMLElement.prototype.scrollIntoView = vi.fn(); From f2ceb1011f1efd9e5c5a445fe78abaf30a3dc8db Mon Sep 17 00:00:00 2001 From: Fabien Mercier Date: Mon, 12 Aug 2024 09:09:22 +0200 Subject: [PATCH 2/2] =?UTF-8?q?Simplification=20du=20formulaire=20de=20can?= =?UTF-8?q?didature=20conseiller=20en=20r=C3=A9duisant=20le=20nombre=20de?= =?UTF-8?q?=20rendu=20quand=20on=20coche=20une=20situation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/commun/Checkbox.jsx | 5 ++-- .../CandidatureConseiller.jsx | 12 ++-------- .../CandidatureConseiller.test.jsx | 23 +++++++++---------- src/views/candidature-conseiller/EnResume.jsx | 2 +- .../SituationEtExperience.jsx | 14 +++-------- 5 files changed, 19 insertions(+), 37 deletions(-) diff --git a/src/components/commun/Checkbox.jsx b/src/components/commun/Checkbox.jsx index 3c63c8f..70406fd 100644 --- a/src/components/commun/Checkbox.jsx +++ b/src/components/commun/Checkbox.jsx @@ -1,11 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -export default function Checkbox({ children, id, name, onCheck, checked }) { +export default function Checkbox({ children, id, name, onCheck }) { return (
- + @@ -19,5 +19,4 @@ Checkbox.propTypes = { id: PropTypes.string, name: PropTypes.string, onCheck: PropTypes.func, - checked: PropTypes.bool, }; diff --git a/src/views/candidature-conseiller/CandidatureConseiller.jsx b/src/views/candidature-conseiller/CandidatureConseiller.jsx index dd6b723..ae2d189 100644 --- a/src/views/candidature-conseiller/CandidatureConseiller.jsx +++ b/src/views/candidature-conseiller/CandidatureConseiller.jsx @@ -5,7 +5,6 @@ import SituationEtExperience from './SituationEtExperience'; import Disponibilite from './Disponibilite'; import Motivation from './Motivation'; import EnResume from './EnResume'; -import { situations } from './situations'; import '@gouvfr/dsfr/dist/component/form/form.min.css'; import '@gouvfr/dsfr/dist/component/input/input.min.css'; @@ -17,11 +16,8 @@ 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) - ); const validerLaCandidature = event => { event.preventDefault(); @@ -48,11 +44,7 @@ export default function CandidatureConseiller() {

Les champs avec * sont obligatoires.

- + diff --git a/src/views/candidature-conseiller/CandidatureConseiller.test.jsx b/src/views/candidature-conseiller/CandidatureConseiller.test.jsx index c432fe9..41c6492 100644 --- a/src/views/candidature-conseiller/CandidatureConseiller.test.jsx +++ b/src/views/candidature-conseiller/CandidatureConseiller.test.jsx @@ -106,7 +106,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(); @@ -214,7 +214,7 @@ describe('candidature conseiller', () => { render(); // 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( @@ -232,14 +232,13 @@ describe('candidature conseiller', () => { expect(descriptionResume).toBeInTheDocument(); }); - it('quand je modifie la date de disponibilité, elle s’affiche dans l’encart "En résumé" est affiché', () => { + it('quand je modifie la date de disponibilité alors elle s’affiche dans l’encart "En résumé" est affiché', () => { // GIVEN render(); - 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( @@ -263,8 +262,8 @@ describe('candidature conseiller', () => { }, ]; - vi.spyOn(global, 'fetch').mockResolvedValue({ - json: () => Promise.resolve(geoApiResponse) + vi.spyOn(global, 'fetch').mockResolvedValueOnce({ + json: async () => Promise.resolve(geoApiResponse) }); // WHEN @@ -272,8 +271,8 @@ describe('candidature conseiller', () => { 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(); }); @@ -303,7 +302,7 @@ describe('candidature conseiller', () => { 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(); }); @@ -330,7 +329,7 @@ describe('candidature conseiller', () => { 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(); }); }); diff --git a/src/views/candidature-conseiller/EnResume.jsx b/src/views/candidature-conseiller/EnResume.jsx index f45eab7..18ee81f 100644 --- a/src/views/candidature-conseiller/EnResume.jsx +++ b/src/views/candidature-conseiller/EnResume.jsx @@ -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(); diff --git a/src/views/candidature-conseiller/SituationEtExperience.jsx b/src/views/candidature-conseiller/SituationEtExperience.jsx index bfbc7bd..9e0e8cf 100644 --- a/src/views/candidature-conseiller/SituationEtExperience.jsx +++ b/src/views/candidature-conseiller/SituationEtExperience.jsx @@ -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); }; @@ -25,8 +19,8 @@ export default function SituationEtExperience({ situationChecks, setSituationChe

Êtes-vous actuellement dans l’une des situations suivantes ? *

- {situations.map(({ id, libelle }, index) => - + {situations.map(({ id, libelle }) => + {libelle} )} @@ -58,7 +52,5 @@ export default function SituationEtExperience({ situationChecks, setSituationChe } SituationEtExperience.propTypes = { - situationChecks: PropTypes.array, - setSituationChecks: PropTypes.func, isSituationValid: PropTypes.bool };