From 82ec0cf4b61d572b2a9dc4cb0cabf3aebf60b163 Mon Sep 17 00:00:00 2001 From: fuuuzz Date: Wed, 28 Jan 2026 11:26:19 +0100 Subject: [PATCH 01/56] feat: anihilate landbot --- .github/workflows/e2e.yml | 1 - .github/workflows/lighthouse.yml | 1 - apps/frontend/.env.example | 1 - .../EligibilityTresholds.svelte} | 0 .../simulateur-eligibilite/Chatbot.svelte | 45 ------------------- .../src/routes/mentions-legales/+page.svelte | 6 --- .../simulateur-eligibilite/+page.server.ts | 8 ---- .../simulateur-eligibilite/+page.svelte | 6 +-- 8 files changed, 2 insertions(+), 66 deletions(-) rename apps/frontend/src/lib/components/{pages/simulateur-eligibilite/Simulator.svelte => common/EligibilityTresholds.svelte} (100%) delete mode 100644 apps/frontend/src/lib/components/pages/simulateur-eligibilite/Chatbot.svelte delete mode 100644 apps/frontend/src/routes/simulateur-eligibilite/+page.server.ts diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index ed80644c2..5608ac344 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -8,7 +8,6 @@ jobs: runs-on: ubuntu-latest env: - LANDBOT_CONFIG_URL: ${{ secrets.LANDBOT_CONFIG_URL }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} API_URL: ${{ secrets.API_URL }} API_KEY: ${{ secrets.API_KEY }} diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml index cb75d461e..0b4ed0d62 100644 --- a/.github/workflows/lighthouse.yml +++ b/.github/workflows/lighthouse.yml @@ -10,7 +10,6 @@ jobs: runs-on: ubuntu-latest env: - LANDBOT_CONFIG_URL: ${{ secrets.LANDBOT_CONFIG_URL }} PUBLIC_NODE_ENV: ${{ secrets.PUBLIC_NODE_ENV }} API_URL: ${{ secrets.API_URL }} API_KEY: ${{ secrets.API_KEY }} diff --git a/apps/frontend/.env.example b/apps/frontend/.env.example index 66abcc67e..3bbbf8257 100644 --- a/apps/frontend/.env.example +++ b/apps/frontend/.env.example @@ -1,4 +1,3 @@ -LANDBOT_CONFIG_URL= # Fill with correct value SENTRY_AUTH_TOKEN= # Fill with correct value API_URL= # Fill with correct value API_KEY= # Fill with correct value diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/Simulator.svelte b/apps/frontend/src/lib/components/common/EligibilityTresholds.svelte similarity index 100% rename from apps/frontend/src/lib/components/pages/simulateur-eligibilite/Simulator.svelte rename to apps/frontend/src/lib/components/common/EligibilityTresholds.svelte diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/Chatbot.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/Chatbot.svelte deleted file mode 100644 index 1c773c207..000000000 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/Chatbot.svelte +++ /dev/null @@ -1,45 +0,0 @@ - - -
-
-
- {@html MascotteWaving} -
-

Voici BoRiS !

-

- En quelques questions, grâce à sa connaissance des différents dispositifs, - il vous indiquera votre éligibilité au BRS et vous aiguillera vers - d'autres outils ou aides. -

-
- -
-
-
diff --git a/apps/frontend/src/routes/mentions-legales/+page.svelte b/apps/frontend/src/routes/mentions-legales/+page.svelte index ab0063c43..46045f1ee 100644 --- a/apps/frontend/src/routes/mentions-legales/+page.svelte +++ b/apps/frontend/src/routes/mentions-legales/+page.svelte @@ -33,10 +33,4 @@ Ce site est hébergé en France par l'entreprise Scalingo, domiciliée au 15 avenue du Rhin 67000 Strasbourg.

- - -

- Landbot Hello Umi S.L Av. Josep Tarradellas, 20, Floor 6, CP 08029B - Barcelona Spain -

diff --git a/apps/frontend/src/routes/simulateur-eligibilite/+page.server.ts b/apps/frontend/src/routes/simulateur-eligibilite/+page.server.ts deleted file mode 100644 index eb843be60..000000000 --- a/apps/frontend/src/routes/simulateur-eligibilite/+page.server.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { PageServerLoad } from './$types'; -import { LANDBOT_CONFIG_URL } from '$env/static/private'; - -export const load: PageServerLoad = () => { - return { - landbotConfigUrl: LANDBOT_CONFIG_URL, - }; -}; diff --git a/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte b/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte index 6391838bf..19f3cadb2 100644 --- a/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte +++ b/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte @@ -3,8 +3,7 @@ import GradientBackgroundWrapper from '$components/common/GradientBackgroundWrapper.svelte'; import Hero from '$components/pages/simulateur-eligibilite/Hero.svelte'; - import Chatbot from '$components/pages/simulateur-eligibilite/Chatbot.svelte'; - import Simulator from '$components/pages/simulateur-eligibilite/Simulator.svelte'; + import EligibilityTresholds from '$components/common/EligibilityTresholds.svelte'; import PageCta from '$components/common/PageCta.svelte'; const { data }: PageProps = $props(); @@ -19,7 +18,6 @@ - - + From 0fae33a988ef66ed7ad0972ccc2e089ad779a4bf Mon Sep 17 00:00:00 2001 From: fuuuzz Date: Wed, 28 Jan 2026 11:47:08 +0100 Subject: [PATCH 02/56] feat: clean eligibility thresholds component --- .../common/EligibilityTresholds.svelte | 95 ++++++++++++++++--- .../pages/simulateur-eligibilite/Data.svelte | 49 ---------- .../simulateur-eligibilite/+page.svelte | 2 + .../+page.svelte | 45 +-------- 4 files changed, 87 insertions(+), 104 deletions(-) delete mode 100644 apps/frontend/src/lib/components/pages/simulateur-eligibilite/Data.svelte diff --git a/apps/frontend/src/lib/components/common/EligibilityTresholds.svelte b/apps/frontend/src/lib/components/common/EligibilityTresholds.svelte index 091f4cf26..743807f36 100644 --- a/apps/frontend/src/lib/components/common/EligibilityTresholds.svelte +++ b/apps/frontend/src/lib/components/common/EligibilityTresholds.svelte @@ -1,29 +1,54 @@ - +

Les plafonds de ressources d'éligibilité au Bail Réel Solidaire.

-

+

Les plafonds en vigueur depuis janvier 2026 sont définis à partir de votre catégorie de ménage et la zone de votre futur bien. Elle correspond à la tension immobilière, du plus tendu où les prix sont les plus haut, Abis, au moins tendu, C.

- - Pour connaître votre zone - -

+

+ + Connaître ma zone + +

+ +

+ Les revenus fiscaux de référence* du ménage ne doivent pas dépasser les + plafonds de ressources ci-dessous : +

+

+ + Où Trouver mon revenus fiscal de référence + +

+

A quelle catégorie de ménage appartenez-vous ?

- + {@render data()}
+ +{#snippet data()} + {#each eligibilityData as data} +
+

{data.category}

+ {#if data.options.length} + +
    + {@html `
  • ${data.options.join('
  • ou ')}
  • `} +
+
+ {/if} +
+
+
+

+ Zones A et Abis +

+

+ Zone B1 +

+

+ Zone B2 et C +

+
+
+

+ {formatEuro(data.zoneAandAbis)} +

+

+ {formatEuro(data.zoneB1)} +

+

+ {formatEuro(data.zoneB2andC)} +

+
+
+ {/each} +{/snippet} diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/Data.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/Data.svelte deleted file mode 100644 index ad2eb4ff7..000000000 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/Data.svelte +++ /dev/null @@ -1,49 +0,0 @@ - - -{#each eligibilityData as data} -
-

{data.category}

- {#if data.options.length} - -
    - {@html `
  • ${data.options.join('
  • ou ')}
  • `} -
-
- {/if} -
-
-
-

- Zones A et Abis -

-

- Zone B1 -

-

- Zone B2 et C -

-
-
-

- {formatEuro(data.zoneAandAbis)} -

-

- {formatEuro(data.zoneB1)} -

-

- {formatEuro(data.zoneB2andC)} -

-
-
-{/each} diff --git a/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte b/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte index 19f3cadb2..cb4710ea2 100644 --- a/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte +++ b/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte @@ -5,6 +5,7 @@ import Hero from '$components/pages/simulateur-eligibilite/Hero.svelte'; import EligibilityTresholds from '$components/common/EligibilityTresholds.svelte'; import PageCta from '$components/common/PageCta.svelte'; + import SimulationCta from '$components/pages/simulateur-eligibilite/SimulationCta.svelte'; const { data }: PageProps = $props(); @@ -18,6 +19,7 @@ + diff --git a/apps/frontend/src/routes/tout-savoir-sur-le-bail-reel-solidaire-brs/+page.svelte b/apps/frontend/src/routes/tout-savoir-sur-le-bail-reel-solidaire-brs/+page.svelte index 9f6453b72..c56200241 100644 --- a/apps/frontend/src/routes/tout-savoir-sur-le-bail-reel-solidaire-brs/+page.svelte +++ b/apps/frontend/src/routes/tout-savoir-sur-le-bail-reel-solidaire-brs/+page.svelte @@ -11,9 +11,8 @@ import Section from '$components/common/Section.svelte'; import Accordion from '$components/common/Accordion.svelte'; import VerticalStepper from '$components/common/Steppers/VerticalStepper.svelte'; - import ShadowedBox from '$components/common/ShadowedBox.svelte'; - import Data from '$components/pages/simulateur-eligibilite/Data.svelte'; import ArticleCard from '$components/pages/blog/ArticleCard.svelte'; + import EligibilityTresholds from '$components/common/EligibilityTresholds.svelte'; const firstArticle = articles.find( (article) => @@ -309,46 +308,6 @@
- -

- Les plafonds de revenus en bail réel solidaire (BRS) -

-

- Les plafonds en vigueur depuis janvier 2025 sont définis à partir de - votre catégorie de ménage et la zone de votre futur bien. Elle - correspond à la tension immobilière, du plus tendu où les prix sont les - plus haut, Abis, au moins tendu, C. -

-

- Le logement acheté en bail réel solidaire doit être utilisé en tant que - résidence principale. -

-

- Les revenus fiscaux de référence* du ménage ne doivent pas dépasser les - plafonds de ressources ci-dessous : -

-
- - *Où se situe le revenus fiscal de référence sur votre avis - d'imposition - - - - - - - +
From 3009914481d55d8ba066358acd5a2313096396d7 Mon Sep 17 00:00:00 2001 From: fuuuzz Date: Wed, 28 Jan 2026 12:20:22 +0100 Subject: [PATCH 03/56] feat: new simulator cta --- .../common/EligibilityTresholds.svelte | 2 +- .../pages/simulateur-eligibilite/Hero.svelte | 4 +-- .../SimulationCta.svelte | 35 +++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 apps/frontend/src/lib/components/pages/simulateur-eligibilite/SimulationCta.svelte diff --git a/apps/frontend/src/lib/components/common/EligibilityTresholds.svelte b/apps/frontend/src/lib/components/common/EligibilityTresholds.svelte index 743807f36..bd6ba4e20 100644 --- a/apps/frontend/src/lib/components/common/EligibilityTresholds.svelte +++ b/apps/frontend/src/lib/components/common/EligibilityTresholds.svelte @@ -42,7 +42,7 @@ class="fr-link fr-link--icon-right" target="_blank" rel="noopener"> - Où Trouver mon revenus fiscal de référence + Où trouver mon revenus fiscal de référence

diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/Hero.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/Hero.svelte index 6991df361..f8e92250e 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/Hero.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/Hero.svelte @@ -2,9 +2,9 @@ import Photograph from '$components/common/Photograph.svelte'; -
+

+ class="text-center !mx-auto md:text-left md:!ml-0 !text-3xl !leading-[2rem] !leading-[2.5rem] sm:!text-4xl md:!leading-[3rem] lg:!text-5xl"> Avec BoRiS,
découvrez si vous êtes
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/SimulationCta.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/SimulationCta.svelte new file mode 100644 index 000000000..a510057d6 --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/SimulationCta.svelte @@ -0,0 +1,35 @@ + + +
+
+
+
+ {@html MascotteWaving} +
+ +

Le simulateur d'éligibilité de BoRiS

+

+ Le bail réel solidaire (BRS) est un dispositif mis en place par l'État + permettant à des ménages qui ne pourraient pas devenir propriétaires + d'un logement au prix du marché d'accéder à la propriété de leur + résidence principale. Le Bail Réel Solidaire s'adresse à des ménages + dont les ressources ne dépassent pas un certain plafond : environ 87% + des personnes résidant en France sont éligibles. +

+

+ Pour savoir si vous êtes éligible au Bail Réel Solidaire, il vous suffit + de remplir le simulateur d'éligibilité de BoRiS. Vous aurez besoin de + vos avis d'imposition des années (n-1 et n-2)/(n-2 et n-3)À CLARIFIER + ICI]. +

+ + Je lance ma simulation + +
+
+
From 37ce2c5224e6069e10d89fd5278cb62ea2cb4afb Mon Sep 17 00:00:00 2001 From: fuuuzz Date: Wed, 28 Jan 2026 12:38:11 +0100 Subject: [PATCH 04/56] rebase --- .../src/lib/components/common/EligibilityTresholds.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/lib/components/common/EligibilityTresholds.svelte b/apps/frontend/src/lib/components/common/EligibilityTresholds.svelte index bd6ba4e20..b946ce6fc 100644 --- a/apps/frontend/src/lib/components/common/EligibilityTresholds.svelte +++ b/apps/frontend/src/lib/components/common/EligibilityTresholds.svelte @@ -38,11 +38,11 @@

- Où trouver mon revenus fiscal de référence + Où trouver mon revenu fiscal de référence

From c8afac7873b4a05e45498386a090d65d6a87ea98 Mon Sep 17 00:00:00 2001 From: fuuuzz Date: Wed, 28 Jan 2026 18:45:43 +0100 Subject: [PATCH 05/56] feat: commonize simulator components --- .../common/Simulator/Description.svelte | 40 +++++++++++ .../Simulator}/Form.svelte | 0 .../Simulator}/Wrapper.svelte | 0 .../simulateur-acquisition/Description.svelte | 30 --------- .../pages/simulateur-acquisition/Step1.svelte | 15 +++-- .../pages/simulateur-acquisition/Step2.svelte | 15 +++-- .../pages/simulateur-acquisition/Step3.svelte | 15 +++-- .../pages/simulateur-acquisition/Step4.svelte | 24 +++++-- .../pages/simulateur-acquisition/Step5.svelte | 15 +++-- .../pages/simulateur-acquisition/Step6.svelte | 24 +++++-- .../pages/simulateur-acquisition/Step7.svelte | 15 +++-- .../pages/simulateur-acquisition/Step8.svelte | 14 ++-- .../SimulationCta.svelte | 2 +- .../simulateur-eligibilite/steps/Step1.svelte | 66 +++++++++++++++++++ .../simulateur-eligibilite/+page.svelte | 4 +- .../simulateur-eligibilite/steps/+page.svelte | 5 ++ 16 files changed, 211 insertions(+), 73 deletions(-) create mode 100644 apps/frontend/src/lib/components/common/Simulator/Description.svelte rename apps/frontend/src/lib/components/{pages/simulateur-acquisition => common/Simulator}/Form.svelte (100%) rename apps/frontend/src/lib/components/{pages/simulateur-acquisition => common/Simulator}/Wrapper.svelte (100%) delete mode 100644 apps/frontend/src/lib/components/pages/simulateur-acquisition/Description.svelte create mode 100644 apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step1.svelte create mode 100644 apps/frontend/src/routes/simulateur-eligibilite/steps/+page.svelte diff --git a/apps/frontend/src/lib/components/common/Simulator/Description.svelte b/apps/frontend/src/lib/components/common/Simulator/Description.svelte new file mode 100644 index 000000000..486c223e8 --- /dev/null +++ b/apps/frontend/src/lib/components/common/Simulator/Description.svelte @@ -0,0 +1,40 @@ + + +
+
+
+

{title}

+
+ + {@render children()} +
+
+
+
diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Form.svelte b/apps/frontend/src/lib/components/common/Simulator/Form.svelte similarity index 100% rename from apps/frontend/src/lib/components/pages/simulateur-acquisition/Form.svelte rename to apps/frontend/src/lib/components/common/Simulator/Form.svelte diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Wrapper.svelte b/apps/frontend/src/lib/components/common/Simulator/Wrapper.svelte similarity index 100% rename from apps/frontend/src/lib/components/pages/simulateur-acquisition/Wrapper.svelte rename to apps/frontend/src/lib/components/common/Simulator/Wrapper.svelte diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Description.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Description.svelte deleted file mode 100644 index 42c1b2e9f..000000000 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Description.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - -
-
-

Simulateur d'acquisition

-
- - {@render children()} -
-
-
diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step1.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step1.svelte index 3598d7fe0..23341656c 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step1.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step1.svelte @@ -17,17 +17,19 @@ import Input from '$components/common/Input.svelte'; import Actions from '$components/pages/simulateur-acquisition/Actions.svelte'; - import Form from '$components/pages/simulateur-acquisition/Form.svelte'; + import Form from '$components/common/Simulator/Form.svelte'; import Autocomplete from '$components/common/Autocomplete.svelte'; import Radio from '$components/common/Radio.svelte'; import RadioFieldset from '$components/common/RadioFieldset.svelte'; import Action from '$components/pages/simulateur-acquisition/Action.svelte'; - import Wrapper from '$components/pages/simulateur-acquisition/Wrapper.svelte'; - import Description from '$components/pages/simulateur-acquisition/Description.svelte'; + import Wrapper from '$components/common/Simulator/Wrapper.svelte'; + import Description from '$components/common/Simulator/Description.svelte'; import acquisitionSimulatorManager from '$lib/managers/acquisition-simulator.svelte'; let { + currentStep, + steps, housingPrice, autocompleteValue, surface, @@ -99,7 +101,12 @@ - +

Indiquez les principales caractéristiques du logement que vous souhaitez acquérir. Ces informations permettront d'estimer les frais associés à diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step2.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step2.svelte index 4916c64d0..d7c7e82a0 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step2.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step2.svelte @@ -4,16 +4,18 @@ import type { FormFieldError } from '$lib/utils/definitions'; import { formatFormErrors } from '$lib/utils/helpers'; - import Form from '$components/pages/simulateur-acquisition/Form.svelte'; + import Form from '$components/common/Simulator/Form.svelte'; import Input from '$components/common/Input.svelte'; import Actions from '$components/pages/simulateur-acquisition/Actions.svelte'; import Action from '$components/pages/simulateur-acquisition/Action.svelte'; - import Wrapper from '$components/pages/simulateur-acquisition/Wrapper.svelte'; - import Description from '$components/pages/simulateur-acquisition/Description.svelte'; + import Wrapper from '$components/common/Simulator/Wrapper.svelte'; + import Description from '$components/common/Simulator/Description.svelte'; import acquisitionSimulatorManager from '$lib/managers/acquisition-simulator.svelte'; let { + currentStep, + steps, ownContribution, nextStep, previousStep, @@ -55,7 +57,12 @@ - +

Renseignez le montant de votre apport personnel. Cette somme vous permettra de financer une partie du coût de votre projet et de réduire le diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step3.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step3.svelte index b69422dc6..b38206462 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step3.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step3.svelte @@ -4,9 +4,9 @@ import type { FormFieldError } from '$lib/utils/definitions'; import { formatFormErrors } from '$lib/utils/helpers'; - import Wrapper from '$components/pages/simulateur-acquisition/Wrapper.svelte'; - import Description from '$components/pages/simulateur-acquisition/Description.svelte'; - import Form from '$components/pages/simulateur-acquisition/Form.svelte'; + import Wrapper from '$components/common/Simulator/Wrapper.svelte'; + import Description from '$components/common/Simulator/Description.svelte'; + import Form from '$components/common/Simulator/Form.svelte'; import Input from '$components/common/Input.svelte'; import Actions from '$components/pages/simulateur-acquisition/Actions.svelte'; import Action from '$components/pages/simulateur-acquisition/Action.svelte'; @@ -28,6 +28,8 @@ }); let { + currentStep, + steps, housingType, notaryFees, oneTimeExpenses, @@ -62,7 +64,12 @@ - +

Précisez les différents frais liés à l'achat du logement : frais de notaire ou autres frais ponctuels. Si certains montants sont inconnus, le diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step4.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step4.svelte index 686a9c3dd..df753588d 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step4.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step4.svelte @@ -1,7 +1,7 @@ - +

Voici un premier récapitulatif de votre apport, du prix du logement et de tous les frais annexes. Cette synthèse vous aide à visualiser le budget diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step5.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step5.svelte index 942abc41f..a1b491a72 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step5.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step5.svelte @@ -5,9 +5,9 @@ import { formatFormErrors } from '$lib/utils/helpers'; import { PretLisse, type Logement, type Zone } from '$lib/utils/lissage-ptz'; - import Wrapper from '$components/pages/simulateur-acquisition/Wrapper.svelte'; - import Description from '$components/pages/simulateur-acquisition/Description.svelte'; - import Form from '$components/pages/simulateur-acquisition/Form.svelte'; + import Wrapper from '$components/common/Simulator/Wrapper.svelte'; + import Description from '$components/common/Simulator/Description.svelte'; + import Form from '$components/common/Simulator/Form.svelte'; import Input from '$components/common/Input.svelte'; import Actions from '$components/pages/simulateur-acquisition/Actions.svelte'; import Action from '$components/pages/simulateur-acquisition/Action.svelte'; @@ -19,6 +19,8 @@ import acquisitionSimulatorManager from '$lib/managers/acquisition-simulator.svelte'; let { + currentStep, + steps, totalCost, interestRate, loanDuration, @@ -108,7 +110,12 @@ - +

Renseignez les différentes informations relatives à un prêt immobilier pour simuler votre projet d'acquisition. Selon les critères que vous aurez diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step6.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step6.svelte index 9b566b8dd..5ff4f81bd 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step6.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step6.svelte @@ -1,17 +1,22 @@ - +

Le lissage de prêt permet de structurer vos emprunts (prêt immobilier classique et/ou prêt à taux zéro) avec des mensualités constantes sur diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step7.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step7.svelte index bfa62b6d8..7fe40f5a5 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step7.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step7.svelte @@ -5,9 +5,9 @@ import { formatFormErrors } from '$lib/utils/helpers'; import { formatEuro } from '$lib/utils/formatters'; - import Wrapper from '$components/pages/simulateur-acquisition/Wrapper.svelte'; - import Description from '$components/pages/simulateur-acquisition/Description.svelte'; - import Form from '$components/pages/simulateur-acquisition/Form.svelte'; + import Wrapper from '$components/common/Simulator/Wrapper.svelte'; + import Description from '$components/common/Simulator/Description.svelte'; + import Form from '$components/common/Simulator/Form.svelte'; import Actions from '$components/pages/simulateur-acquisition/Actions.svelte'; import Action from '$components/pages/simulateur-acquisition/Action.svelte'; import Input from '$components/common/Input.svelte'; @@ -17,6 +17,8 @@ import acquisitionSimulatorManager from '$lib/managers/acquisition-simulator.svelte'; let { + currentStep, + steps, brsFees, yearlyPropertyTax, yearlyHouseingInsurance, @@ -101,7 +103,12 @@ - +

En renseignant cette section, le simulateur vous donne une estimation claire du coût mensuel et annuel à prévoir une fois propriétaire. C'est diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step8.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step8.svelte index 565bf56bd..eaf5ba54d 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step8.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step8.svelte @@ -1,16 +1,16 @@ - +

Voici le résumé de la simulation de votre projet d'acquisition en bail réel solidaire (BRS). Les montants et calculs présentés ci-dessous sont diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/SimulationCta.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/SimulationCta.svelte index a510057d6..98b6e14f3 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/SimulationCta.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/SimulationCta.svelte @@ -26,7 +26,7 @@ ICI].

Je lance ma simulation diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step1.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step1.svelte new file mode 100644 index 000000000..7bb8a6a37 --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step1.svelte @@ -0,0 +1,66 @@ + + + + +

Bonjour

+
+
{ + console.log('submit'); + }}> +

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+

yo

+
+
diff --git a/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte b/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte index cb4710ea2..0eb80c041 100644 --- a/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte +++ b/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte @@ -3,11 +3,9 @@ import GradientBackgroundWrapper from '$components/common/GradientBackgroundWrapper.svelte'; import Hero from '$components/pages/simulateur-eligibilite/Hero.svelte'; + import SimulationCta from '$components/pages/simulateur-eligibilite/SimulationCta.svelte'; import EligibilityTresholds from '$components/common/EligibilityTresholds.svelte'; import PageCta from '$components/common/PageCta.svelte'; - import SimulationCta from '$components/pages/simulateur-eligibilite/SimulationCta.svelte'; - - const { data }: PageProps = $props(); diff --git a/apps/frontend/src/routes/simulateur-eligibilite/steps/+page.svelte b/apps/frontend/src/routes/simulateur-eligibilite/steps/+page.svelte new file mode 100644 index 000000000..dc3dd5b66 --- /dev/null +++ b/apps/frontend/src/routes/simulateur-eligibilite/steps/+page.svelte @@ -0,0 +1,5 @@ + + + From 32641ba75afccd7a22766ff2bbeb2a782d2767bf Mon Sep 17 00:00:00 2001 From: fuuuzz Date: Thu, 29 Jan 2026 16:46:48 +0100 Subject: [PATCH 06/56] feat initial steps on new simulator --- apps/frontend/package.json | 1 + .../Simulator}/Action.svelte | 0 .../Simulator}/Actions.svelte | 0 .../common/Simulator/Wrapper.svelte | 2 +- .../pages/simulateur-acquisition/Step1.svelte | 4 +- .../pages/simulateur-acquisition/Step2.svelte | 4 +- .../pages/simulateur-acquisition/Step3.svelte | 4 +- .../pages/simulateur-acquisition/Step4.svelte | 4 +- .../pages/simulateur-acquisition/Step5.svelte | 4 +- .../pages/simulateur-acquisition/Step6.svelte | 4 +- .../pages/simulateur-acquisition/Step7.svelte | 4 +- .../pages/simulateur-acquisition/Step8.svelte | 4 +- .../simulateur-eligibilite/steps/Step1.svelte | 86 +++++++------------ .../simulateur-eligibilite/steps/Step2.svelte | 51 +++++++++++ .../simulateur-eligibilite/steps/Step3.svelte | 36 ++++++++ .../managers/acquisition-simulator.svelte.ts | 2 +- .../managers/eligibility-simulator.svelte.ts | 65 ++++++++++++++ .../simulateur-eligibilite/steps/+page.svelte | 21 ++++- .../e2e/simulateur-eligibilite/+page.spec.ts | 20 +++++ .../steps/+page.spec.ts | 41 +++++++++ 20 files changed, 283 insertions(+), 74 deletions(-) rename apps/frontend/src/lib/components/{pages/simulateur-acquisition => common/Simulator}/Action.svelte (100%) rename apps/frontend/src/lib/components/{pages/simulateur-acquisition => common/Simulator}/Actions.svelte (100%) create mode 100644 apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step2.svelte create mode 100644 apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step3.svelte create mode 100644 apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts create mode 100644 apps/frontend/tests/e2e/simulateur-eligibilite/+page.spec.ts create mode 100644 apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 3988c6c8d..378d21344 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -17,6 +17,7 @@ "lint": "prettier --check . && eslint .", "format": "prettier --write .", "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", "test:unit": "vitest", "lighthouse-mobile": "lhci autorun --config=./.lighthouse-mobile.json", "lighthouse-desktop": "lhci autorun --config=./.lighthouse-desktop.json", diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Action.svelte b/apps/frontend/src/lib/components/common/Simulator/Action.svelte similarity index 100% rename from apps/frontend/src/lib/components/pages/simulateur-acquisition/Action.svelte rename to apps/frontend/src/lib/components/common/Simulator/Action.svelte diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Actions.svelte b/apps/frontend/src/lib/components/common/Simulator/Actions.svelte similarity index 100% rename from apps/frontend/src/lib/components/pages/simulateur-acquisition/Actions.svelte rename to apps/frontend/src/lib/components/common/Simulator/Actions.svelte diff --git a/apps/frontend/src/lib/components/common/Simulator/Wrapper.svelte b/apps/frontend/src/lib/components/common/Simulator/Wrapper.svelte index 832ae09ad..172e403d8 100644 --- a/apps/frontend/src/lib/components/common/Simulator/Wrapper.svelte +++ b/apps/frontend/src/lib/components/common/Simulator/Wrapper.svelte @@ -9,7 +9,7 @@
{@render children()}
diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step1.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step1.svelte index 23341656c..c63f82a01 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step1.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step1.svelte @@ -16,12 +16,12 @@ } from '$lib/utils/helpers'; import Input from '$components/common/Input.svelte'; - import Actions from '$components/pages/simulateur-acquisition/Actions.svelte'; + import Actions from '$components/common/Simulator/Actions.svelte'; import Form from '$components/common/Simulator/Form.svelte'; import Autocomplete from '$components/common/Autocomplete.svelte'; import Radio from '$components/common/Radio.svelte'; import RadioFieldset from '$components/common/RadioFieldset.svelte'; - import Action from '$components/pages/simulateur-acquisition/Action.svelte'; + import Action from '$components/common/Simulator/Action.svelte'; import Wrapper from '$components/common/Simulator/Wrapper.svelte'; import Description from '$components/common/Simulator/Description.svelte'; diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step2.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step2.svelte index d7c7e82a0..95a87c31c 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step2.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step2.svelte @@ -6,8 +6,8 @@ import Form from '$components/common/Simulator/Form.svelte'; import Input from '$components/common/Input.svelte'; - import Actions from '$components/pages/simulateur-acquisition/Actions.svelte'; - import Action from '$components/pages/simulateur-acquisition/Action.svelte'; + import Actions from '$components/common/Simulator/Actions.svelte'; + import Action from '$components/common/Simulator/Action.svelte'; import Wrapper from '$components/common/Simulator/Wrapper.svelte'; import Description from '$components/common/Simulator/Description.svelte'; diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step3.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step3.svelte index b38206462..ab4952dda 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step3.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step3.svelte @@ -8,8 +8,8 @@ import Description from '$components/common/Simulator/Description.svelte'; import Form from '$components/common/Simulator/Form.svelte'; import Input from '$components/common/Input.svelte'; - import Actions from '$components/pages/simulateur-acquisition/Actions.svelte'; - import Action from '$components/pages/simulateur-acquisition/Action.svelte'; + import Actions from '$components/common/Simulator/Actions.svelte'; + import Action from '$components/common/Simulator/Action.svelte'; import { formatEuro } from '$lib/utils/formatters'; import acquisitionSimulatorManager from '$lib/managers/acquisition-simulator.svelte'; diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step4.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step4.svelte index df753588d..3256048b4 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step4.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step4.svelte @@ -2,8 +2,8 @@ import Wrapper from '$components/common/Simulator/Wrapper.svelte'; import Description from '$components/common/Simulator/Description.svelte'; import Form from '$components/common/Simulator/Form.svelte'; - import Actions from '$components/pages/simulateur-acquisition/Actions.svelte'; - import Action from '$components/pages/simulateur-acquisition/Action.svelte'; + import Actions from '$components/common/Simulator/Actions.svelte'; + import Action from '$components/common/Simulator/Action.svelte'; import OperationSynthesis from './OperationSynthesis.svelte'; import Highlight from '$components/common/Highlight.svelte'; diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step5.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step5.svelte index a1b491a72..c8b54956b 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step5.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step5.svelte @@ -9,8 +9,8 @@ import Description from '$components/common/Simulator/Description.svelte'; import Form from '$components/common/Simulator/Form.svelte'; import Input from '$components/common/Input.svelte'; - import Actions from '$components/pages/simulateur-acquisition/Actions.svelte'; - import Action from '$components/pages/simulateur-acquisition/Action.svelte'; + import Actions from '$components/common/Simulator/Actions.svelte'; + import Action from '$components/common/Simulator/Action.svelte'; import { formatEuro } from '$lib/utils/formatters'; import Badge from '$components/common/Badge.svelte'; import RadioFieldset from '$components/common/RadioFieldset.svelte'; diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step6.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step6.svelte index 5ff4f81bd..059870a1d 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step6.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step6.svelte @@ -4,8 +4,8 @@ import Wrapper from '$components/common/Simulator/Wrapper.svelte'; import Description from '$components/common/Simulator/Description.svelte'; import Form from '$components/common/Simulator/Form.svelte'; - import Actions from '$components/pages/simulateur-acquisition/Actions.svelte'; - import Action from '$components/pages/simulateur-acquisition/Action.svelte'; + import Actions from '$components/common/Simulator/Actions.svelte'; + import Action from '$components/common/Simulator/Action.svelte'; import LissageSynthesis from './LissageSynthesis.svelte'; import Highlight from '$components/common/Highlight.svelte'; diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step7.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step7.svelte index 7fe40f5a5..fa175b29c 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step7.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step7.svelte @@ -8,8 +8,8 @@ import Wrapper from '$components/common/Simulator/Wrapper.svelte'; import Description from '$components/common/Simulator/Description.svelte'; import Form from '$components/common/Simulator/Form.svelte'; - import Actions from '$components/pages/simulateur-acquisition/Actions.svelte'; - import Action from '$components/pages/simulateur-acquisition/Action.svelte'; + import Actions from '$components/common/Simulator/Actions.svelte'; + import Action from '$components/common/Simulator/Action.svelte'; import Input from '$components/common/Input.svelte'; import Radio from '$components/common/Radio.svelte'; import RadioFieldset from '$components/common/RadioFieldset.svelte'; diff --git a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step8.svelte b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step8.svelte index eaf5ba54d..00a471a5e 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step8.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-acquisition/Step8.svelte @@ -4,8 +4,8 @@ import Wrapper from '$components/common/Simulator/Wrapper.svelte'; import Description from '$components/common/Simulator/Description.svelte'; import Form from '$components/common/Simulator/Form.svelte'; - import Actions from '$components/pages/simulateur-acquisition/Actions.svelte'; - import Action from '$components/pages/simulateur-acquisition/Action.svelte'; + import Actions from '$components/common/Simulator/Actions.svelte'; + import Action from '$components/common/Simulator/Action.svelte'; import GlobalSynthesis from '$components/pages/simulateur-acquisition/GlobalSynthesis.svelte'; import acquisitionSimulatorManager from '$lib/managers/acquisition-simulator.svelte'; diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step1.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step1.svelte index 7bb8a6a37..e38c5bc52 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step1.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step1.svelte @@ -1,66 +1,42 @@ - -

Bonjour

+ stepTitle={`${currentStep.step}. ${currentStep.title}`} + currentStep={currentStep.step} + stepCount={steps.length}> +

Step 1 description

-
{ - console.log('submit'); - }}> -

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

-

yo

+ +

Step 1 content

+ + {#if nextStep} + + + + {/if}
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step2.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step2.svelte new file mode 100644 index 000000000..771474ca6 --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step2.svelte @@ -0,0 +1,51 @@ + + + + +

Step 2 description

+
+
+

Step 2 content

+ + + + + +
+
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step3.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step3.svelte new file mode 100644 index 000000000..2a16d65f0 --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step3.svelte @@ -0,0 +1,36 @@ + + + + +

Step 3 description

+
+
{ + console.log('submit'); + }}> +

Step 3 content

+ + + + +
+
diff --git a/apps/frontend/src/lib/managers/acquisition-simulator.svelte.ts b/apps/frontend/src/lib/managers/acquisition-simulator.svelte.ts index 25cc52ff2..33c3894c5 100644 --- a/apps/frontend/src/lib/managers/acquisition-simulator.svelte.ts +++ b/apps/frontend/src/lib/managers/acquisition-simulator.svelte.ts @@ -202,7 +202,7 @@ class AcquisitionSimulator { private resetScroll = () => { if (browser) { - document.getElementById('simulateur-acquisition')?.scrollIntoView({ + document.getElementById('simulateur')?.scrollIntoView({ behavior: 'smooth', block: 'start', }); diff --git a/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts b/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts new file mode 100644 index 000000000..68bb12680 --- /dev/null +++ b/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts @@ -0,0 +1,65 @@ +import { browser } from '$app/environment'; + +type Step = { + title: string; + step: number; +}; + +class EligibilitySimulator { + public steps: Step[] = [ + { + title: 'Définir mon éligibilité', + step: 1, + }, + { + title: "Mon résultat d'éligibilité", + step: 2, + }, + { + title: 'Ma recherche', + step: 3, + }, + ]; + public currentStep: Step = $state(this.steps[0]); + public previousStep: Step | null = $derived.by(() => { + if (this.currentStep.step < 2) { + return null; + } else { + return this.steps[this.currentStep.step - 2]; + } + }); + public nextStep: Step | null = $derived.by(() => { + if (this.currentStep.step >= this.steps.length) { + return null; + } else { + return this.steps[this.currentStep.step]; + } + }); + public loading: boolean = $state(false); + + public goToPreviousStep = () => { + if (this.previousStep) { + this.currentStep = this.previousStep; + this.resetScroll(); + } + }; + + public goToNextStep = () => { + if (this.nextStep) { + this.currentStep = this.nextStep; + this.resetScroll(); + } + }; + + private resetScroll = () => { + if (browser) { + document.getElementById('simulateur')?.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + } + }; +} + +const eligibilitySimulatorManager = new EligibilitySimulator(); +export default eligibilitySimulatorManager; diff --git a/apps/frontend/src/routes/simulateur-eligibilite/steps/+page.svelte b/apps/frontend/src/routes/simulateur-eligibilite/steps/+page.svelte index dc3dd5b66..2a03cea95 100644 --- a/apps/frontend/src/routes/simulateur-eligibilite/steps/+page.svelte +++ b/apps/frontend/src/routes/simulateur-eligibilite/steps/+page.svelte @@ -1,5 +1,24 @@ - + + Simulateur d'éligibilité au Bail Réel Solidaire - BoRiS + + + +{#if currentStep.step === 1} + +{:else if currentStep.step === 2} + +{:else if currentStep.step === 3} + +{/if} diff --git a/apps/frontend/tests/e2e/simulateur-eligibilite/+page.spec.ts b/apps/frontend/tests/e2e/simulateur-eligibilite/+page.spec.ts new file mode 100644 index 000000000..81f5afadb --- /dev/null +++ b/apps/frontend/tests/e2e/simulateur-eligibilite/+page.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; + +test.describe('navigation', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/simulateur-eligibilite'); + }); + + test('has title', async ({ page }) => { + await expect(page).toHaveTitle( + /Suis-je éligible au Bail Réel Solidaire - BRS - BoRiS/, + ); + }); + + test('has link to simulator steps page', async ({ page }) => { + const cta = page.getByRole('link', { name: 'Je lance ma simulation' }); + + await expect(cta).toHaveAttribute('href', '/simulateur-eligibilite/steps'); + await expect(cta).toBeVisible(); + }); +}); diff --git a/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts b/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts new file mode 100644 index 000000000..4b2e64068 --- /dev/null +++ b/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test'; + +test.describe('navigation', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/simulateur-eligibilite/steps'); + }); + + test('should have title', async ({ page }) => { + await expect(page).toHaveTitle( + /Simulateur d'éligibilité au Bail Réel Solidaire - BoRiS/, + ); + }); + + test('should have a valid h1', async ({ page }) => { + const h1 = page.getByRole('heading', { level: 1 }); + + expect(h1).toHaveText("Simulateur d'éligibilité"); + }); + + test('simulator completion', async ({ page }) => { + const simulatorWrapper = page.getByTestId('simulator-wrapper'); + + // Step 1 + let stepTitle = simulatorWrapper.getByRole('heading', { level: 2 }); + expect(stepTitle).toHaveText('1. Définir mon éligibilité Étape 1 sur 3'); + let nextStepButton = simulatorWrapper.getByRole('button', { + name: /Étape suivante/i, + }); + await nextStepButton.click(); + + stepTitle = simulatorWrapper.getByRole('heading', { level: 2 }); + expect(stepTitle).toHaveText("2. Mon résultat d'éligibilité Étape 2 sur 3"); + nextStepButton = simulatorWrapper.getByRole('button', { + name: /Étape suivante/i, + }); + await nextStepButton.click(); + + stepTitle = simulatorWrapper.getByRole('heading', { level: 2 }); + expect(stepTitle).toHaveText('3. Ma recherche Étape 3 sur 3'); + }); +}); From eb8d6ddd56120e05b2e694ebc8b4e387faf4b69c Mon Sep 17 00:00:00 2001 From: fuuuzz Date: Mon, 2 Feb 2026 18:01:47 +0100 Subject: [PATCH 07/56] feat: starting flow --- .../src/lib/components/common/Select.svelte | 64 +++-- .../EligibilityDefinition.svelte | 39 ++++ .../HouseholdComposition.svelte | 220 ++++++++++++++++++ .../simulateur-eligibilite/steps/Step1.svelte | 42 ---- .../managers/eligibility-simulator.svelte.ts | 3 + apps/frontend/src/lib/utils/constants.ts | 1 - apps/frontend/src/routes/+layout.svelte | 1 - .../simulateur-eligibilite/+page.svelte | 2 - .../simulateur-eligibilite/steps/+page.svelte | 4 +- .../steps/+page.spec.ts | 45 ++-- 10 files changed, 340 insertions(+), 81 deletions(-) create mode 100644 apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/EligibilityDefinition.svelte create mode 100644 apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/HouseholdComposition.svelte delete mode 100644 apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step1.svelte diff --git a/apps/frontend/src/lib/components/common/Select.svelte b/apps/frontend/src/lib/components/common/Select.svelte index 686bb350f..0bbae4d81 100644 --- a/apps/frontend/src/lib/components/common/Select.svelte +++ b/apps/frontend/src/lib/components/common/Select.svelte @@ -9,6 +9,9 @@ label?: string; options: { value: unknown; label: string; selected: boolean }[]; disabled?: boolean; + error?: string; + required?: boolean; + errorDataTestId?: string; onChange?: (event: Event) => void; }; @@ -18,26 +21,47 @@ options, disabled, onChange, + error, + required, + errorDataTestId = 'select-error-message', }: Props = $props(); - - +
+ + + {#if error} +
+

+ {error} +

+
+ {/if} +
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/EligibilityDefinition.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/EligibilityDefinition.svelte new file mode 100644 index 000000000..816ea381a --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/EligibilityDefinition.svelte @@ -0,0 +1,39 @@ + + + + +

+ La première étape du simulateur BoRiS + consiste à définir votre éligibilité au dispositif. +

+

+ Pour cela, nous allons vous demander de renseigner les informatins + suivantes : +

+
    +
  1. La composition de votre foyer
  2. +
  3. Vos revenus fiscaux
  4. +
  5. Votre situation immobilière
  6. +
+ + + +
+ + +
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/HouseholdComposition.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/HouseholdComposition.svelte new file mode 100644 index 000000000..1eeb5b5b2 --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/HouseholdComposition.svelte @@ -0,0 +1,220 @@ + + +
+
+
+

Composition de mon foyer

+
+ +
+ { + const { value } = e.target as HTMLSelectElement; + + console.log(value); + console.log(typeof value); + + eligibilitySimulatorManager.hasDisability = + value === '' ? undefined : value === 'true' ? true : false; + }} + error={errors.hasDisability} + errorDataTestId="select-has-disability-error-message" /> +
+ {/if} + + {#if selectedHouseholdSize === -1} +
+ { + const value = (e.target as HTMLInputElement).value; + + if (value) { + const numericValue = value.replace(/\D/g, '').slice(0, 9); + (e.target as HTMLInputElement).value = numericValue; + inputHouseholdSize = Number(numericValue); + } + }} + error={errors.inputHouseholdSize} /> +
+ {/if} +
+ + + + +
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step1.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step1.svelte deleted file mode 100644 index e38c5bc52..000000000 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step1.svelte +++ /dev/null @@ -1,42 +0,0 @@ - - - - -

Step 1 description

-
-
-

Step 1 content

- - {#if nextStep} - - - - {/if} -
-
diff --git a/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts b/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts index 68bb12680..fbb208746 100644 --- a/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts +++ b/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts @@ -37,6 +37,9 @@ class EligibilitySimulator { }); public loading: boolean = $state(false); + public householdSize: number | undefined = $state(0); + public hasDisability: boolean | undefined = $state(undefined); + public goToPreviousStep = () => { if (this.previousStep) { this.currentStep = this.previousStep; diff --git a/apps/frontend/src/lib/utils/constants.ts b/apps/frontend/src/lib/utils/constants.ts index 0b1c9e78b..9412bf1f5 100644 --- a/apps/frontend/src/lib/utils/constants.ts +++ b/apps/frontend/src/lib/utils/constants.ts @@ -1,5 +1,4 @@ import type { EligibilityData, MapBounds } from '$lib/utils/definitions'; -import { formatEuro } from './formatters'; export const eligibilityData: EligibilityData[] = [ { diff --git a/apps/frontend/src/routes/+layout.svelte b/apps/frontend/src/routes/+layout.svelte index 80f39d28a..33cdb3361 100644 --- a/apps/frontend/src/routes/+layout.svelte +++ b/apps/frontend/src/routes/+layout.svelte @@ -23,7 +23,6 @@ import Consent from '$components/common/Consent.svelte'; import { blockSearchEngineIndexing } from '$lib/utils/helpers'; import { page } from '$app/state'; - import Notice from '$components/common/Notice.svelte'; type Props = { children: Snippet }; const { children }: Props = $props(); diff --git a/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte b/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte index 0eb80c041..9ecec12f4 100644 --- a/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte +++ b/apps/frontend/src/routes/simulateur-eligibilite/+page.svelte @@ -1,6 +1,4 @@ @@ -35,5 +39,11 @@
- + {#if currentPhase.phase === 1} + + {:else if currentPhase.phase === 2} + + {:else if currentPhase.phase === 3} + + {/if}
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/FiscalRevenues.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/FiscalRevenues.svelte new file mode 100644 index 000000000..ff86a2482 --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/FiscalRevenues.svelte @@ -0,0 +1,41 @@ + + +
+
+
+

Revenus fiscaux

+
+
+ + + + + +
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/HouseholdComposition.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/HouseholdComposition.svelte index 1eeb5b5b2..41e7c7d90 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/HouseholdComposition.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/HouseholdComposition.svelte @@ -11,9 +11,8 @@ import { formatFormErrors } from '$lib/utils/helpers'; import Input from '$components/common/Input.svelte'; - const { householdSize, hasDisability, loading } = $derived( - eligibilitySimulatorManager, - ); + const { householdSize, hasDisability, nextPhase, goToNextPhase, loading } = + $derived(eligibilitySimulatorManager); let errors: FormFieldError = $state({}); let selectedHouseholdSize: number | undefined = $derived.by(() => { @@ -34,6 +33,16 @@ } }); + let singlePersonInHousehold: boolean = $derived(selectedHouseholdSize === 1); + let twoToSixPersonsInHousehold: boolean = $derived( + selectedHouseholdSize !== undefined && + selectedHouseholdSize >= 2 && + selectedHouseholdSize <= 6, + ); + let moreThanSixPersonsInHousehold: boolean = $derived( + selectedHouseholdSize === -1, + ); + let FormData = z .object({ selectedHouseholdSize: z.number({ @@ -73,25 +82,26 @@ e.preventDefault(); // TODO: Define type here with DTOs (same as in step1 from acquisition simulator) - const payload = { - selectedHouseholdSize, - inputHouseholdSize, - hasDisability, - }; - - try { - FormData.parse(payload); - errors = {}; - } catch (e) { - errors = formatFormErrors((e as ZodError).issues); - } + // const payload = { + // selectedHouseholdSize, + // inputHouseholdSize, + // hasDisability, + // }; + + // try { + // FormData.parse(payload); + // errors = {}; + goToNextPhase(); + // } catch (e) { + // errors = formatFormErrors((e as ZodError).issues); + // } };
-

Composition de mon foyer

+

Composition de mon foyer

@@ -149,8 +159,7 @@ errorDataTestId="select-household-size-error-message" />
- - {#if selectedHouseholdSize === 1} + {#if singlePersonInHousehold}
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/PropertySituation.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/PropertySituation.svelte new file mode 100644 index 000000000..4c2ed8fc8 --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/PropertySituation.svelte @@ -0,0 +1,36 @@ + + + +
+
+

Situation immobilière

+
+
+ + + + + + diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/EligibilityResult.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/EligibilityResult.svelte new file mode 100644 index 000000000..23d524aa3 --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/EligibilityResult.svelte @@ -0,0 +1,40 @@ + + + + +

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quam veritatis, + quidem perspiciatis aspernatur id tempora, esse quis saepe necessitatibus + similique animi praesentium? Distinctio sunt rerum quidem. Hic error natus + facere? +

+

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Fuga accusantium + a error praesentium veritatis quisquam officia iste sit eveniet dolorem + deserunt ut, laborum dolores. Nesciunt nisi soluta minima provident + voluptatibus. +

+
+ + {#if currentPhase.phase === 1} + + {:else if currentPhase.phase === 2} + + {/if} +
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/ResultDetails.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/ResultDetails.svelte new file mode 100644 index 000000000..547ad559c --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/ResultDetails.svelte @@ -0,0 +1,36 @@ + + +
+
+
+

Détails du résultat

+
+
+ + + + + +
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/UserDetails.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/UserDetails.svelte new file mode 100644 index 000000000..cd265e829 --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/UserDetails.svelte @@ -0,0 +1,36 @@ + + +
+
+
+

Informations personnelles

+
+
+ + + + + +
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/AdditionalInformations.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/AdditionalInformations.svelte new file mode 100644 index 000000000..1e89a8e10 --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/AdditionalInformations.svelte @@ -0,0 +1,36 @@ + + +
+
+
+

Informations additionnelles

+
+
+ + + + + +
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/FinancialInformations.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/FinancialInformations.svelte new file mode 100644 index 000000000..b2f95649d --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/FinancialInformations.svelte @@ -0,0 +1,41 @@ + + +
+
+
+

Informations financières

+
+
+ + + + + +
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/HousingInformations.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/HousingInformations.svelte new file mode 100644 index 000000000..1fafc85ba --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/HousingInformations.svelte @@ -0,0 +1,36 @@ + + +
+
+
+

Informations sur le logement

+
+
+ + + + + +
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/SearchInformations.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/SearchInformations.svelte new file mode 100644 index 000000000..e085955f1 --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/SearchInformations.svelte @@ -0,0 +1,44 @@ + + + + +

+ Lorem ipsum dolor, sit amet consectetur adipisicing elit. Porro, cumque, + quae, molestias numquam vero reiciendis nihil voluptates repellendus + itaque eos repellat sequi? Asperiores ea modi quis quia, quam beatae + architecto. +

+

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempora nam + obcaecati est recusandae expedita in, officia vel fuga ab perferendis + accusamus explicabo temporibus. Alias molestiae, aliquid hic incidunt rem + voluptatibus! +

+
+ + {#if currentPhase.phase === 1} + + {:else if currentPhase.phase === 2} + + {:else if currentPhase.phase === 3} + + {/if} +
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step2.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step2.svelte deleted file mode 100644 index 771474ca6..000000000 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step2.svelte +++ /dev/null @@ -1,51 +0,0 @@ - - - - -

Step 2 description

-
-
-

Step 2 content

- - - - - -
-
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step3.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step3.svelte deleted file mode 100644 index 2a16d65f0..000000000 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Step3.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - - - -

Step 3 description

-
-
{ - console.log('submit'); - }}> -

Step 3 content

- - - - -
-
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Synthesis/Synthesis.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Synthesis/Synthesis.svelte new file mode 100644 index 000000000..bf0aa0567 --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Synthesis/Synthesis.svelte @@ -0,0 +1,35 @@ + + + + +

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ducimus ullam + corrupti dolores. Placeat autem obcaecati consectetur illum aperiam dicta + eaque. Et tempore atque totam culpa itaque omnis ullam sequi vel! +

+

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore obcaecati + corporis numquam sint quia enim reiciendis porro! Modi omnis fuga + similique totam harum! Voluptates eaque beatae consequatur nemo, libero + asperiores. +

+
+ + {#if currentPhase.phase === 1} + + {/if} +
diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Synthesis/SynthesisContent.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Synthesis/SynthesisContent.svelte new file mode 100644 index 000000000..a9593c163 --- /dev/null +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/Synthesis/SynthesisContent.svelte @@ -0,0 +1,30 @@ + + +
+
+
+

Contenu de la synthèse

+
+
+ + + + +
diff --git a/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts b/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts index fbb208746..d5d3236c5 100644 --- a/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts +++ b/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts @@ -1,8 +1,14 @@ import { browser } from '$app/environment'; +type Phase = { + title: string; + phase: number; +}; + type Step = { title: string; step: number; + phases: Phase[]; }; class EligibilitySimulator { @@ -10,16 +16,65 @@ class EligibilitySimulator { { title: 'Définir mon éligibilité', step: 1, + phases: [ + { + title: 'Composition de mon foyer', + phase: 1, + }, + { + title: 'Revenus fiscaux', + phase: 2, + }, + { + title: 'Situation immobilière', + phase: 3, + }, + ], }, { title: "Mon résultat d'éligibilité", step: 2, + phases: [ + { + title: 'Détail du resultat', + phase: 1, + }, + { + title: 'Informations personnelles', + phase: 2, + }, + ], }, { title: 'Ma recherche', step: 3, + phases: [ + { + title: 'Informations sur le logement', + phase: 1, + }, + { + title: 'Informations financières', + phase: 2, + }, + { + title: 'Informations additionnelles', + phase: 3, + }, + ], + }, + { + title: 'Synthése', + step: 4, + phases: [ + { + title: 'Synthèse', + phase: 1, + }, + ], }, ]; + public currentStep: Step = $state(this.steps[0]); public previousStep: Step | null = $derived.by(() => { if (this.currentStep.step < 2) { @@ -35,22 +90,53 @@ class EligibilitySimulator { return this.steps[this.currentStep.step]; } }); - public loading: boolean = $state(false); + public currentPhase: Phase = $state(this.steps[0].phases[0]); + public previousPhase: Phase | null = $derived.by(() => { + if (this.currentPhase.phase > 1) { + return this.currentStep.phases[this.currentPhase.phase - 2]; + } else if (this.previousStep) { + return this.previousStep.phases[this.previousStep.phases.length - 1]; + } else { + return null; + } + }); + public nextPhase: Phase | null = $derived.by(() => { + if (this.currentPhase.phase < this.currentStep.phases.length) { + return this.currentStep.phases[this.currentPhase.phase]; + } else if (this.nextStep) { + return this.nextStep?.phases[0]; + } else { + return null; + } + }); + + public loading: boolean = $state(false); public householdSize: number | undefined = $state(0); public hasDisability: boolean | undefined = $state(undefined); - public goToPreviousStep = () => { - if (this.previousStep) { - this.currentStep = this.previousStep; - this.resetScroll(); + public goToPreviousPhase = () => { + if (this.previousPhase) { + const shouldGoToPreviousStep = this.currentPhase.phase === 1; + + this.currentPhase = this.previousPhase; + + if (shouldGoToPreviousStep && this.previousStep) { + this.currentStep = this.previousStep; + } } }; - public goToNextStep = () => { - if (this.nextStep) { - this.currentStep = this.nextStep; - this.resetScroll(); + public goToNextPhase = () => { + if (this.nextPhase) { + const shouldGoToNextStep = + this.currentPhase.phase === this.currentStep.phases.length; + + this.currentPhase = this.nextPhase; + + if (shouldGoToNextStep && this.nextStep) { + this.currentStep = this.nextStep; + } } }; diff --git a/apps/frontend/src/routes/simulateur-eligibilite/steps/+page.svelte b/apps/frontend/src/routes/simulateur-eligibilite/steps/+page.svelte index 1f586a243..00b0093f6 100644 --- a/apps/frontend/src/routes/simulateur-eligibilite/steps/+page.svelte +++ b/apps/frontend/src/routes/simulateur-eligibilite/steps/+page.svelte @@ -1,7 +1,8 @@
-

Revenus fiscaux

+

{currentPhase?.title as string}

+ {#if householdSize === 1} +
+ { + eligibilitySimulatorManager.formattedTaxableIncome = formatThousands( + (e.target as HTMLInputElement).value, + ); + }} + error={errors.formattedTaxableIncome} + errorDataTestId="formatted-taxable-income-error-message" /> +
+ {/if} + import z, { ZodError } from 'zod'; + import type { FormFieldError } from '$lib/utils/definitions'; + import { formatFormErrors } from '$lib/utils/helpers'; import Select from '$components/common/Select.svelte'; - - import eligibilitySimulatorManager from '$lib/managers/eligibility-simulator.svelte'; - import type { FormFieldError } from '$lib/utils/definitions'; import Form from '$components/common/Simulator/Form.svelte'; import Actions from '$components/common/Simulator/Actions.svelte'; import Action from '$components/common/Simulator/Action.svelte'; - import { formatFormErrors } from '$lib/utils/helpers'; import Input from '$components/common/Input.svelte'; - const { householdSize, hasDisability, nextPhase, goToNextPhase, loading } = - $derived(eligibilitySimulatorManager); + import eligibilitySimulatorManager from '$lib/managers/eligibility-simulator.svelte'; + + const { + currentPhase, + householdSize, + hasDisability, + nextPhase, + goToNextPhase, + loading, + } = $derived(eligibilitySimulatorManager); let errors: FormFieldError = $state({}); let selectedHouseholdSize: number | undefined = $derived.by(() => { @@ -82,26 +88,32 @@ e.preventDefault(); // TODO: Define type here with DTOs (same as in step1 from acquisition simulator) - // const payload = { - // selectedHouseholdSize, - // inputHouseholdSize, - // hasDisability, - // }; - - // try { - // FormData.parse(payload); - // errors = {}; - goToNextPhase(); - // } catch (e) { - // errors = formatFormErrors((e as ZodError).issues); - // } + const payload = { + selectedHouseholdSize, + inputHouseholdSize, + hasDisability, + }; + + try { + FormData.parse(payload); + errors = {}; + + eligibilitySimulatorManager.householdSize = Math.max( + selectedHouseholdSize || 0, + inputHouseholdSize || 0, + ); + + goToNextPhase(); + } catch (e) { + errors = formatFormErrors((e as ZodError).issues); + } };
-

Composition de mon foyer

+

{currentPhase?.title as string}

@@ -184,9 +196,6 @@ onChange={(e) => { const { value } = e.target as HTMLSelectElement; - console.log(value); - console.log(typeof value); - eligibilitySimulatorManager.hasDisability = value === '' ? undefined : value === 'true' ? true : false; }} @@ -201,6 +210,7 @@ value={inputHouseholdSize} label="Pouvez vous indiquer précisément le nombre de personnes qui composent votre foyer ?" required + skipHTML5Required type="number" id="input-household-size" step={1} diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/PropertySituation.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/PropertySituation.svelte index 4c2ed8fc8..cc9032853 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/PropertySituation.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/PropertySituation.svelte @@ -1,23 +1,112 @@
-

Situation immobilière

+

{currentPhase?.title as string}

+
+ +
+ { + const { value } = e.target as HTMLSelectElement; + + eligibilitySimulatorManager.dependantsAmount = value + ? Number(value) + : undefined; + }} + error={errors.dependantsAmount} + errorDataTestId="select-dependants-amount-error-message" /> +
+
+ { + const { value } = e.target as HTMLInputElement; + + eligibilitySimulatorManager.birthday = value; + }} + error={errors.birthday} + errorDataTestId="input-birthday-error-message" /> +
+
+ { + const { value } = e.target as HTMLInputElement; + console.log(value); + + eligibilitySimulatorManager.coBuyerBirthday = value; + }} + error={errors.coBuyerBirthday} + errorDataTestId="input-co-buyer-birthday-error-message" /> +
+ {/if} {:else if moreThanSixPersonsInHousehold}
{ if (this.previousPhase) { diff --git a/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts b/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts index 13b5b80c3..7bd8edd88 100644 --- a/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts +++ b/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts @@ -1,145 +1,322 @@ import { steps } from '$routes/simulateur-eligibilite/steps/steps'; -import { test, expect } from '@playwright/test'; +import { test, expect, type Locator } from '@playwright/test'; test.describe('navigation', () => { + let simulatorWrapper: Locator; + let submitButton: Locator; + let householdSizeSelect: Locator; + let householdSizeErrorMessage: Locator; + let hasDisabilitySelect: Locator; + let hasDisabilityErrorMessage: Locator; + let dependantsAmountSelect: Locator; + let dependantsAmountErrorMessage: Locator; + let birthdayInput: Locator; + let birthdayErrorMessage: Locator; + let coBuyerBirthdayInput: Locator; + let coBuyerBirthdayErrorMessage: Locator; + test.beforeEach(async ({ page }) => { await page.goto('/simulateur-eligibilite/steps'); - }); - - test('should have title', async ({ page }) => { - await expect(page).toHaveTitle( - /Simulateur d'éligibilité au Bail Réel Solidaire - BoRiS/, - ); - }); - - test('should have a valid h1', async ({ page }) => { - const h1 = page.getByRole('heading', { level: 1 }); - - expect(h1).toHaveText("Simulateur d'éligibilité"); - }); - test('Flow with 1 person in household', async ({ page }) => { - const simulatorWrapper = page.getByTestId('simulator-wrapper'); - const submitButton = simulatorWrapper.getByRole('button', { + simulatorWrapper = page.getByTestId('simulator-wrapper'); + submitButton = simulatorWrapper.getByRole('button', { name: /Étape suivante/i, }); - /** - * ------------- - * Phase "Composition de mon foyer" - * ------------- - */ - const stepTitle = simulatorWrapper.getByRole('heading', { level: 2 }); - expect(stepTitle).toHaveText( - `1. ${steps[0].title} Étape 1 sur ${steps.length}`, - ); - - const phaseTitle = simulatorWrapper.getByRole('heading', { level: 3 }); - expect(phaseTitle).toHaveText(`${steps[0].phases[0].title}`); - - expect(submitButton).toHaveText( - `Étape suivante ${steps[0].phases[1].title}`, - ); - - const selectHouseholdSize = simulatorWrapper.getByRole('combobox', { + householdSizeSelect = simulatorWrapper.getByRole('combobox', { name: 'Combien de personnes composent votre foyer ?', }); - expect(selectHouseholdSize).toBeVisible(); - - // Submit without selecting a household size - await submitButton.click(); - const householdSizeErrorMessage = simulatorWrapper.getByTestId( + householdSizeErrorMessage = simulatorWrapper.getByTestId( 'select-household-size-error-message', ); - expect(householdSizeErrorMessage).toBeVisible(); - // Selecting a household size - await selectHouseholdSize.selectOption('1'); - const selectHasDisability = simulatorWrapper.getByRole('combobox', { - name: 'Êtes-vous en situation de handicap ?', + hasDisabilitySelect = simulatorWrapper.getByRole('combobox', { + name: /situation de handicap/i, }); - expect(selectHasDisability).toBeVisible(); - - // Submit without selecting hasDisability - await submitButton.click(); - const hasDisabilityErrorMessage = simulatorWrapper.getByTestId( + hasDisabilityErrorMessage = simulatorWrapper.getByTestId( 'select-has-disability-error-message', ); - expect(hasDisabilityErrorMessage).toBeVisible(); - - // Selecting hasDisability - await selectHasDisability.selectOption('true'); - await submitButton.click(); - - /** - * ------------- - * Phase "Revenus fiscaux" - * ------------- - */ - expect(stepTitle).toHaveText( - `1. ${steps[0].title} Étape 1 sur ${steps.length}`, - ); - expect(phaseTitle).toHaveText(`${steps[0].phases[1].title}`); - expect(submitButton).toHaveText( - `Étape suivante ${steps[0].phases[2].title}`, - ); - await submitButton.click(); - // Submit without providing a formatted taxable income - const formattedTaxableErrorMessage = simulatorWrapper.getByTestId( - 'formatted-taxable-income-error-message', + dependantsAmountSelect = simulatorWrapper.getByRole('combobox', { + name: 'Combien avez-vous de personnes à charge (enfants compris) ?', + }); + dependantsAmountErrorMessage = simulatorWrapper.getByTestId( + 'select-dependants-amount-error-message', ); - expect(formattedTaxableErrorMessage).toBeVisible(); - // Providing invalid value for formatted taxable income - const formattedTaxableIncomeInput = simulatorWrapper.getByRole('textbox', { - name: /revenu fiscal de référence/i, + birthdayInput = simulatorWrapper.getByRole('textbox', { + name: 'Quelle est votre date de naissance ?', }); - await formattedTaxableIncomeInput.fill('sss'); - await submitButton.click(); - expect(formattedTaxableErrorMessage).toBeVisible(); - - // Providing valid value for formatted taxable income - await formattedTaxableIncomeInput.fill('25000'); - await submitButton.click(); - - /** - * ------------- - * Phase "Situation immobilière" - * ------------- - */ - expect(stepTitle).toHaveText( - `1. ${steps[0].title} Étape 1 sur ${steps.length}`, + birthdayErrorMessage = simulatorWrapper.getByTestId( + 'input-birthday-error-message', ); - expect(phaseTitle).toHaveText(`${steps[0].phases[2].title}`); - expect(submitButton).toHaveText(`Étape suivante ${steps[1].title}`); - const selectPropertySituation = simulatorWrapper.getByRole('combobox', { - name: 'Quelle est votre situation immobilière ?', + coBuyerBirthdayInput = simulatorWrapper.getByRole('textbox', { + name: 'Quelle est la date de naissance de votre co-acquéreur·euse ?', }); - expect(selectPropertySituation).toBeVisible(); - - // Submit without selecting a property situation - await submitButton.click(); - const propertySituationErrorMessage = simulatorWrapper.getByTestId( - 'select-property-situation-error-message', - ); - expect(propertySituationErrorMessage).toBeVisible(); - - // Selecting a property situation - await selectPropertySituation.selectOption('LOCATAIRE_SOCIAL'); - await submitButton.click(); - - /** - * ------------- - * Phase "Détail du resultat" - */ - expect(stepTitle).toHaveText( - `2. ${steps[1].title} Étape 2 sur ${steps.length}`, - ); - expect(phaseTitle).toHaveText(`${steps[1].phases[0].title}`); - expect(submitButton).toHaveText( - `Étape suivante ${steps[1].phases[1].title}`, + coBuyerBirthdayErrorMessage = simulatorWrapper.getByTestId( + 'input-co-buyer-birthday-error-message', ); }); + + test.describe('Définir mon éligibilité', () => { + test.describe('Composition de mon foyer', () => { + test.describe('Form validation', () => { + test('Submit without householdSize', async () => { + await submitButton.click(); + expect(householdSizeErrorMessage).toBeVisible(); + }); + + test('1 person in household, submit without hasDisability', async () => { + await householdSizeSelect.selectOption('1'); + expect(hasDisabilitySelect).toBeVisible(); + await submitButton.click(); + expect(hasDisabilityErrorMessage).toBeVisible(); + }); + + test('2 persons in household, submit without dependantsAmount & hasDisability', async () => { + await householdSizeSelect.selectOption('2'); + expect(dependantsAmountSelect).toBeVisible(); + expect(hasDisabilitySelect).toBeVisible(); + await submitButton.click(); + expect(dependantsAmountErrorMessage).toBeVisible(); + expect(hasDisabilityErrorMessage).toBeVisible(); + }); + + test('2 persons in household, submit without hasDisability & birthday & coBuyerBirthday', async () => { + await householdSizeSelect.selectOption('2'); + expect(dependantsAmountSelect).toBeVisible(); + await dependantsAmountSelect.selectOption('0'); + expect(birthdayInput).toBeVisible(); + expect(coBuyerBirthdayInput).toBeVisible(); + expect(hasDisabilitySelect).toBeVisible(); + await submitButton.click(); + expect(hasDisabilityErrorMessage).toBeVisible(); + expect(birthdayErrorMessage).toBeVisible(); + expect(coBuyerBirthdayErrorMessage).toBeVisible(); + }); + + test('4 persons in household, submit without dependantsAmount & hasDisability', async () => { + await householdSizeSelect.selectOption('2'); + expect(dependantsAmountSelect).toBeVisible(); + expect(hasDisabilitySelect).toBeVisible(); + await submitButton.click(); + expect(dependantsAmountErrorMessage).toBeVisible(); + expect(hasDisabilityErrorMessage).toBeVisible(); + }); + }); + }); + }); + + // test('Flow with 1 person in household', async ({ page }) => { + // const simulatorWrapper = page.getByTestId('simulator-wrapper'); + // const submitButton = simulatorWrapper.getByRole('button', { + // name: /Étape suivante/i, + // }); + + // /** + // * ------------- + // * Phase "Composition de mon foyer" + // * ------------- + // */ + // const stepTitle = simulatorWrapper.getByRole('heading', { level: 2 }); + // expect(stepTitle).toHaveText( + // `1. ${steps[0].title} Étape 1 sur ${steps.length}`, + // ); + + // const phaseTitle = simulatorWrapper.getByRole('heading', { level: 3 }); + // expect(phaseTitle).toHaveText(`${steps[0].phases[0].title}`); + + // expect(submitButton).toHaveText( + // `Étape suivante ${steps[0].phases[1].title}`, + // ); + + // const selectHouseholdSize = simulatorWrapper.getByRole('combobox', { + // name: 'Combien de personnes composent votre foyer ?', + // }); + // expect(selectHouseholdSize).toBeVisible(); + + // // Submit without selecting a household size + // await submitButton.click(); + // const householdSizeErrorMessage = simulatorWrapper.getByTestId( + // 'select-household-size-error-message', + // ); + // expect(householdSizeErrorMessage).toBeVisible(); + + // // Selecting a household size + // await selectHouseholdSize.selectOption('1'); + // const selectHasDisability = simulatorWrapper.getByRole('combobox', { + // name: 'Êtes-vous en situation de handicap ?', + // }); + // expect(selectHasDisability).toBeVisible(); + + // // Submit without selecting hasDisability + // await submitButton.click(); + // const hasDisabilityErrorMessage = simulatorWrapper.getByTestId( + // 'select-has-disability-error-message', + // ); + // expect(hasDisabilityErrorMessage).toBeVisible(); + + // // Selecting hasDisability + // await selectHasDisability.selectOption('true'); + // await submitButton.click(); + + // /** + // * ------------- + // * Phase "Revenus fiscaux" + // * ------------- + // */ + // expect(stepTitle).toHaveText( + // `1. ${steps[0].title} Étape 1 sur ${steps.length}`, + // ); + // expect(phaseTitle).toHaveText(`${steps[0].phases[1].title}`); + // expect(submitButton).toHaveText( + // `Étape suivante ${steps[0].phases[2].title}`, + // ); + // await submitButton.click(); + + // // Submit without providing a formatted taxable income + // const formattedTaxableErrorMessage = simulatorWrapper.getByTestId( + // 'formatted-taxable-income-error-message', + // ); + // expect(formattedTaxableErrorMessage).toBeVisible(); + + // // Providing invalid value for formatted taxable income + // const formattedTaxableIncomeInput = simulatorWrapper.getByRole('textbox', { + // name: /revenu fiscal de référence/i, + // }); + // await formattedTaxableIncomeInput.fill('sss'); + // await submitButton.click(); + // expect(formattedTaxableErrorMessage).toBeVisible(); + + // // Providing valid value for formatted taxable income + // await formattedTaxableIncomeInput.fill('25000'); + // await submitButton.click(); + + // /** + // * ------------- + // * Phase "Situation immobilière" + // * ------------- + // */ + // expect(stepTitle).toHaveText( + // `1. ${steps[0].title} Étape 1 sur ${steps.length}`, + // ); + // expect(phaseTitle).toHaveText(`${steps[0].phases[2].title}`); + // expect(submitButton).toHaveText(`Étape suivante ${steps[1].title}`); + + // const selectPropertySituation = simulatorWrapper.getByRole('combobox', { + // name: 'Quelle est votre situation immobilière ?', + // }); + // expect(selectPropertySituation).toBeVisible(); + + // // Submit without selecting a property situation + // await submitButton.click(); + // const propertySituationErrorMessage = simulatorWrapper.getByTestId( + // 'select-property-situation-error-message', + // ); + // expect(propertySituationErrorMessage).toBeVisible(); + + // // Selecting a property situation + // await selectPropertySituation.selectOption('LOCATAIRE_SOCIAL'); + // await submitButton.click(); + + // /** + // * ------------- + // * Phase "Détail du resultat" + // */ + // expect(stepTitle).toHaveText( + // `2. ${steps[1].title} Étape 2 sur ${steps.length}`, + // ); + // expect(phaseTitle).toHaveText(`${steps[1].phases[0].title}`); + // expect(submitButton).toHaveText( + // `Étape suivante ${steps[1].phases[1].title}`, + // ); + // }); + + // test('Flow with between 2 and 6 persons in household', async ({ page }) => { + // const simulatorWrapper = page.getByTestId('simulator-wrapper'); + // const submitButton = simulatorWrapper.getByRole('button', { + // name: /Étape suivante/i, + // }); + + // /** + // * ------------- + // * Phase "Composition de mon foyer" + // * ------------- + // */ + // const stepTitle = simulatorWrapper.getByRole('heading', { level: 2 }); + // expect(stepTitle).toHaveText( + // `1. ${steps[0].title} Étape 1 sur ${steps.length}`, + // ); + + // const phaseTitle = simulatorWrapper.getByRole('heading', { level: 3 }); + // expect(phaseTitle).toHaveText(`${steps[0].phases[0].title}`); + + // expect(submitButton).toHaveText( + // `Étape suivante ${steps[0].phases[1].title}`, + // ); + + // const selectHouseholdSize = simulatorWrapper.getByRole('combobox', { + // name: 'Combien de personnes composent votre foyer ?', + // }); + // expect(selectHouseholdSize).toBeVisible(); + + // // Submit without selecting a household size + // await submitButton.click(); + // const householdSizeErrorMessage = simulatorWrapper.getByTestId( + // 'select-household-size-error-message', + // ); + // expect(householdSizeErrorMessage).toBeVisible(); + + // // Selecting a household size + // await selectHouseholdSize.selectOption('2'); + + // const selectDependantsAmount = simulatorWrapper.getByRole('combobox', { + // name: 'Combien avez-vous de personnes à charge (enfants compris) ?', + // }); + // expect(selectDependantsAmount).toBeVisible(); + + // const selectHasDisability = simulatorWrapper.getByRole('combobox', { + // name: "Dans votre foyer (vous y compris), est-ce qu'une ou plusieurs personnes sont en situation de handicap ?", + // }); + // expect(selectDependantsAmount).toBeVisible(); + + // await submitButton.click(); + + // // Submit without selecting a dependants amount and hasDisability + // const dependantsAmountErrorMessage = simulatorWrapper.getByTestId( + // 'select-dependants-amount-error-message', + // ); + // expect(dependantsAmountErrorMessage).toBeVisible(); + // const hasDisabilityErrorMessage = simulatorWrapper.getByTestId( + // 'select-has-disability-error-message', + // ); + // expect(hasDisabilityErrorMessage).toBeVisible(); + + // // Selecting a dependants amount + // await selectDependantsAmount.selectOption('0'); + + // // Selecting hasDisability + // await selectHasDisability.selectOption('false'); + + // await submitButton.click(); + + // /** + // * ------------- + // * Phase "Revenus fiscaux" + // * ------------- + // */ + // expect(stepTitle).toHaveText( + // `1. ${steps[0].title} Étape 1 sur ${steps.length}`, + // ); + // expect(phaseTitle).toHaveText(`${steps[0].phases[1].title}`); + // expect(submitButton).toHaveText( + // `Étape suivante ${steps[0].phases[2].title}`, + // ); + + // // await submitButton.click(); + // }); }); + +// test errors messages +// test flow screen by screen From ddbf01c9bf37a6b9f70e05142e0604934d27e2aa Mon Sep 17 00:00:00 2001 From: fuuuzz Date: Wed, 4 Feb 2026 18:19:55 +0100 Subject: [PATCH 11/56] clean: refacto --- .../EligibilityDefinition.svelte | 3 +- .../FiscalRevenues.svelte | 2 +- .../HouseholdComposition.svelte | 189 +++------ .../managers/eligibility-simulator.svelte.ts | 6 +- .../src/lib/utils/eligibility-simulator.ts | 206 ++++++++++ .../simulateur-eligibilite/steps/steps.ts | 73 ---- .../steps/+page.spec.ts | 380 +++++++----------- 7 files changed, 403 insertions(+), 456 deletions(-) create mode 100644 apps/frontend/src/lib/utils/eligibility-simulator.ts delete mode 100644 apps/frontend/src/routes/simulateur-eligibilite/steps/steps.ts diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/EligibilityDefinition.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/EligibilityDefinition.svelte index 75276ebf7..4841c3b1b 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/EligibilityDefinition.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/EligibilityDefinition.svelte @@ -7,6 +7,7 @@ import FiscalRevenues from '$components/pages/simulateur-eligibilite/steps/EligibilityDefinition/FiscalRevenues.svelte'; import HouseholdComposition from '$components/pages/simulateur-eligibilite/steps/EligibilityDefinition/HouseholdComposition.svelte'; import PropertySituation from '$components/pages/simulateur-eligibilite/steps/EligibilityDefinition/PropertySituation.svelte'; + import { formatYearMinusN } from '$lib/utils/formatters'; const { currentStep, steps, currentPhase } = $derived( eligibilitySimulatorManager, @@ -35,7 +36,7 @@ + content={`Pensez à vous vous munir de votre avis d'imposition de l'année ${formatYearMinusN(1)}, concernant vos revenus de l'année ${formatYearMinusN(2)}, afin de renseigner vos revenus fiscaux.`}> diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/FiscalRevenues.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/FiscalRevenues.svelte index 24a5a0b51..f91e48a09 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/FiscalRevenues.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/FiscalRevenues.svelte @@ -70,7 +70,7 @@
{ + return value > 6; + }, 'Veuillez saisir un chiffre supérieur à 6.'), + }); } return schema; @@ -96,6 +108,7 @@ // TODO: Define type here with DTOs (same as in step1 from acquisition simulator) const payload = { selectedHouseholdSize, + inputHouseholdSize, dependantsAmount, hasDisability, birthday, @@ -117,8 +130,6 @@ errors = formatFormErrors((e as ZodError).issues); } }; - - $inspect(birthday, coBuyerBirthday); @@ -129,81 +140,32 @@
({ + ...question, + selected: hasDisability === question.value, + }), + )} onChange={(e) => { const { value } = e.target as HTMLSelectElement; @@ -211,60 +173,18 @@ value === '' ? undefined : value === 'true' ? true : false; }} error={errors.hasDisability} - errorDataTestId="select-has-disability-error-message" /> + errorDataTestId={questions.singlePersonInHouseholdHasDisability + .dataTestId} />
{:else if twoToSixPersonsInHousehold}
({ + ...question, + selected: hasDisability === question.value, + }), + )} onChange={(e) => { const { value } = e.target as HTMLSelectElement; @@ -303,13 +212,14 @@ value === '' ? undefined : value === 'true' ? true : false; }} error={errors.hasDisability} - errorDataTestId="select-has-disability-error-message" /> + errorDataTestId={questions.twoToSixPersonsInHouseholdHasDisability + .dataTestId} />
{#if selectedHouseholdSize === 2 && dependantsAmount === 0}
+ errorDataTestId={questions.birthday.dataTestId} />
+ errorDataTestId={questions.coBuyerBirthday.dataTestId} />
{/if} {:else if moreThanSixPersonsInHousehold}
+ error={errors.inputHouseholdSize} + errorDataTestId={questions.inputHouseholdSize.dataTestId} />
{/if}
diff --git a/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts b/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts index cac35e07b..15904be44 100644 --- a/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts +++ b/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts @@ -1,9 +1,5 @@ import { browser } from '$app/environment'; -import { - steps, - type Phase, - type Step, -} from '$routes/simulateur-eligibilite/steps/steps'; +import { steps, type Phase, type Step } from '$lib/utils/eligibility-simulator'; export type PropertySituation = | 'LOCATAIRE_SOCIAL' diff --git a/apps/frontend/src/lib/utils/eligibility-simulator.ts b/apps/frontend/src/lib/utils/eligibility-simulator.ts new file mode 100644 index 000000000..6e6c32528 --- /dev/null +++ b/apps/frontend/src/lib/utils/eligibility-simulator.ts @@ -0,0 +1,206 @@ +export type Phase = { + title: string; + phase: number; +}; + +export type Step = { + title: string; + step: number; + phases: Phase[]; +}; + +export const steps: Step[] = [ + { + title: 'Définir mon éligibilité', + step: 1, + phases: [ + { + title: 'Composition de mon foyer', + phase: 1, + }, + { + title: 'Revenus fiscaux', + phase: 2, + }, + { + title: 'Situation immobilière', + phase: 3, + }, + ], + }, + { + title: "Mon résultat d'éligibilité", + step: 2, + phases: [ + { + title: 'Détail du resultat', + phase: 1, + }, + { + title: 'Informations personnelles', + phase: 2, + }, + ], + }, + { + title: 'Ma recherche', + step: 3, + phases: [ + { + title: 'Informations sur le logement', + phase: 1, + }, + { + title: 'Informations financières', + phase: 2, + }, + { + title: 'Informations additionnelles', + phase: 3, + }, + ], + }, + { + title: 'Synthése', + step: 4, + phases: [ + { + title: 'Synthèse', + phase: 1, + }, + ], + }, +]; + +export const questions = { + selectedHouseholdSize: { + label: 'Combien de personnes composent votre foyer ?', + dataTestId: 'select-household-size-error-message', + options: [ + { + value: undefined, + label: 'Veuillez sélectionner une option', + }, + { + value: 1, + label: '1 personne', + }, + { + value: 2, + label: '2 personnes', + }, + { + value: 3, + label: '3 personnes', + }, + { + value: 4, + label: '4 personnes', + }, + { + value: 5, + label: '5 personnes', + }, + { + value: 6, + label: '6 personnes', + }, + { + value: -1, + label: 'Plus de 6 personnes', + }, + ], + }, + singlePersonInHouseholdHasDisability: { + label: 'Êtes-vous en situation de handicap ?', + dataTestId: 'select-has-disability-error-message', + options: [ + { + value: undefined, + label: 'Veuillez sélectionner une option', + }, + { + value: true, + label: 'Oui', + }, + { + value: false, + label: 'Non', + }, + ], + }, + dependantsAmount: { + label: 'Combien avez-vous de personnes à charge (enfants compris) ?', + dataTestId: 'select-dependants-amount-error-message', + options: [ + { + value: undefined, + label: 'Veuillez sélectionner une option', + }, + { + value: 0, + label: '0 personne', + }, + { + value: 1, + label: '1 personne', + }, + { + value: 2, + label: '2 personnes', + }, + { + value: 3, + label: '3 personnes', + }, + { + value: 4, + label: '4 personnes', + }, + { + value: 5, + label: '5 personnes', + }, + { + value: 6, + label: '6 personnes', + }, + { + value: -1, + label: 'Plus de 6 personnes', + }, + ], + }, + twoToSixPersonsInHouseholdHasDisability: { + label: + "Dans votre foyer (vous y compris), est-ce qu'une ou plusieurs personnes sont en situation de handicap ?", + dataTestId: 'select-has-disability-error-message', + options: [ + { + value: undefined, + label: 'Veuillez sélectionner une option', + }, + { + value: true, + label: 'Oui', + }, + { + value: false, + label: 'Non', + }, + ], + }, + birthday: { + label: 'Quelle est votre date de naissance ?', + dataTestId: 'input-birthday-error-message', + }, + coBuyerBirthday: { + label: 'Quelle est la date de naissance de votre co-acquéreur·euse ?', + dataTestId: 'input-co-buyer-birthday-error-message', + }, + inputHouseholdSize: { + label: + 'Pouvez vous indiquer précisément le nombre de personnes qui composent votre foyer ?', + dataTestId: 'input-household-size-error-message', + }, +}; diff --git a/apps/frontend/src/routes/simulateur-eligibilite/steps/steps.ts b/apps/frontend/src/routes/simulateur-eligibilite/steps/steps.ts deleted file mode 100644 index 2b88e4ef3..000000000 --- a/apps/frontend/src/routes/simulateur-eligibilite/steps/steps.ts +++ /dev/null @@ -1,73 +0,0 @@ -export type Phase = { - title: string; - phase: number; -}; - -export type Step = { - title: string; - step: number; - phases: Phase[]; -}; - -export const steps: Step[] = [ - { - title: 'Définir mon éligibilité', - step: 1, - phases: [ - { - title: 'Composition de mon foyer', - phase: 1, - }, - { - title: 'Revenus fiscaux', - phase: 2, - }, - { - title: 'Situation immobilière', - phase: 3, - }, - ], - }, - { - title: "Mon résultat d'éligibilité", - step: 2, - phases: [ - { - title: 'Détail du resultat', - phase: 1, - }, - { - title: 'Informations personnelles', - phase: 2, - }, - ], - }, - { - title: 'Ma recherche', - step: 3, - phases: [ - { - title: 'Informations sur le logement', - phase: 1, - }, - { - title: 'Informations financières', - phase: 2, - }, - { - title: 'Informations additionnelles', - phase: 3, - }, - ], - }, - { - title: 'Synthése', - step: 4, - phases: [ - { - title: 'Synthèse', - phase: 1, - }, - ], - }, -]; diff --git a/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts b/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts index 7bd8edd88..f49456c02 100644 --- a/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts +++ b/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts @@ -1,67 +1,130 @@ -import { steps } from '$routes/simulateur-eligibilite/steps/steps'; +import { questions } from '$lib/utils/eligibility-simulator'; +import { steps } from '$lib/utils/eligibility-simulator'; import { test, expect, type Locator } from '@playwright/test'; test.describe('navigation', () => { let simulatorWrapper: Locator; + let stepTitle: Locator; + let phaseTitle: Locator; let submitButton: Locator; let householdSizeSelect: Locator; let householdSizeErrorMessage: Locator; - let hasDisabilitySelect: Locator; - let hasDisabilityErrorMessage: Locator; + let singlePersonInHouseholdHasDisabilitySelect: Locator; + let singlePersonInHouseholdHasDisabilityErrorMessage: Locator; + let twoToSixPersonsInHouseholdHasDisabilitySelect: Locator; + let twoToSixPersonsInHouseholdHasDisabilityErrorMessage: Locator; let dependantsAmountSelect: Locator; let dependantsAmountErrorMessage: Locator; let birthdayInput: Locator; let birthdayErrorMessage: Locator; let coBuyerBirthdayInput: Locator; let coBuyerBirthdayErrorMessage: Locator; + let inputHouseholdSizeInput: Locator; + let inputHouseholdSizeErrorMessage: Locator; test.beforeEach(async ({ page }) => { await page.goto('/simulateur-eligibilite/steps'); simulatorWrapper = page.getByTestId('simulator-wrapper'); + stepTitle = simulatorWrapper.getByRole('heading', { level: 2 }); + phaseTitle = simulatorWrapper.getByRole('heading', { level: 3 }); submitButton = simulatorWrapper.getByRole('button', { name: /Étape suivante/i, }); - householdSizeSelect = simulatorWrapper.getByRole('combobox', { - name: 'Combien de personnes composent votre foyer ?', + name: questions.selectedHouseholdSize.label, }); householdSizeErrorMessage = simulatorWrapper.getByTestId( - 'select-household-size-error-message', + questions.selectedHouseholdSize.dataTestId, ); - - hasDisabilitySelect = simulatorWrapper.getByRole('combobox', { - name: /situation de handicap/i, - }); - hasDisabilityErrorMessage = simulatorWrapper.getByTestId( - 'select-has-disability-error-message', + singlePersonInHouseholdHasDisabilitySelect = simulatorWrapper.getByRole( + 'combobox', + { + name: questions.singlePersonInHouseholdHasDisability.label, + }, ); - + singlePersonInHouseholdHasDisabilityErrorMessage = + simulatorWrapper.getByTestId( + questions.singlePersonInHouseholdHasDisability.dataTestId, + ); + twoToSixPersonsInHouseholdHasDisabilitySelect = simulatorWrapper.getByRole( + 'combobox', + { + name: questions.twoToSixPersonsInHouseholdHasDisability.label, + }, + ); + twoToSixPersonsInHouseholdHasDisabilityErrorMessage = + simulatorWrapper.getByTestId( + questions.twoToSixPersonsInHouseholdHasDisability.dataTestId, + ); dependantsAmountSelect = simulatorWrapper.getByRole('combobox', { - name: 'Combien avez-vous de personnes à charge (enfants compris) ?', + name: questions.dependantsAmount.label, }); dependantsAmountErrorMessage = simulatorWrapper.getByTestId( - 'select-dependants-amount-error-message', + questions.dependantsAmount.dataTestId, ); - birthdayInput = simulatorWrapper.getByRole('textbox', { - name: 'Quelle est votre date de naissance ?', + name: questions.birthday.label, }); birthdayErrorMessage = simulatorWrapper.getByTestId( - 'input-birthday-error-message', + questions.birthday.dataTestId, ); - coBuyerBirthdayInput = simulatorWrapper.getByRole('textbox', { - name: 'Quelle est la date de naissance de votre co-acquéreur·euse ?', + name: questions.coBuyerBirthday.label, }); coBuyerBirthdayErrorMessage = simulatorWrapper.getByTestId( - 'input-co-buyer-birthday-error-message', + questions.coBuyerBirthday.dataTestId, + ); + inputHouseholdSizeInput = simulatorWrapper.getByRole('spinbutton', { + name: questions.inputHouseholdSize.label, + }); + inputHouseholdSizeErrorMessage = simulatorWrapper.getByTestId( + questions.inputHouseholdSize.dataTestId, ); }); + const scenarii = { + singlePersonInHousehold: { + performHouseholdComposition: async () => { + await householdSizeSelect.selectOption('1'); + await singlePersonInHouseholdHasDisabilitySelect.selectOption('false'); + await submitButton.click(); + }, + }, + twoPersonsInHouseholdWithoutDependants: { + performHouseholdComposition: async () => { + await householdSizeSelect.selectOption('2'); + await dependantsAmountSelect.selectOption('0'); + await twoToSixPersonsInHouseholdHasDisabilitySelect.selectOption( + 'false', + ); + await birthdayInput.fill('2000-01-01'); + await coBuyerBirthdayInput.fill('2000-01-01'); + await submitButton.click(); + }, + }, + fourPersonsInHouseholdWithDependants: { + performHouseholdComposition: async () => { + await householdSizeSelect.selectOption('4'); + await dependantsAmountSelect.selectOption('2'); + await twoToSixPersonsInHouseholdHasDisabilitySelect.selectOption( + 'false', + ); + await submitButton.click(); + }, + }, + moreThanSixPersonsInHousehold: { + performHouseholdComposition: async () => { + await householdSizeSelect.selectOption('-1'); + await inputHouseholdSizeInput.fill('7'); + await submitButton.click(); + }, + }, + }; + test.describe('Définir mon éligibilité', () => { test.describe('Composition de mon foyer', () => { - test.describe('Form validation', () => { + test.describe('Form errors', () => { test('Submit without householdSize', async () => { await submitButton.click(); expect(householdSizeErrorMessage).toBeVisible(); @@ -69,18 +132,22 @@ test.describe('navigation', () => { test('1 person in household, submit without hasDisability', async () => { await householdSizeSelect.selectOption('1'); - expect(hasDisabilitySelect).toBeVisible(); + expect(singlePersonInHouseholdHasDisabilitySelect).toBeVisible(); await submitButton.click(); - expect(hasDisabilityErrorMessage).toBeVisible(); + expect( + singlePersonInHouseholdHasDisabilityErrorMessage, + ).toBeVisible(); }); test('2 persons in household, submit without dependantsAmount & hasDisability', async () => { await householdSizeSelect.selectOption('2'); expect(dependantsAmountSelect).toBeVisible(); - expect(hasDisabilitySelect).toBeVisible(); + expect(twoToSixPersonsInHouseholdHasDisabilitySelect).toBeVisible(); await submitButton.click(); expect(dependantsAmountErrorMessage).toBeVisible(); - expect(hasDisabilityErrorMessage).toBeVisible(); + expect( + twoToSixPersonsInHouseholdHasDisabilityErrorMessage, + ).toBeVisible(); }); test('2 persons in household, submit without hasDisability & birthday & coBuyerBirthday', async () => { @@ -89,9 +156,11 @@ test.describe('navigation', () => { await dependantsAmountSelect.selectOption('0'); expect(birthdayInput).toBeVisible(); expect(coBuyerBirthdayInput).toBeVisible(); - expect(hasDisabilitySelect).toBeVisible(); + expect(twoToSixPersonsInHouseholdHasDisabilitySelect).toBeVisible(); await submitButton.click(); - expect(hasDisabilityErrorMessage).toBeVisible(); + expect( + twoToSixPersonsInHouseholdHasDisabilityErrorMessage, + ).toBeVisible(); expect(birthdayErrorMessage).toBeVisible(); expect(coBuyerBirthdayErrorMessage).toBeVisible(); }); @@ -99,224 +168,61 @@ test.describe('navigation', () => { test('4 persons in household, submit without dependantsAmount & hasDisability', async () => { await householdSizeSelect.selectOption('2'); expect(dependantsAmountSelect).toBeVisible(); - expect(hasDisabilitySelect).toBeVisible(); + expect(twoToSixPersonsInHouseholdHasDisabilitySelect).toBeVisible(); await submitButton.click(); expect(dependantsAmountErrorMessage).toBeVisible(); - expect(hasDisabilityErrorMessage).toBeVisible(); + expect( + twoToSixPersonsInHouseholdHasDisabilityErrorMessage, + ).toBeVisible(); }); - }); - }); - }); - - // test('Flow with 1 person in household', async ({ page }) => { - // const simulatorWrapper = page.getByTestId('simulator-wrapper'); - // const submitButton = simulatorWrapper.getByRole('button', { - // name: /Étape suivante/i, - // }); - - // /** - // * ------------- - // * Phase "Composition de mon foyer" - // * ------------- - // */ - // const stepTitle = simulatorWrapper.getByRole('heading', { level: 2 }); - // expect(stepTitle).toHaveText( - // `1. ${steps[0].title} Étape 1 sur ${steps.length}`, - // ); - - // const phaseTitle = simulatorWrapper.getByRole('heading', { level: 3 }); - // expect(phaseTitle).toHaveText(`${steps[0].phases[0].title}`); - - // expect(submitButton).toHaveText( - // `Étape suivante ${steps[0].phases[1].title}`, - // ); - - // const selectHouseholdSize = simulatorWrapper.getByRole('combobox', { - // name: 'Combien de personnes composent votre foyer ?', - // }); - // expect(selectHouseholdSize).toBeVisible(); - - // // Submit without selecting a household size - // await submitButton.click(); - // const householdSizeErrorMessage = simulatorWrapper.getByTestId( - // 'select-household-size-error-message', - // ); - // expect(householdSizeErrorMessage).toBeVisible(); - - // // Selecting a household size - // await selectHouseholdSize.selectOption('1'); - // const selectHasDisability = simulatorWrapper.getByRole('combobox', { - // name: 'Êtes-vous en situation de handicap ?', - // }); - // expect(selectHasDisability).toBeVisible(); - - // // Submit without selecting hasDisability - // await submitButton.click(); - // const hasDisabilityErrorMessage = simulatorWrapper.getByTestId( - // 'select-has-disability-error-message', - // ); - // expect(hasDisabilityErrorMessage).toBeVisible(); - - // // Selecting hasDisability - // await selectHasDisability.selectOption('true'); - // await submitButton.click(); - - // /** - // * ------------- - // * Phase "Revenus fiscaux" - // * ------------- - // */ - // expect(stepTitle).toHaveText( - // `1. ${steps[0].title} Étape 1 sur ${steps.length}`, - // ); - // expect(phaseTitle).toHaveText(`${steps[0].phases[1].title}`); - // expect(submitButton).toHaveText( - // `Étape suivante ${steps[0].phases[2].title}`, - // ); - // await submitButton.click(); - - // // Submit without providing a formatted taxable income - // const formattedTaxableErrorMessage = simulatorWrapper.getByTestId( - // 'formatted-taxable-income-error-message', - // ); - // expect(formattedTaxableErrorMessage).toBeVisible(); - - // // Providing invalid value for formatted taxable income - // const formattedTaxableIncomeInput = simulatorWrapper.getByRole('textbox', { - // name: /revenu fiscal de référence/i, - // }); - // await formattedTaxableIncomeInput.fill('sss'); - // await submitButton.click(); - // expect(formattedTaxableErrorMessage).toBeVisible(); - - // // Providing valid value for formatted taxable income - // await formattedTaxableIncomeInput.fill('25000'); - // await submitButton.click(); - - // /** - // * ------------- - // * Phase "Situation immobilière" - // * ------------- - // */ - // expect(stepTitle).toHaveText( - // `1. ${steps[0].title} Étape 1 sur ${steps.length}`, - // ); - // expect(phaseTitle).toHaveText(`${steps[0].phases[2].title}`); - // expect(submitButton).toHaveText(`Étape suivante ${steps[1].title}`); - // const selectPropertySituation = simulatorWrapper.getByRole('combobox', { - // name: 'Quelle est votre situation immobilière ?', - // }); - // expect(selectPropertySituation).toBeVisible(); - - // // Submit without selecting a property situation - // await submitButton.click(); - // const propertySituationErrorMessage = simulatorWrapper.getByTestId( - // 'select-property-situation-error-message', - // ); - // expect(propertySituationErrorMessage).toBeVisible(); - - // // Selecting a property situation - // await selectPropertySituation.selectOption('LOCATAIRE_SOCIAL'); - // await submitButton.click(); - - // /** - // * ------------- - // * Phase "Détail du resultat" - // */ - // expect(stepTitle).toHaveText( - // `2. ${steps[1].title} Étape 2 sur ${steps.length}`, - // ); - // expect(phaseTitle).toHaveText(`${steps[1].phases[0].title}`); - // expect(submitButton).toHaveText( - // `Étape suivante ${steps[1].phases[1].title}`, - // ); - // }); - - // test('Flow with between 2 and 6 persons in household', async ({ page }) => { - // const simulatorWrapper = page.getByTestId('simulator-wrapper'); - // const submitButton = simulatorWrapper.getByRole('button', { - // name: /Étape suivante/i, - // }); - - // /** - // * ------------- - // * Phase "Composition de mon foyer" - // * ------------- - // */ - // const stepTitle = simulatorWrapper.getByRole('heading', { level: 2 }); - // expect(stepTitle).toHaveText( - // `1. ${steps[0].title} Étape 1 sur ${steps.length}`, - // ); - - // const phaseTitle = simulatorWrapper.getByRole('heading', { level: 3 }); - // expect(phaseTitle).toHaveText(`${steps[0].phases[0].title}`); - - // expect(submitButton).toHaveText( - // `Étape suivante ${steps[0].phases[1].title}`, - // ); - - // const selectHouseholdSize = simulatorWrapper.getByRole('combobox', { - // name: 'Combien de personnes composent votre foyer ?', - // }); - // expect(selectHouseholdSize).toBeVisible(); - - // // Submit without selecting a household size - // await submitButton.click(); - // const householdSizeErrorMessage = simulatorWrapper.getByTestId( - // 'select-household-size-error-message', - // ); - // expect(householdSizeErrorMessage).toBeVisible(); - - // // Selecting a household size - // await selectHouseholdSize.selectOption('2'); - - // const selectDependantsAmount = simulatorWrapper.getByRole('combobox', { - // name: 'Combien avez-vous de personnes à charge (enfants compris) ?', - // }); - // expect(selectDependantsAmount).toBeVisible(); - - // const selectHasDisability = simulatorWrapper.getByRole('combobox', { - // name: "Dans votre foyer (vous y compris), est-ce qu'une ou plusieurs personnes sont en situation de handicap ?", - // }); - // expect(selectDependantsAmount).toBeVisible(); + test('More than 6 persons in selectedHousholdSize, submit without inputHouseholdSize', async () => { + await householdSizeSelect.selectOption('-1'); + expect(inputHouseholdSizeInput).toBeVisible(); + await submitButton.click(); + expect(inputHouseholdSizeErrorMessage).toBeVisible(); + }); - // await submitButton.click(); + test('More than 6 persons in selectedHousholdSize, submit with invalid inputHouseholdSize', async () => { + await householdSizeSelect.selectOption('-1'); + expect(inputHouseholdSizeInput).toBeVisible(); + await inputHouseholdSizeInput.fill('5'); + await submitButton.click(); + expect(inputHouseholdSizeErrorMessage).toBeVisible(); + }); + }); - // // Submit without selecting a dependants amount and hasDisability - // const dependantsAmountErrorMessage = simulatorWrapper.getByTestId( - // 'select-dependants-amount-error-message', - // ); - // expect(dependantsAmountErrorMessage).toBeVisible(); - // const hasDisabilityErrorMessage = simulatorWrapper.getByTestId( - // 'select-has-disability-error-message', - // ); - // expect(hasDisabilityErrorMessage).toBeVisible(); + test.describe('Form success', () => { + test.beforeEach(async () => { + expect(stepTitle).toHaveText( + `1. ${steps[0].title} Étape 1 sur ${steps.length}`, + ); + expect(phaseTitle).toHaveText(`${steps[0].phases[0].title}`); + }); - // // Selecting a dependants amount - // await selectDependantsAmount.selectOption('0'); + test.afterEach(async () => { + expect(stepTitle).toHaveText( + `1. ${steps[0].title} Étape 1 sur ${steps.length}`, + ); + expect(phaseTitle).toHaveText(`${steps[0].phases[1].title}`); + }); - // // Selecting hasDisability - // await selectHasDisability.selectOption('false'); + test('1 person in household', async () => { + await scenarii.singlePersonInHousehold.performHouseholdComposition(); + }); - // await submitButton.click(); + test('2 persons in household without dependants', async () => { + await scenarii.twoPersonsInHouseholdWithoutDependants.performHouseholdComposition(); + }); - // /** - // * ------------- - // * Phase "Revenus fiscaux" - // * ------------- - // */ - // expect(stepTitle).toHaveText( - // `1. ${steps[0].title} Étape 1 sur ${steps.length}`, - // ); - // expect(phaseTitle).toHaveText(`${steps[0].phases[1].title}`); - // expect(submitButton).toHaveText( - // `Étape suivante ${steps[0].phases[2].title}`, - // ); + test('4 persons in household with dependants', async () => { + await scenarii.fourPersonsInHouseholdWithDependants.performHouseholdComposition(); + }); - // // await submitButton.click(); - // }); + test('More than 6 persons in household', async () => { + await scenarii.moreThanSixPersonsInHousehold.performHouseholdComposition(); + }); + }); + }); + }); }); - -// test errors messages -// test flow screen by screen From 07dad932e20e4996e19ea991b0ff9f3eb4b85fc1 Mon Sep 17 00:00:00 2001 From: fuuuzz Date: Thu, 5 Feb 2026 18:05:57 +0100 Subject: [PATCH 12/56] specs: fixing e2e frotnend --- apps/frontend/package.json | 2 +- .../src/lib/components/common/Input.svelte | 20 +- .../FiscalRevenues.svelte | 202 +++++++++++--- .../HouseholdComposition.svelte | 59 ++-- .../PropertySituation.svelte | 56 ++-- .../managers/eligibility-simulator.svelte.ts | 45 ++- .../src/lib/utils/eligibility-simulator.ts | 102 ++++++- .../steps/+page.spec.ts | 257 +++++++++++++++++- apps/frontend/tsconfig.json | 6 +- 9 files changed, 616 insertions(+), 133 deletions(-) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 378d21344..962dacd04 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -16,7 +16,7 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --check . && eslint .", "format": "prettier --write .", - "test:e2e": "playwright test", + "test:e2e": "npx playwright test --headed", "test:e2e:ui": "playwright test --ui", "test:unit": "vitest", "lighthouse-mobile": "lhci autorun --config=./.lighthouse-mobile.json", diff --git a/apps/frontend/src/lib/components/common/Input.svelte b/apps/frontend/src/lib/components/common/Input.svelte index 5be4fdd19..bb7fa03a1 100644 --- a/apps/frontend/src/lib/components/common/Input.svelte +++ b/apps/frontend/src/lib/components/common/Input.svelte @@ -6,6 +6,7 @@ import { nanoid } from 'nanoid'; import type { AriaAttributes, AriaRole, FullAutoFill } from 'svelte/elements'; import Tooltip from './Tooltip.svelte'; + import type { boolean } from 'zod'; type Props = { id?: string; @@ -23,6 +24,7 @@ currency?: boolean; value?: string | number; label?: string; + rawLabel?: boolean; hint?: string; labelTooltip?: string; required?: boolean; @@ -38,6 +40,7 @@ forceNoMarginBottom?: boolean; error?: string; errorDataTestId?: string; + dataTestId?: string; disabled?: boolean; onChange?: (event: Event) => void; onKeydown?: (event: KeyboardEvent) => void; @@ -51,6 +54,7 @@ currency = false, value, label = '', + rawLabel = false, hint, labelTooltip = '', required = false, @@ -64,6 +68,7 @@ step, forceNoMarginBottom = false, error, + dataTestId, errorDataTestId = 'input-error-message', disabled, onChange, @@ -84,9 +89,20 @@ for={id}>
- {label} + {#if rawLabel} + {@html label} + {:else} + {label} + {/if} {required ? '*' : ''} + {#if labelTooltip} + +
+ {@html labelTooltip} +
+
+ {/if}
{#if hint} {hint} @@ -125,8 +141,8 @@ {min} {max} {step} + data-testid={dataTestId} oninput={(e) => { - // console.log((e.target as HTMLInputElement).value); if (currency) { let { value } = e.target as HTMLInputElement; const lastCharacter = value[value.length - 1]; diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/FiscalRevenues.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/FiscalRevenues.svelte index f91e48a09..76c3c896b 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/FiscalRevenues.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/FiscalRevenues.svelte @@ -12,12 +12,20 @@ import { formattedThousandsToNumber, formatThousands, - formatYearMinusN, } from '$lib/utils/formatters'; + import { + questions, + type DeclarationType, + } from '$lib/utils/eligibility-simulator'; + import Select from '$components/common/Select.svelte'; + const { formattedTaxableIncome, - householdSize, + singlePersonInHousehold, + declarationType, + firstCoBuyerFormattedTaxableIncome, + secondCoBuyerFormattedTaxableIncome, currentPhase, nextPhase, goToNextPhase, @@ -28,12 +36,52 @@ let errors: FormFieldError = $state({}); - let FormData = z.object({ - formattedTaxableIncome: z - .string('Veuillez saisir un chiffre supérieur à 0.') - .refine((value) => { - return formattedThousandsToNumber(value) > 0; - }, 'Veuillez saisir un chiffre supérieur à 0.'), + let formData = $derived.by(() => { + let schema = z.object({}); + + if (singlePersonInHousehold) { + schema = schema.extend({ + formattedTaxableIncome: z + .string(questions.formattedTaxableIncome.errorMessage) + .refine((value) => { + return formattedThousandsToNumber(value) > 0; + }, questions.formattedTaxableIncome.errorMessage), + }); + } else { + schema = schema.extend({ + declarationType: z.enum( + ['SEUL_SOUHAIT_SEUL', 'SEUL_SOUHAIT_PARTENAIRE', 'COMMUN'], + { + message: 'Veuillez sélectionner une option', + }, + ), + }); + + if (declarationType !== 'SEUL_SOUHAIT_PARTENAIRE') { + schema = schema.extend({ + formattedTaxableIncome: z + .string(questions.formattedTaxableIncome.errorMessage) + .refine((value) => { + return formattedThousandsToNumber(value) > 0; + }, questions.formattedTaxableIncome.errorMessage), + }); + } else { + schema = schema.extend({ + firstCoBuyerFormattedTaxableIncome: z + .string(questions.firstCoBuyerFormattedTaxableIncome.errorMessage) + .refine((value) => { + return formattedThousandsToNumber(value) > 0; + }, questions.firstCoBuyerFormattedTaxableIncome.errorMessage), + secondCoBuyerFormattedTaxableIncome: z + .string(questions.secondCoBuyerFormattedTaxableIncome.errorMessage) + .refine((value) => { + return formattedThousandsToNumber(value) > 0; + }, questions.secondCoBuyerFormattedTaxableIncome.errorMessage), + }); + } + } + + return schema; }); const handleSubmit = (e: SubmitEvent) => { @@ -42,18 +90,17 @@ // TODO: Define type here with DTOs (same as in step1 from acquisition simulator) const payload = { formattedTaxableIncome, + declarationType, + firstCoBuyerFormattedTaxableIncome, + secondCoBuyerFormattedTaxableIncome, }; try { - FormData.parse(payload); + formData.parse(payload); errors = {}; - eligibilitySimulatorManager.formattedTaxableIncome = - formattedTaxableIncome; - goToNextPhase(); } catch (e) { - console.log(e); errors = formatFormErrors((e as ZodError).issues); } }; @@ -64,30 +111,86 @@

{currentPhase?.title as string}

-
- {#if householdSize === 1} -
- { - eligibilitySimulatorManager.formattedTaxableIncome = formatThousands( - (e.target as HTMLInputElement).value, - ); - }} - error={errors.formattedTaxableIncome} - errorDataTestId="formatted-taxable-income-error-message" /> -
- {/if} + {#if singlePersonInHousehold} + {@render formattedTaxableIncomeInput()} + {:else} +
+ { + eligibilitySimulatorManager.firstCoBuyerFormattedTaxableIncome = + formatThousands((e.target as HTMLInputElement).value); + }} + dataTestId={questions.firstCoBuyerFormattedTaxableIncome + .inputDataTestId} + error={errors.firstCoBuyerFormattedTaxableIncome} + errorDataTestId={questions.firstCoBuyerFormattedTaxableIncome + .errorDataTestId} /> +
+
+ { + eligibilitySimulatorManager.secondCoBuyerFormattedTaxableIncome = + formatThousands((e.target as HTMLInputElement).value); + }} + dataTestId={questions.secondCoBuyerFormattedTaxableIncome + .inputDataTestId} + error={errors.secondCoBuyerFormattedTaxableIncome} + errorDataTestId={questions.secondCoBuyerFormattedTaxableIncome + .errorDataTestId} /> +
+ {/if} + {/if} + {/if} +
+ +{#snippet formattedTaxableIncomeInput()} +
+ { + eligibilitySimulatorManager.formattedTaxableIncome = formatThousands( + (e.target as HTMLInputElement).value, + ); + }} + dataTestId={questions.formattedTaxableIncome.inputDataTestId} + error={errors.formattedTaxableIncome} + errorDataTestId={questions.formattedTaxableIncome.errorDataTestId} /> +
+{/snippet} diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/HouseholdComposition.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/HouseholdComposition.svelte index fd63dd346..e5fda68ce 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/HouseholdComposition.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/HouseholdComposition.svelte @@ -16,6 +16,10 @@ const { currentPhase, householdSize, + selectedHouseholdSize, + singlePersonInHousehold, + twoToSixPersonsInHousehold, + moreThanSixPersonsInHousehold, hasDisability, dependantsAmount, birthday, @@ -26,15 +30,6 @@ } = $derived(eligibilitySimulatorManager); let errors: FormFieldError = $state({}); - let selectedHouseholdSize: number | undefined = $derived.by(() => { - if (!householdSize) { - return undefined; - } else if (householdSize < 7) { - return householdSize; - } else { - return -1; - } - }); let inputHouseholdSize: number | undefined = $derived.by(() => { if (!householdSize || (householdSize && householdSize < 7)) { @@ -44,16 +39,6 @@ } }); - let singlePersonInHousehold: boolean = $derived(selectedHouseholdSize === 1); - let twoToSixPersonsInHousehold: boolean = $derived( - selectedHouseholdSize !== undefined && - selectedHouseholdSize >= 2 && - selectedHouseholdSize <= 6, - ); - let moreThanSixPersonsInHousehold: boolean = $derived( - selectedHouseholdSize === -1, - ); - let formData = $derived.by(() => { let schema = z.object({ selectedHouseholdSize: z.number({ @@ -126,7 +111,6 @@ goToNextPhase(); } catch (e) { - console.log(e); errors = formatFormErrors((e as ZodError).issues); } }; @@ -149,10 +133,16 @@ onChange={(e) => { const { value } = e.target as HTMLSelectElement; - selectedHouseholdSize = value ? Number(value) : undefined; + if (value) { + delete errors.selectedHouseholdSize; + } + + eligibilitySimulatorManager.selectedHouseholdSize = value + ? Number(value) + : undefined; }} error={errors.selectedHouseholdSize} - errorDataTestId={questions.selectedHouseholdSize.dataTestId} /> + errorDataTestId={questions.selectedHouseholdSize.errorDataTestId} /> {#if singlePersonInHousehold} @@ -169,12 +159,16 @@ onChange={(e) => { const { value } = e.target as HTMLSelectElement; + if (value) { + delete errors.hasDisability; + } + eligibilitySimulatorManager.hasDisability = value === '' ? undefined : value === 'true' ? true : false; }} error={errors.hasDisability} errorDataTestId={questions.singlePersonInHouseholdHasDisability - .dataTestId} /> + .errorDataTestId} /> {:else if twoToSixPersonsInHousehold}
@@ -188,12 +182,16 @@ onChange={(e) => { const { value } = e.target as HTMLSelectElement; + if (value) { + delete errors.dependantsAmount; + } + eligibilitySimulatorManager.dependantsAmount = value ? Number(value) : undefined; }} error={errors.dependantsAmount} - errorDataTestId={questions.dependantsAmount.dataTestId} /> + errorDataTestId={questions.dependantsAmount.errorDataTestId} />
{ const { value } = e.target as HTMLInputElement; - console.log(value); eligibilitySimulatorManager.coBuyerBirthday = value; }} error={errors.coBuyerBirthday} - errorDataTestId={questions.coBuyerBirthday.dataTestId} /> + errorDataTestId={questions.coBuyerBirthday.errorDataTestId} />
{/if} {:else if moreThanSixPersonsInHousehold} @@ -271,7 +272,7 @@ } }} error={errors.inputHouseholdSize} - errorDataTestId={questions.inputHouseholdSize.dataTestId} /> + errorDataTestId={questions.inputHouseholdSize.errorDataTestId} /> {/if} diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/PropertySituation.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/PropertySituation.svelte index cc9032853..eaf488349 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/PropertySituation.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityDefinition/PropertySituation.svelte @@ -5,10 +5,12 @@ import Actions from '$components/common/Simulator/Actions.svelte'; import Action from '$components/common/Simulator/Action.svelte'; import Select from '$components/common/Select.svelte'; - import eligibilitySimulatorManager, { - type PropertySituation, - } from '$lib/managers/eligibility-simulator.svelte'; + import eligibilitySimulatorManager from '$lib/managers/eligibility-simulator.svelte'; import { formatFormErrors } from '$lib/utils/helpers'; + import { + questions, + type PropertySituation, + } from '$lib/utils/eligibility-simulator'; const { currentPhase, @@ -48,8 +50,6 @@ FormData.parse(payload); errors = {}; - eligibilitySimulatorManager.propertySituation = propertySituation; - goToNextPhase(); } catch (e) { errors = formatFormErrors((e as ZodError).issues); @@ -65,48 +65,24 @@
{ + eligibilitySimulatorManager.firstName = ( + e.target as HTMLInputElement + ).value; + }} + error={errors.firstName} + errorDataTestId={stepsContent.firstName.errorDataTestId} /> +
+ +
+ { + eligibilitySimulatorManager.lastName = ( + e.target as HTMLInputElement + ).value; + }} + error={errors.lastName} + errorDataTestId={stepsContent.lastName.errorDataTestId} /> +
+ +
+ { + eligibilitySimulatorManager.email = ( + e.target as HTMLInputElement + ).value; + }} + error={errors.email} + errorDataTestId={stepsContent.email.errorDataTestId} /> +
+ { if (this.currentStep.step < 2) { return null; @@ -30,7 +30,7 @@ class EligibilitySimulator { } }); - public currentPhase: Phase = $state(this.steps[0].phases[0]); + public currentPhase: Phase = $state(this.steps[1].phases[1]); public previousPhase: Phase | null = $derived.by(() => { if (this.currentPhase.phase > 1) { return this.currentStep.phases[this.currentPhase.phase - 2]; @@ -122,6 +122,11 @@ class EligibilitySimulator { return undefined; }); + // User details + public firstName: string | undefined = $state(undefined); + public lastName: string | undefined = $state(undefined); + public email: string | undefined = $state(undefined); + public goToPreviousPhase = () => { if (this.previousPhase) { const shouldGoToPreviousStep = this.currentPhase.phase === 1; diff --git a/apps/frontend/src/lib/utils/eligibility-simulator.ts b/apps/frontend/src/lib/utils/eligibility-simulator.ts index c1137d897..1c4343686 100644 --- a/apps/frontend/src/lib/utils/eligibility-simulator.ts +++ b/apps/frontend/src/lib/utils/eligibility-simulator.ts @@ -409,6 +409,21 @@ export const stepsContent = { 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', }, }, + firstName: { + label: 'Prénom', + errorDataTestId: 'input-first-name-error-message', + errorMessage: 'Veuillez saisir votre prénom', + }, + lastName: { + label: 'Nom de famille', + errorDataTestId: 'input-last-name-error-message', + errorMessage: 'Veuillez saisir votre nom de famille', + }, + email: { + label: 'Adresse email', + errorDataTestId: 'input-email-error-message', + errorMessage: 'Veuillez saisir votre adresse email', + }, }; const calculateAge = (birthDate: string) => { From 8c1442325e824a8ddebfaeaad31d51e4c4b61a3b Mon Sep 17 00:00:00 2001 From: fuuuzz Date: Wed, 11 Feb 2026 16:46:27 +0100 Subject: [PATCH 20/56] feat: complete form for user detail, and exit simulator case --- .../EligibilityResult/ResultDetails.svelte | 26 + .../EligibilityResult/UserDetails.svelte | 148 +- .../steps/Synthesis/SynthesisContent.svelte | 21 +- .../managers/eligibility-simulator.svelte.ts | 19 +- .../src/lib/utils/eligibility-simulator.ts | 13 + .../steps/+page.spec.ts | 1669 +++++++++-------- 6 files changed, 1070 insertions(+), 826 deletions(-) diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/ResultDetails.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/ResultDetails.svelte index 67d0da9a9..ac77c330e 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/ResultDetails.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/ResultDetails.svelte @@ -15,6 +15,7 @@ goToNextPhase, previousStep, goToPreviousPhase, + refuseConnection, loading, } = $derived(eligibilitySimulatorManager); @@ -37,15 +38,19 @@ {#if propertySituation === 'PROPRIETAIRE'} {@render resultAlert(stepsContent.eligibility.isOwner.title, 'error')}

{stepsContent.eligibility.isOwner.content}

+ {@render connectionCta()} {:else if eligibility?.eligibleZoneB2andC} {@render resultAlert(stepsContent.eligibility.zoneB2andC.title)}

{stepsContent.eligibility.zoneB2andC.content}

+ {@render connectionCta()} {:else if eligibility?.eligibleZoneB1} {@render resultAlert(stepsContent.eligibility.zoneB1.title)}

{stepsContent.eligibility.zoneB1.content}

+ {@render connectionCta()} {:else if eligibility?.eligibleZoneAandAbis} {@render resultAlert(stepsContent.eligibility.zoneAandAbis.title)}

{stepsContent.eligibility.zoneAandAbis.content}

+ {@render connectionCta()} {:else} {@render resultAlert( stepsContent.eligibility.notEligible.title, @@ -81,3 +86,24 @@ {/snippet} + +{#snippet connectionCta()} +
+
+

{stepsContent.connection.title}

+

+ {stepsContent.connection.connectionCtaText} +

+

+ {stepsContent.exitSimulatorText} + + . +

+
+{/snippet} diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/UserDetails.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/UserDetails.svelte index 2b0b4c655..308f66f07 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/UserDetails.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/EligibilityResult/UserDetails.svelte @@ -13,6 +13,7 @@ firstName, lastName, email, + phone, currentPhase, nextStep, goToNextPhase, @@ -25,14 +26,23 @@ let formData = z.object({ firstName: z.string({ - message: 'Veuillez saisir votre prénom', + message: stepsContent.firstName.errorMessage, }), lastName: z.string({ - message: 'Veuillez saisir votre nom', + message: stepsContent.lastName.errorMessage, }), email: z.email({ - message: 'Veuillez saisir une adresse email valide', + message: stepsContent.email.errorMessage, }), + phone: z + .string({ + message: stepsContent.phone.errorMessage, + }) + .refine((value) => { + const phoneRegex = /^(0|(\+[0-9]{2}[. -]?))[1-9]([. -]?[0-9][0-9]){4}$/; + console.log(phoneRegex.test(value)); + return phoneRegex.test(value); + }, stepsContent.phone.errorMessage), }); const handleSubmit = (e: SubmitEvent) => { @@ -42,10 +52,12 @@ firstName, lastName, email, + phone, }; try { formData.parse(payload); + eligibilitySimulatorManager.hasRefusedConnection = false; goToNextPhase(); } catch (e) { errors = formatFormErrors((e as ZodError).issues); @@ -58,64 +70,84 @@

{currentPhase?.title as string}

- -
- { - eligibilitySimulatorManager.firstName = ( - e.target as HTMLInputElement - ).value; - }} - error={errors.firstName} - errorDataTestId={stepsContent.firstName.errorDataTestId} /> -
+
+ { + eligibilitySimulatorManager.firstName = ( + e.target as HTMLInputElement + ).value; + }} + error={errors.firstName} + errorDataTestId={stepsContent.firstName.errorDataTestId} /> +
-
- { - eligibilitySimulatorManager.lastName = ( - e.target as HTMLInputElement - ).value; - }} - error={errors.lastName} - errorDataTestId={stepsContent.lastName.errorDataTestId} /> -
+
+ { + eligibilitySimulatorManager.lastName = ( + e.target as HTMLInputElement + ).value; + }} + error={errors.lastName} + errorDataTestId={stepsContent.lastName.errorDataTestId} /> +
-
- { - eligibilitySimulatorManager.email = ( - e.target as HTMLInputElement - ).value; - }} - error={errors.email} - errorDataTestId={stepsContent.email.errorDataTestId} /> -
+
+ { + eligibilitySimulatorManager.email = ( + e.target as HTMLInputElement + ).value; + }} + error={errors.email} + errorDataTestId={stepsContent.email.errorDataTestId} /> +
+ +
+ { + eligibilitySimulatorManager.phone = ( + e.target as HTMLInputElement + ).value; + }} + error={errors.phone} + errorDataTestId={stepsContent.phone.errorDataTestId} /> +
+ { + if (hasRefusedConnection) { + return steps[1].title; + } + + return previousStep?.title as string; + }); const handleSubmit = (e: SubmitEvent) => { e.preventDefault(); @@ -23,7 +36,7 @@ diff --git a/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts b/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts index bd12dd4cf..df5d10faa 100644 --- a/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts +++ b/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts @@ -14,7 +14,7 @@ import { formattedThousandsToNumber } from '$lib/utils/formatters'; class EligibilitySimulator { public steps: Step[] = steps; - public currentStep: Step = $state(this.steps[1]); + public currentStep: Step = $state(this.steps[0]); public previousStep: Step | null = $derived.by(() => { if (this.currentStep.step < 2) { return null; @@ -30,7 +30,7 @@ class EligibilitySimulator { } }); - public currentPhase: Phase = $state(this.steps[1].phases[1]); + public currentPhase: Phase = $state(this.steps[0].phases[0]); public previousPhase: Phase | null = $derived.by(() => { if (this.currentPhase.phase > 1) { return this.currentStep.phases[this.currentPhase.phase - 2]; @@ -126,9 +126,15 @@ class EligibilitySimulator { public firstName: string | undefined = $state(undefined); public lastName: string | undefined = $state(undefined); public email: string | undefined = $state(undefined); + public phone: string | undefined = $state(undefined); + public hasRefusedConnection: boolean = $state(false); public goToPreviousPhase = () => { - if (this.previousPhase) { + if (this.hasRefusedConnection) { + this.currentStep = this.steps[1]; + this.currentPhase = this.steps[1].phases[0]; + this.resetScroll(); + } else if (this.previousPhase) { const shouldGoToPreviousStep = this.currentPhase.phase === 1; this.currentPhase = this.previousPhase; @@ -156,6 +162,13 @@ class EligibilitySimulator { } }; + public refuseConnection = () => { + this.currentStep = this.steps[3]; + this.currentPhase = this.steps[3].phases[0]; + this.hasRefusedConnection = true; + this.resetScroll(); + }; + private resetScroll = () => { if (browser) { document.getElementById('simulateur')?.scrollIntoView({ diff --git a/apps/frontend/src/lib/utils/eligibility-simulator.ts b/apps/frontend/src/lib/utils/eligibility-simulator.ts index 1c4343686..fe7c27a0c 100644 --- a/apps/frontend/src/lib/utils/eligibility-simulator.ts +++ b/apps/frontend/src/lib/utils/eligibility-simulator.ts @@ -424,6 +424,19 @@ export const stepsContent = { errorDataTestId: 'input-email-error-message', errorMessage: 'Veuillez saisir votre adresse email', }, + phone: { + label: 'Numéro de téléphone', + errorDataTestId: 'input-phone-error-message', + errorMessage: + 'Veuillez saisir un numéro de téléphone valide. Le format attendu est: +33122334455', + }, + connection: { + title: 'Mise en relation', + connectionCtaText: + 'Si vous le souhaitez, nous pouvons vous mettre en relation avec les professionnels du Bail Réel Solidaire. Pour cela, vous pouvez cliquer sur le bouton "Étape suivante" ci-dessous et renseigner vos informations personnelles.', + }, + exitSimulatorText: + 'Si vous ne souhaitez pas continuer, vous pouvez quitter le simulateur en cliquant ', }; const calculateAge = (birthDate: string) => { diff --git a/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts b/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts index 98dd9ac0d..8fa466bfd 100644 --- a/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts +++ b/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts @@ -11,6 +11,7 @@ test.describe('Eligibility simulator', () => { let stepTitle: Locator; let phaseTitle: Locator; let submitButton: Locator; + let previousPhaseButton: Locator; // EligibilityDefinition let householdSizeSelect: Locator; @@ -44,6 +45,17 @@ test.describe('Eligibility simulator', () => { // Résultat d'éligibilité let resultText: Locator; + let refuseConnectionLink: Locator; + + // Informations personnelles + let firstNameInput: Locator; + let firstNameErrorMessage: Locator; + let lastNameInput: Locator; + let lastNameErrorMessage: Locator; + let emailInput: Locator; + let emailErrorMessage: Locator; + let phoneInput: Locator; + let phoneErrorMessage: Locator; test.beforeEach(async ({ page }) => { await page.goto('/simulateur-eligibilite/steps'); @@ -54,6 +66,9 @@ test.describe('Eligibility simulator', () => { submitButton = simulatorWrapper.getByRole('button', { name: /Étape suivante/i, }); + previousPhaseButton = simulatorWrapper.getByRole('button', { + name: /Étape précédente/i, + }); // EligibilityDefinition householdSizeSelect = simulatorWrapper.getByRole('combobox', { @@ -145,6 +160,35 @@ test.describe('Eligibility simulator', () => { // Résultat d'éligibilité resultText = simulatorWrapper.getByTestId('eligibility-result-text'); + refuseConnectionLink = simulatorWrapper.getByTestId( + 'refuse-connection-link', + ); + + // Informations personnelles + firstNameInput = simulatorWrapper.getByRole('textbox', { + name: stepsContent.firstName.label, + }); + firstNameErrorMessage = simulatorWrapper.getByTestId( + stepsContent.firstName.errorDataTestId, + ); + lastNameInput = simulatorWrapper.getByRole('textbox', { + name: stepsContent.lastName.label, + }); + lastNameErrorMessage = simulatorWrapper.getByTestId( + stepsContent.lastName.errorDataTestId, + ); + emailInput = simulatorWrapper.getByRole('textbox', { + name: stepsContent.email.label, + }); + emailErrorMessage = simulatorWrapper.getByTestId( + stepsContent.email.errorDataTestId, + ); + phoneInput = simulatorWrapper.getByRole('textbox', { + name: stepsContent.phone.label, + }); + phoneErrorMessage = simulatorWrapper.getByTestId( + stepsContent.phone.errorDataTestId, + ); }); const performHouseholdComposition = async ( @@ -234,6 +278,39 @@ test.describe('Eligibility simulator', () => { await submitButton.click(); }; + const performResultDetails = async () => { + await submitButton.click(); + }; + + const performRefuseConnection = async () => { + await refuseConnectionLink.click(); + }; + + const performUserDetails = async ( + firstName?: string, + lastName?: string, + email?: string, + phone?: string, + ) => { + if (typeof firstName === 'string') { + await firstNameInput.fill(firstName); + } + + if (typeof lastName === 'string') { + await lastNameInput.fill(lastName); + } + + if (typeof email === 'string') { + await emailInput.fill(email); + } + + if (typeof phone === 'string') { + await phoneInput.fill(phone); + } + + await submitButton.click(); + }; + test.describe('Définir mon éligibilité', () => { test.describe('Composition de mon foyer', () => { test.describe("Cas d'erreur du formulaire", () => { @@ -520,875 +597,945 @@ test.describe('Eligibility simulator', () => { }); test.describe("Résultat d'éligibilité", () => { - const validateStepAndPhaseTitles = () => { - expect(stepTitle).toHaveText( - `2. ${steps[1].title} Étape 2 sur ${steps.length}`, - ); - expect(phaseTitle).toHaveText(`${steps[1].phases[0].title}`); - }; - - test.describe('Personne seule sans handicap', () => { - test.beforeEach(async () => { - await performHouseholdComposition(1, false); - }); - - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('25000'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + test.describe('Détails du résultat', () => { + const validateStepAndPhaseTitles = () => { + expect(stepTitle).toHaveText( + `2. ${steps[1].title} Étape 2 sur ${steps.length}`, ); - }); + expect(phaseTitle).toHaveText(`${steps[1].phases[0].title}`); + }; - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('35000'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('Personne seule sans handicap', () => { + test.beforeEach(async () => { + await performHouseholdComposition(1, false); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('100000'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('25000'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('Personne seule en situation de handicap', () => { - test.beforeEach(async () => { - await performHouseholdComposition(1, true); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('35000'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('25000'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); + test('Devrait être inéligible', async () => { + await performFiscalRevenues('100000'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('50000'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('Personne seule en situation de handicap', () => { + test.beforeEach(async () => { + await performHouseholdComposition(1, true); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('100000'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('25000'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('2 personnes ne comportant aucune personne à charge hors jeunes ménages ayant déclaré en commun', () => { - test.beforeEach(async () => { - await performHouseholdComposition( - 2, - false, - 0, - '1950-01-01', - '1950-01-01', - ); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('50000'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('25000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); + test('Devrait être inéligible', async () => { + await performFiscalRevenues('100000'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('50000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('2 personnes ne comportant aucune personne à charge hors jeunes ménages ayant déclaré en commun', () => { + test.beforeEach(async () => { + await performHouseholdComposition( + 2, + false, + 0, + '1950-01-01', + '1950-01-01', + ); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('100000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('25000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('2 personnes ne comportant aucune personne à charge hors jeunes ménages ayant déclaré en "Seul·e et vous souhaitez et vous souhaitez acheter avec un·e partenaire"', () => { - test.beforeEach(async () => { - await performHouseholdComposition( - 2, - false, - 0, - '1950-01-01', - '1950-01-01', - ); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('50000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues( - undefined, - 'SEUL_SOUHAIT_PARTENAIRE', - '20000', - '20000', - ); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); + test('Devrait être inéligible', async () => { + await performFiscalRevenues('100000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues( - undefined, - 'SEUL_SOUHAIT_PARTENAIRE', - '30000', - '20000', - ); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('2 personnes ne comportant aucune personne à charge hors jeunes ménages ayant déclaré en "Seul·e et vous souhaitez et vous souhaitez acheter avec un·e partenaire"', () => { + test.beforeEach(async () => { + await performHouseholdComposition( + 2, + false, + 0, + '1950-01-01', + '1950-01-01', + ); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues( - undefined, - 'SEUL_SOUHAIT_PARTENAIRE', - '50000', - '50000', - ); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues( + undefined, + 'SEUL_SOUHAIT_PARTENAIRE', + '20000', + '20000', + ); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('3 personnes, les co-acquéreurs ayant déclaré en commun', () => { - test.beforeEach(async () => { - await performHouseholdComposition(3, false, 1); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues( + undefined, + 'SEUL_SOUHAIT_PARTENAIRE', + '30000', + '20000', + ); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('50000', 'COMMUN'); - await performPropertySituation('AUTRE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); + test('Devrait être inéligible', async () => { + await performFiscalRevenues( + undefined, + 'SEUL_SOUHAIT_PARTENAIRE', + '50000', + '50000', + ); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('68000', 'COMMUN'); - await performPropertySituation('HEBERGE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('3 personnes, les co-acquéreurs ayant déclaré en commun', () => { + test.beforeEach(async () => { + await performHouseholdComposition(3, false, 1); + }); - test('Devrait être éligible dans les zones A et Abis', async () => { - await performFiscalRevenues('75000', 'COMMUN'); - await performPropertySituation('HEBERGE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('50000', 'COMMUN'); + await performPropertySituation('AUTRE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('100000', 'COMMUN'); - await performPropertySituation('HEBERGE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('68000', 'COMMUN'); + await performPropertySituation('HEBERGE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('Personne seule avec 1 personne à charge', () => { - test.beforeEach(async () => { - await performHouseholdComposition(2, false, 1); - }); + test('Devrait être éligible dans les zones A et Abis', async () => { + await performFiscalRevenues('75000', 'COMMUN'); + await performPropertySituation('HEBERGE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('50000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('AUTRE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); + test('Devrait être inéligible', async () => { + await performFiscalRevenues('100000', 'COMMUN'); + await performPropertySituation('HEBERGE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('68000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('HEBERGE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('Personne seule avec 1 personne à charge', () => { + test.beforeEach(async () => { + await performHouseholdComposition(2, false, 1); + }); - test('Devrait être éligible dans les zones A et Abis', async () => { - await performFiscalRevenues('75000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('HEBERGE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('50000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('AUTRE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('100000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('HEBERGE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('68000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('HEBERGE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('Jeune ménage, où la somme des ages des co-acquéreurs ne dépasse pas 55 ans', () => { - test.beforeEach(async () => { - await performHouseholdComposition( - 2, - false, - 0, - '2000-01-01', - '2000-01-01', - ); - }); + test('Devrait être éligible dans les zones A et Abis', async () => { + await performFiscalRevenues('75000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('HEBERGE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('50000', 'COMMUN'); - await performPropertySituation('AUTRE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); + test('Devrait être inéligible', async () => { + await performFiscalRevenues('100000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('HEBERGE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('68000', 'COMMUN'); - await performPropertySituation('HEBERGE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('Jeune ménage, où la somme des ages des co-acquéreurs ne dépasse pas 55 ans', () => { + test.beforeEach(async () => { + await performHouseholdComposition( + 2, + false, + 0, + '2000-01-01', + '2000-01-01', + ); + }); - test('Devrait être éligible dans les zones A et Abis', async () => { - await performFiscalRevenues('75000', 'COMMUN'); - await performPropertySituation('HEBERGE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('50000', 'COMMUN'); + await performPropertySituation('AUTRE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('100000', 'COMMUN'); - await performPropertySituation('HEBERGE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('68000', 'COMMUN'); + await performPropertySituation('HEBERGE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('2 personnes dont au moins 1 est en situation de handicap', () => { - test.beforeEach(async () => { - await performHouseholdComposition( - 2, - true, - 0, - '2000-01-01', - '2000-01-01', - ); - }); + test('Devrait être éligible dans les zones A et Abis', async () => { + await performFiscalRevenues('75000', 'COMMUN'); + await performPropertySituation('HEBERGE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('50000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); + test('Devrait être inéligible', async () => { + await performFiscalRevenues('100000', 'COMMUN'); + await performPropertySituation('HEBERGE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('68000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('2 personnes dont au moins 1 est en situation de handicap', () => { + test.beforeEach(async () => { + await performHouseholdComposition( + 2, + true, + 0, + '2000-01-01', + '2000-01-01', + ); + }); - test('Devrait être éligible dans les zones A et Abis', async () => { - await performFiscalRevenues('75000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('50000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('100000', 'COMMUN'); - await performPropertySituation('HEBERGE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('68000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); + + test('Devrait être éligible dans les zones A et Abis', async () => { + await performFiscalRevenues('75000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('4 personnes', () => { - test.beforeEach(async () => { - await performHouseholdComposition(4, false, 2); + test('Devrait être inéligible', async () => { + await performFiscalRevenues('100000', 'COMMUN'); + await performPropertySituation('HEBERGE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues( - undefined, - 'SEUL_SOUHAIT_PARTENAIRE', - '30000', - '30000', - ); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('4 personnes', () => { + test.beforeEach(async () => { + await performHouseholdComposition(4, false, 2); + }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues( - undefined, - 'SEUL_SOUHAIT_PARTENAIRE', - '40000', - '40000', - ); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues( + undefined, + 'SEUL_SOUHAIT_PARTENAIRE', + '30000', + '30000', + ); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans les zones A et Abis', async () => { - await performFiscalRevenues( - undefined, - 'SEUL_SOUHAIT_PARTENAIRE', - '44000', - '44000', - ); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues( + undefined, + 'SEUL_SOUHAIT_PARTENAIRE', + '40000', + '40000', + ); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues( - undefined, - 'SEUL_SOUHAIT_PARTENAIRE', - '50000', - '50000', - ); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans les zones A et Abis', async () => { + await performFiscalRevenues( + undefined, + 'SEUL_SOUHAIT_PARTENAIRE', + '44000', + '44000', + ); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('Personne seule avec 2 personne à charge', () => { - test.beforeEach(async () => { - await performHouseholdComposition(3, false, 2); + test('Devrait être inéligible', async () => { + await performFiscalRevenues( + undefined, + 'SEUL_SOUHAIT_PARTENAIRE', + '50000', + '50000', + ); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('60000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('Personne seule avec 2 personne à charge', () => { + test.beforeEach(async () => { + await performHouseholdComposition(3, false, 2); + }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('68000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('60000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans les zones A et Abis', async () => { - await performFiscalRevenues('90000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('68000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('100000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans les zones A et Abis', async () => { + await performFiscalRevenues('90000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('3 personnes dont au moins 1 est en situation de handicap', () => { - test.beforeEach(async () => { - await performHouseholdComposition(3, true, 1); + test('Devrait être inéligible', async () => { + await performFiscalRevenues('100000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('60000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('3 personnes dont au moins 1 est en situation de handicap', () => { + test.beforeEach(async () => { + await performHouseholdComposition(3, true, 1); + }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('68000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('60000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans les zones A et Abis', async () => { - await performFiscalRevenues('90000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('68000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('100000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans les zones A et Abis', async () => { + await performFiscalRevenues('90000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('5 personnes', () => { - test.beforeEach(async () => { - await performHouseholdComposition(5, false, 2); + test('Devrait être inéligible', async () => { + await performFiscalRevenues('100000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('60000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('5 personnes', () => { + test.beforeEach(async () => { + await performHouseholdComposition(5, false, 2); + }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('90000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('60000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans les zones A et Abis', async () => { - await performFiscalRevenues('100000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('90000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('110000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans les zones A et Abis', async () => { + await performFiscalRevenues('100000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('Personne seule avec 3 personne à charge', () => { - test.beforeEach(async () => { - await performHouseholdComposition(4, false, 3); + test('Devrait être inéligible', async () => { + await performFiscalRevenues('110000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('60000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('Personne seule avec 3 personne à charge', () => { + test.beforeEach(async () => { + await performHouseholdComposition(4, false, 3); + }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('90000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('60000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans les zones A et Abis', async () => { - await performFiscalRevenues('100000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('90000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('110000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans les zones A et Abis', async () => { + await performFiscalRevenues('100000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('4 personnes dont au moins 1 est en situation de handicap', () => { - test.beforeEach(async () => { - await performHouseholdComposition(4, true, 1); + test('Devrait être inéligible', async () => { + await performFiscalRevenues('110000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('60000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('4 personnes dont au moins 1 est en situation de handicap', () => { + test.beforeEach(async () => { + await performHouseholdComposition(4, true, 1); + }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('90000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('60000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans les zones A et Abis', async () => { - await performFiscalRevenues('100000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('90000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('110000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans les zones A et Abis', async () => { + await performFiscalRevenues('100000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('6 personnes', () => { - test.beforeEach(async () => { - await performHouseholdComposition(6, false, 2); + test('Devrait être inéligible', async () => { + await performFiscalRevenues('110000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('80000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('6 personnes', () => { + test.beforeEach(async () => { + await performHouseholdComposition(6, false, 2); + }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('110000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('80000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans les zones A et Abis', async () => { - await performFiscalRevenues('121000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('110000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('130000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans les zones A et Abis', async () => { + await performFiscalRevenues('121000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('Personne seule avec 4 personne à charge', () => { - test.beforeEach(async () => { - await performHouseholdComposition(5, false, 4); + test('Devrait être inéligible', async () => { + await performFiscalRevenues('130000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('80000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('Personne seule avec 4 personne à charge', () => { + test.beforeEach(async () => { + await performHouseholdComposition(5, false, 4); + }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('110000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('80000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans les zones A et Abis', async () => { - await performFiscalRevenues('121000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('110000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('130000', 'SEUL_SOUHAIT_SEUL'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans les zones A et Abis', async () => { + await performFiscalRevenues('121000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('5 personnes dont au moins 1 est en situation de handicap', () => { - test.beforeEach(async () => { - await performHouseholdComposition(5, true, 1); + test('Devrait être inéligible', async () => { + await performFiscalRevenues('130000', 'SEUL_SOUHAIT_SEUL'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('80000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('5 personnes dont au moins 1 est en situation de handicap', () => { + test.beforeEach(async () => { + await performHouseholdComposition(5, true, 1); + }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('110000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('80000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être éligible dans les zones A et Abis', async () => { - await performFiscalRevenues('121000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), - ); - }); + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('110000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB1.title.replace(/<[^>]*>/g, ''), + ); + }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('130000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); - }); - }); + test('Devrait être éligible dans les zones A et Abis', async () => { + await performFiscalRevenues('121000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), + ); + }); - test.describe('12 personnes', () => { - test.beforeEach(async () => { - await performHouseholdComposition( - -1, - undefined, - undefined, - undefined, - undefined, - '12', - ); + test('Devrait être inéligible', async () => { + await performFiscalRevenues('130000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans toutes les zones', async () => { - await performFiscalRevenues('40000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), - ); + test.describe('12 personnes', () => { + test.beforeEach(async () => { + await performHouseholdComposition( + -1, + undefined, + undefined, + undefined, + undefined, + '12', + ); + }); + + test('Devrait être éligible dans toutes les zones', async () => { + await performFiscalRevenues('40000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneB2andC.title.replace(/<[^>]*>/g, ''), + ); + }); + + test('Devrait être éligible dans les zones A, Abis et B1', async () => { + await performFiscalRevenues('170000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_PRIVE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + }); + + test('Devrait être éligible dans les zones A et Abis', async () => { + await performFiscalRevenues('200000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), + ); + }); + + test('Devrait être inéligible', async () => { + await performFiscalRevenues('210000', 'COMMUN'); + await performPropertySituation('LOCATAIRE_SOCIAL'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), + ); + }); }); - test('Devrait être éligible dans les zones A, Abis et B1', async () => { - await performFiscalRevenues('170000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_PRIVE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); + test.describe('Personne seule propriétaire', () => { + test('Devrait être inéligible', async () => { + await performHouseholdComposition(1, false); + await performFiscalRevenues('25000'); + await performPropertySituation('PROPRIETAIRE'); + validateStepAndPhaseTitles(); + expect(resultText).toBeVisible(); + expect(resultText).toHaveText( + stepsContent.eligibility.isOwner.title.replace(/<[^>]*>/g, ''), + ); + }); }); + }); - test('Devrait être éligible dans les zones A et Abis', async () => { - await performFiscalRevenues('200000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.zoneAandAbis.title.replace(/<[^>]*>/g, ''), - ); + test.describe('Informations personnelles', () => { + test.describe("Cas d'erreur du formulaire", () => { + test('Soumettre sans prénom, sans nom de famille, sans adresse email ou sans numéro de téléphone', async () => { + await performHouseholdComposition(1, false); + await performFiscalRevenues('25000'); + await performPropertySituation('LOCATAIRE_PRIVE'); + await performResultDetails(); + await performUserDetails(); + expect(firstNameErrorMessage).toBeVisible(); + expect(firstNameErrorMessage).toHaveText( + stepsContent.firstName.errorMessage, + ); + expect(lastNameErrorMessage).toBeVisible(); + expect(lastNameErrorMessage).toHaveText( + stepsContent.lastName.errorMessage, + ); + expect(emailErrorMessage).toBeVisible(); + expect(emailErrorMessage).toHaveText(stepsContent.email.errorMessage); + expect(phoneErrorMessage).toBeVisible(); + expect(phoneErrorMessage).toHaveText(stepsContent.phone.errorMessage); + }); }); - test('Devrait être inéligible', async () => { - await performFiscalRevenues('210000', 'COMMUN'); - await performPropertySituation('LOCATAIRE_SOCIAL'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.notEligible.title.replace(/<[^>]*>/g, ''), - ); + test.describe('Scenarii valides', () => { + const validateStepAndPhaseTitles = () => { + expect(stepTitle).toHaveText( + `2. ${steps[1].title} Étape 2 sur ${steps.length}`, + ); + expect(phaseTitle).toHaveText(`${steps[1].phases[1].title}`); + }; + + test.afterEach(async () => { + expect(stepTitle).toHaveText( + `3. ${steps[2].title} Étape 3 sur ${steps.length}`, + ); + expect(phaseTitle).toHaveText(`${steps[2].phases[0].title}`); + }); + + test('Soumettre avec prénom, nom de famille, adresse email et numéro de téléphone', async () => { + await performHouseholdComposition(1, false); + await performFiscalRevenues('25000'); + await performPropertySituation('LOCATAIRE_PRIVE'); + await performResultDetails(); + validateStepAndPhaseTitles(); + await performUserDetails( + 'Gandalf', + 'Le Gris', + 'gandalf.legris@example.com', + '+33122334455', + ); + }); }); }); + }); - test.describe('Personne seule propriétaire', () => { - test('Devrait être inéligible', async () => { - await performHouseholdComposition(1, false); - await performFiscalRevenues('25000'); - await performPropertySituation('PROPRIETAIRE'); - validateStepAndPhaseTitles(); - expect(resultText).toBeVisible(); - expect(resultText).toHaveText( - stepsContent.eligibility.isOwner.title.replace(/<[^>]*>/g, ''), - ); - }); + test.describe('Synthèse', () => { + test("L'utilisateur obtient son résultat d'éligibilité et refuse la mise en relation", async () => { + await performHouseholdComposition(1, false); + await performFiscalRevenues('25000'); + await performPropertySituation('LOCATAIRE_PRIVE'); + await performRefuseConnection(); + expect(stepTitle).toHaveText( + `4. ${steps[3].title} Étape 4 sur ${steps.length}`, + ); + expect(phaseTitle).toHaveText(`${steps[3].phases[0].title}`); + expect(previousPhaseButton).toContainText(`${steps[1].title}`); }); }); }); From f2c67975ca67331b08989d3c084b8633ab842c3d Mon Sep 17 00:00:00 2001 From: fuuuzz Date: Wed, 11 Feb 2026 17:48:09 +0100 Subject: [PATCH 21/56] feat: housing informations screen --- .../lib/components/common/Autocomplete.svelte | 11 +- .../HousingInformations.svelte | 80 +++++++++- .../managers/eligibility-simulator.svelte.ts | 8 + .../src/lib/utils/eligibility-simulator.ts | 40 +++++ .../steps/+page.spec.ts | 141 +++++++++++++++++- 5 files changed, 275 insertions(+), 5 deletions(-) diff --git a/apps/frontend/src/lib/components/common/Autocomplete.svelte b/apps/frontend/src/lib/components/common/Autocomplete.svelte index 41d2b10c6..431e83c8c 100644 --- a/apps/frontend/src/lib/components/common/Autocomplete.svelte +++ b/apps/frontend/src/lib/components/common/Autocomplete.svelte @@ -16,6 +16,8 @@ label: string; placeholder: string; error?: string; + dataTestId?: string; + errorDataTestId?: string; onSelect: (suggestion: GeocodedResponse['properties']) => void; }; @@ -25,6 +27,8 @@ placeholder, error, onSelect, + dataTestId, + errorDataTestId, }: Props = $props(); let suggestions = $state(null); @@ -155,7 +159,9 @@ }} forceNoMarginBottom onChange={handleChange} - onKeydown={handleKeydown} /> + onKeydown={handleKeydown} + {dataTestId} + {errorDataTestId} /> {#if isListExpanded}
handleSelect(suggestion?.id)}> + onclick={() => handleSelect(suggestion?.id)} + data-testid={`suggestion-${index}-data-test-id`}> {getGeocodedResponseLabel(suggestion)} {/snippet} diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/HousingInformations.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/HousingInformations.svelte index 7b1a7f34c..4b3fe62e4 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/HousingInformations.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/HousingInformations.svelte @@ -3,8 +3,23 @@ import Form from '$components/common/Simulator/Form.svelte'; import Actions from '$components/common/Simulator/Actions.svelte'; import Action from '$components/common/Simulator/Action.svelte'; + import Autocomplete from '$components/common/Autocomplete.svelte'; + import type { + FormFieldError, + GeocodedResponse, + } from '$lib/utils/definitions'; + import { formatFormErrors } from '$lib/utils/helpers'; + import z, { ZodError } from 'zod'; + import { + stepsContent, + type HousingType, + } from '$lib/utils/eligibility-simulator'; + import Select from '$components/common/Select.svelte'; - const { + let { + autocompleteValue, + selectedLocation, + housingType, currentPhase, nextPhase, goToNextPhase, @@ -13,10 +28,40 @@ loading, } = $derived(eligibilitySimulatorManager); + let errors = $state({}); + + const FormData = z.object({ + autocompleteValue: z + .string({ + message: stepsContent.location.errorMessage, + }) + .min(3, stepsContent.location.errorMessage), + housingType: z.enum(['T1', 'T2', 'T3', 'T4', 'T5'], { + message: stepsContent.housingType.errorMessage, + }), + }); + + const onLocationSelect = async ( + suggestion: GeocodedResponse['properties'], + ) => { + eligibilitySimulatorManager.selectedLocation = suggestion; + }; + const handleSubmit = (e: SubmitEvent) => { e.preventDefault(); - goToNextPhase(); + const payload = { + autocompleteValue: selectedLocation?.label, + housingType, + }; + + try { + FormData.parse(payload); + errors = {}; + goToNextPhase(); + } catch (e) { + errors = formatFormErrors((e as ZodError).issues); + } }; @@ -25,6 +70,37 @@

{currentPhase?.title as string}

+ +
+ +
+ +
+ { + eligibilitySimulatorManager.formattedContribution = formatThousands( + (e.target as HTMLInputElement).value, + ); + }} + dataTestId={stepsContent.contribution.inputDataTestId} + error={errors.formattedContribution} + errorDataTestId={stepsContent.contribution.errorDataTestId} /> +
+ +
+ { + eligibilitySimulatorManager.formattedResources = formatThousands( + (e.target as HTMLInputElement).value, + ); + }} + dataTestId={stepsContent.resources.inputDataTestId} + error={errors.formattedResources} + errorDataTestId={stepsContent.resources.errorDataTestId} /> +
diff --git a/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts b/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts index a39bd16c3..755b1c302 100644 --- a/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts +++ b/apps/frontend/src/lib/managers/eligibility-simulator.svelte.ts @@ -137,6 +137,24 @@ class EligibilitySimulator { public autocompleteValue = $derived(this.selectedLocation?.label || ''); public housingType: HousingType | undefined = $state(undefined); + // Financial informations + public formattedContribution: string | undefined = $state(undefined); + public formattedResources: string | undefined = $state(undefined); + public contribution: number | undefined = $derived.by(() => { + if (this.formattedContribution) { + return formattedThousandsToNumber(this.formattedContribution); + } + + return undefined; + }); + public resources: number | undefined = $derived.by(() => { + if (this.formattedResources) { + return formattedThousandsToNumber(this.formattedResources); + } + + return undefined; + }); + public goToPreviousPhase = () => { if (this.hasRefusedConnection) { this.currentStep = this.steps[1]; diff --git a/apps/frontend/src/lib/utils/eligibility-simulator.ts b/apps/frontend/src/lib/utils/eligibility-simulator.ts index f292309cc..c112cdbf3 100644 --- a/apps/frontend/src/lib/utils/eligibility-simulator.ts +++ b/apps/frontend/src/lib/utils/eligibility-simulator.ts @@ -477,6 +477,19 @@ export const stepsContent = { ], errorMessage: 'Veuillez sélectionner une option', }, + contribution: { + label: 'Quel est le montant de votre apport ?', + errorDataTestId: 'input-contribution-error-message', + errorMessage: 'Veuillez saisir un chiffre supérieur à 0.', + inputDataTestId: 'input-contribution', + }, + resources: { + label: + 'Quelle est la somme des salaires nets mensuels et autres ressources mensuelles que touchent chaque personnes de votre ménage ?', + errorDataTestId: 'input-resources-error-message', + errorMessage: 'Veuillez saisir un chiffre supérieur à 0.', + inputDataTestId: 'input-resources', + }, }; const calculateAge = (birthDate: string) => { diff --git a/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts b/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts index 6f56ab5bd..ca40eac96 100644 --- a/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts +++ b/apps/frontend/tests/e2e/simulateur-eligibilite/steps/+page.spec.ts @@ -61,10 +61,15 @@ test.describe('Eligibility simulator', () => { // Informations sur le logement let locationInput: Locator; let locationErrorMessage: Locator; - let housingTypeSelect: Locator; let housingTypeErrorMessage: Locator; + // Informations financières + let contributionInput: Locator; + let contributionErrorMessage: Locator; + let resourcesInput: Locator; + let resourcesErrorMessage: Locator; + test.beforeEach(async ({ page }) => { await page.goto('/simulateur-eligibilite/steps'); @@ -211,6 +216,20 @@ test.describe('Eligibility simulator', () => { housingTypeErrorMessage = simulatorWrapper.getByTestId( stepsContent.housingType.errorDataTestId, ); + + // Informations financières + contributionInput = simulatorWrapper.getByTestId( + stepsContent.contribution.inputDataTestId, + ); + contributionErrorMessage = simulatorWrapper.getByTestId( + stepsContent.contribution.errorDataTestId, + ); + resourcesInput = simulatorWrapper.getByTestId( + stepsContent.resources.inputDataTestId, + ); + resourcesErrorMessage = simulatorWrapper.getByTestId( + stepsContent.resources.errorDataTestId, + ); }); const performHouseholdComposition = async ( @@ -360,6 +379,21 @@ test.describe('Eligibility simulator', () => { await submitButton.click(); }; + const performFinancialInformations = async ( + contribution?: string, + resources?: string, + ) => { + if (typeof contribution === 'string') { + await contributionInput.fill(contribution); + } + + if (typeof resources === 'string') { + await resourcesInput.fill(resources); + } + + await submitButton.click(); + }; + test.describe('Définir mon éligibilité', () => { test.describe('Composition de mon foyer', () => { test.describe("Cas d'erreur du formulaire", () => { @@ -1658,9 +1692,91 @@ test.describe('Eligibility simulator', () => { }); test.describe('Informations financières', () => { - test.describe("Cas d'erreur du formulaire", () => {}); + test.describe("Cas d'erreur du formulaire", () => { + test.beforeEach(async ({ page }) => { + await performHouseholdComposition(1, false); + await performFiscalRevenues('25000'); + await performPropertySituation('LOCATAIRE_PRIVE'); + await performResultDetails(); + await performUserDetails( + 'Gandalf', + 'Le Gris', + 'gandalf.legris@example.com', + '+33122334455', + ); + await performHousingInformations( + 'Paris', + 'T1', + 'suggestion-3-data-test-id', + page, + ); + }); - test.describe('Scénarii valides', () => {}); + test('Soumettre sans apport et sans ressources', async () => { + await performFinancialInformations(); + expect(contributionErrorMessage).toBeVisible(); + expect(contributionErrorMessage).toHaveText( + stepsContent.contribution.errorMessage, + ); + expect(resourcesErrorMessage).toBeVisible(); + expect(resourcesErrorMessage).toHaveText( + stepsContent.resources.errorMessage, + ); + }); + + test('Soumettre avec apport et sans ressources', async () => { + await performFinancialInformations('10000'); + expect(resourcesErrorMessage).toBeVisible(); + expect(resourcesErrorMessage).toHaveText( + stepsContent.resources.errorMessage, + ); + }); + + test('Soumettre sans apport et avec ressources', async () => { + await performFinancialInformations(undefined, '10000'); + expect(contributionErrorMessage).toBeVisible(); + expect(contributionErrorMessage).toHaveText( + stepsContent.contribution.errorMessage, + ); + }); + }); + + test.describe('Scénarii valides', () => { + const validateStepAndPhaseTitles = () => { + expect(stepTitle).toHaveText( + `3. ${steps[2].title} Étape 3 sur ${steps.length}`, + ); + expect(phaseTitle).toHaveText(`${steps[2].phases[1].title}`); + }; + + test.afterEach(async () => { + expect(stepTitle).toHaveText( + `3. ${steps[2].title} Étape 3 sur ${steps.length}`, + ); + expect(phaseTitle).toHaveText(`${steps[2].phases[2].title}`); + }); + + test('Soumettre avec apport et avec ressources', async ({ page }) => { + await performHouseholdComposition(1, false); + await performFiscalRevenues('25000'); + await performPropertySituation('LOCATAIRE_PRIVE'); + await performResultDetails(); + await performUserDetails( + 'Gandalf', + 'Le Gris', + 'gandalf.legris@example.com', + '+33122334455', + ); + await performHousingInformations( + 'Paris', + 'T1', + 'suggestion-3-data-test-id', + page, + ); + validateStepAndPhaseTitles(); + await performFinancialInformations('10000', '10000'); + }); + }); }); }); From 2f02673a93c7b6ea063156208ca2f8431cf72b2c Mon Sep 17 00:00:00 2001 From: fuuuzz Date: Thu, 12 Feb 2026 16:12:50 +0100 Subject: [PATCH 23/56] feat: user search screen --- .../AdditionalInformations.svelte | 409 +++++++++++++++- .../managers/eligibility-simulator.svelte.ts | 16 + .../src/lib/utils/eligibility-simulator.ts | 220 +++++++++ .../steps/+page.spec.ts | 458 ++++++++++++++++++ 4 files changed, 1102 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/AdditionalInformations.svelte b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/AdditionalInformations.svelte index ab3862962..26433d94b 100644 --- a/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/AdditionalInformations.svelte +++ b/apps/frontend/src/lib/components/pages/simulateur-eligibilite/steps/SearchInformations/AdditionalInformations.svelte @@ -3,9 +3,30 @@ import Form from '$components/common/Simulator/Form.svelte'; import Actions from '$components/common/Simulator/Actions.svelte'; import Action from '$components/common/Simulator/Action.svelte'; + import z, { ZodError } from 'zod'; + import { + stepsContent, + type EmploymentStatus, + type PositionType, + type ContractType, + } from '$lib/utils/eligibility-simulator'; + import type { FormFieldError } from '$lib/utils/definitions'; + import { formatFormErrors } from '$lib/utils/helpers'; + import Select from '$components/common/Select.svelte'; + import Input from '$components/common/Input.svelte'; const { currentPhase, + hadBrsKnowledge, + employmentStatus, + laposteEmployer, + canSendInformationsToLaposte, + positionType, + positionStage, + hasCompanyMoreThan10Employees, + hasCompanyMoreThan50Employees, + allowFinancingAndOwnershipAdvices, + positionContractType, nextStep, goToNextPhase, previousPhase, @@ -13,10 +34,122 @@ loading, } = $derived(eligibilitySimulatorManager); + let errors: FormFieldError = $state({}); + + let formData = $derived.by(() => { + let schema = z.object({ + hadBrsKnowledge: z.boolean({ + message: stepsContent.hadBrsKnowledge.errorMessage, + }), + employmentStatus: z.enum( + [ + 'SALARIE_PRIVE_NON_AGRICOLE', + 'SALARIE_AGRICOLE', + 'SALARIE_PUBLIC_OU_FONCTIONNAIRE', + 'INDEPENDANT', + 'SALARIE_GROUPE_LA_POSTE', + 'SANS_ACTIVITE_PROFESSIONNELLE', + 'RETRAITE', + ], + { + message: stepsContent.employmentStatus.errorMessage, + }, + ), + }); + + const publicSectorSchema = { + positionType: z.enum( + ['CADRE', 'NON_CADRE', 'NO_CATEGORIE_PROFESSIONNELLE'], + { + message: stepsContent.positionType.errorMessage, + }, + ), + positionStage: z.boolean({ + message: stepsContent.positionStage.errorMessage, + }), + }; + + if (employmentStatus === 'SALARIE_GROUPE_LA_POSTE') { + schema = schema.extend({ + laposteEmployer: z + .string({ + message: stepsContent.laposteEmployer.errorMessage, + }) + .refine((value) => { + console.log(value); + return value.length > 0; + }, stepsContent.laposteEmployer.errorMessage), + canSendInformationsToLaposte: z.boolean({ + message: stepsContent.canSendInformationsToLaposte.errorMessage, + }), + ...publicSectorSchema, + }); + } else if (employmentStatus === 'SALARIE_PUBLIC_OU_FONCTIONNAIRE') { + schema = schema.extend(publicSectorSchema); + } else if (employmentStatus === 'SALARIE_PRIVE_NON_AGRICOLE') { + schema = schema.extend({ + hasCompanyMoreThan10Employees: z.boolean({ + message: stepsContent.hasCompanyMoreThan10Employees.errorMessage, + }), + positionContractType: z.enum(['CDI', 'CDD'], { + message: stepsContent.positionContractType.errorMessage, + }), + }); + + if (hasCompanyMoreThan10Employees) { + schema = schema.extend({ + allowFinancingAndOwnershipAdvices: z.boolean({ + message: + stepsContent.allowFinancingAndOwnershipAdvices.errorMessage, + }), + }); + } + } else if (employmentStatus === 'SALARIE_AGRICOLE') { + schema = schema.extend({ + hasCompanyMoreThan50Employees: z.boolean({ + message: stepsContent.hasCompanyMoreThan50Employees.errorMessage, + }), + positionContractType: z.enum(['CDI', 'CDD'], { + message: stepsContent.positionContractType.errorMessage, + }), + }); + + if (hasCompanyMoreThan50Employees) { + schema = schema.extend({ + allowFinancingAndOwnershipAdvices: z.boolean({ + message: + stepsContent.allowFinancingAndOwnershipAdvices.errorMessage, + }), + }); + } + } + + return schema; + }); + const handleSubmit = (e: SubmitEvent) => { e.preventDefault(); - goToNextPhase(); + const payload = { + hadBrsKnowledge, + employmentStatus, + laposteEmployer, + canSendInformationsToLaposte, + positionType, + positionStage, + hasCompanyMoreThan10Employees, + hasCompanyMoreThan50Employees, + allowFinancingAndOwnershipAdvices, + positionContractType, + }; + + try { + formData.parse(payload); + errors = {}; + goToNextPhase(); + } catch (e) { + errors = formatFormErrors((e as ZodError).issues); + } }; @@ -25,6 +158,60 @@

{currentPhase?.title as string}

+ +
+ ({ + ...stepContent, + selected: employmentStatus === stepContent.value, + }))} + onChange={(e) => { + const { value } = e.target as HTMLSelectElement; + + if (value) { + delete errors.employmentStatus; + } + + eligibilitySimulatorManager.employmentStatus = + value as EmploymentStatus; + }} + error={errors.employmentStatus} + errorDataTestId={stepsContent.employmentStatus.errorDataTestId} /> +
+ + {#if employmentStatus === 'SALARIE_GROUPE_LA_POSTE'} + {@render laPoste()} + {:else if employmentStatus === 'SALARIE_PUBLIC_OU_FONCTIONNAIRE'} + {@render publicSector()} + {:else if employmentStatus === 'SALARIE_PRIVE_NON_AGRICOLE'} + {@render privateSector()} + {:else if employmentStatus === 'SALARIE_AGRICOLE'} + {@render agricoleSector()} + {/if} @@ -40,3 +227,223 @@ {loading} /> + +{#snippet laPoste()} +
+ { + eligibilitySimulatorManager.laposteEmployer = ( + e.target as HTMLInputElement + ).value; + }} + error={errors.laposteEmployer} + errorDataTestId={stepsContent.laposteEmployer.errorDataTestId} /> +
+ +
+ ({ + ...stepContent, + selected: positionType === stepContent.value, + }))} + onChange={(e) => { + const { value } = e.target as HTMLSelectElement; + + if (value) { + delete errors.positionType; + } + + eligibilitySimulatorManager.positionType = value as PositionType; + }} + error={errors.positionType} + errorDataTestId={stepsContent.positionType.errorDataTestId} /> +
+ +
+ ({ + ...stepContent, + selected: hasCompanyMoreThan10Employees === stepContent.value, + }), + )} + onChange={(e) => { + const { value } = e.target as HTMLSelectElement; + + if (value) { + delete errors.hasCompanyMoreThan10Employees; + } + + eligibilitySimulatorManager.hasCompanyMoreThan10Employees = + value === '' ? undefined : value === 'true' ? true : false; + }} + error={errors.hasCompanyMoreThan10Employees} + errorDataTestId={stepsContent.hasCompanyMoreThan10Employees + .errorDataTestId} /> +
+ + {#if hasCompanyMoreThan10Employees} + {@render financingAndOwnershipAdvices()} + {/if} + + {@render positionContractTypeSnippet()} +{/snippet} + +{#snippet agricoleSector()} +
+ ({ + ...stepContent, + selected: allowFinancingAndOwnershipAdvices === stepContent.value, + }), + )} + onChange={(e) => { + const { value } = e.target as HTMLSelectElement; + + if (value) { + delete errors.allowFinancingAndOwnershipAdvices; + } + + eligibilitySimulatorManager.allowFinancingAndOwnershipAdvices = + value === '' ? undefined : value === 'true' ? true : false; + }} + error={errors.allowFinancingAndOwnershipAdvices} + errorDataTestId={stepsContent.allowFinancingAndOwnershipAdvices + .errorDataTestId} /> +
+{/snippet} + +{#snippet positionContractTypeSnippet()} +
+