From 6d41b124816502a1de2e7fd41d12e1168a23c0a1 Mon Sep 17 00:00:00 2001 From: Dorian De Rosa <57262105+Naorid@users.noreply.github.com> Date: Wed, 7 Feb 2024 09:23:36 +0100 Subject: [PATCH] feat(alternance): passage recherche en ssr (#2621) * feat(alternance): passage recherche ssr * fix(jobetudiant): enleve le focus sur le premier element pour permettre le retour au scroll precedent * refacto(alternance): supprime le service alternance * refacto(alternance): supprime le controller index alternance * refacto(alternance): fix test e2e + ajout repository mock * fix: erreur build * fix: retour review * fix: tests * fix: tests e2e * test: onSubmit formulaire * fix: linter --- .env.scalingo | 2 + .env.test | 1 + cypress/e2e/apprentissage.cy.ts | 89 +++---- .../FormulaireRechercheAlternance.test.tsx | 68 +++-- .../FormulaireRechercheAlternance.tsx | 15 +- .../Rechercher/RechercherAlternance.test.tsx | 109 +++----- .../Rechercher/RechercherAlternance.tsx | 73 +++--- .../FormulaireRechercheJobEte.tsx | 1 - ...FormulaireRechercheJob\303\211tudiant.tsx" | 1 - src/client/dependencies.container.ts | 8 +- .../alternance/alternance.service.fixture.ts | 18 -- .../alternance/alternance.service.test.ts | 66 ----- .../services/alternance/alternance.service.ts | 40 --- .../api/alternances/index.controller.test.ts | 75 ------ src/pages/api/alternances/index.controller.ts | 39 --- src/pages/apprentissage/index.page.test.tsx | 238 ++++++++++++++---- src/pages/apprentissage/index.page.tsx | 84 +++++-- src/pages/emplois/index.page.test.tsx | 4 +- .../alternances/domain/alternance.fixture.ts | 16 +- .../repositories/mockAlternance.repository.ts | 30 +++ .../configuration/dependencies.container.ts | 5 +- .../repositories/mockOffre.repository.ts | 8 +- .../services/configuration.service.fixture.ts | 1 + .../services/serverConfiguration.service.ts | 2 + 24 files changed, 480 insertions(+), 513 deletions(-) delete mode 100644 src/client/services/alternance/alternance.service.fixture.ts delete mode 100644 src/client/services/alternance/alternance.service.test.ts delete mode 100644 src/client/services/alternance/alternance.service.ts delete mode 100644 src/pages/api/alternances/index.controller.test.ts delete mode 100644 src/pages/api/alternances/index.controller.ts create mode 100644 src/server/alternances/infra/repositories/mockAlternance.repository.ts diff --git a/.env.scalingo b/.env.scalingo index f8defd7d1c..b137bd88cb 100644 --- a/.env.scalingo +++ b/.env.scalingo @@ -70,12 +70,14 @@ API_IMMERSION_FACILE_STAGE_3EME_API_KEY=${API_IMMERSION_FACILE_STAGE_3EME_API_KE API_IMMERSION_FACILE_STAGE_3EME_URL=${API_IMMERSION_FACILE_STAGE_3EME_URL} API_GEO_BASE_URL=${API_GEO_BASE_URL} API_LA_BONNE_ALTERNANCE_CALLER=${API_LA_BONNE_ALTERNANCE_CALLER} +API_LA_BONNE_ALTERNANCE_IS_ALTERNANCE_MOCK_ACTIVE=${API_LA_BONNE_ALTERNANCE_IS_ALTERNANCE_MOCK_ACTIVE} API_LA_BONNE_ALTERNANCE_URL=${API_LA_BONNE_ALTERNANCE_URL} API_LES_ENTREPRISES_SENGAGENT_URL=${API_LES_ENTREPRISES_SENGAGENT_URL} API_ONISEP_BASE_URL=${API_ONISEP_BASE_URL} API_ONISEP_ACCOUNT_EMAIL=${API_ONISEP_ACCOUNT_EMAIL} API_ONISEP_ACCOUNT_PASSWORD=${API_ONISEP_ACCOUNT_PASSWORD} API_ONISEP_APPLICATION_ID=${API_ONISEP_APPLICATION_ID} +API_POLE_EMPLOI_IS_MOCK_ACTIVE=${API_POLE_EMPLOI_IS_MOCK_ACTIVE} API_POLE_EMPLOI_OFFRES_URL=${API_POLE_EMPLOI_OFFRES_URL} API_POLE_EMPLOI_REFERENTIEL_URL=${API_POLE_EMPLOI_REFERENTIEL_URL} API_TRAJECTOIRES_PRO_URL=${API_TRAJECTOIRES_PRO_URL} diff --git a/.env.test b/.env.test index 68a363473b..8d392efaaf 100644 --- a/.env.test +++ b/.env.test @@ -37,6 +37,7 @@ POLE_EMPLOI_CONNECT_URL=https://entreprise.pole-emploi.fr # API LA BONNE ALTERNANCE API_LA_BONNE_ALTERNANCE_CALLER=1jeune1solution +API_LA_BONNE_ALTERNANCE_IS_ALTERNANCE_MOCK_ACTIVE=1 API_LA_BONNE_ALTERNANCE_URL=https://labonnealternance-recette.apprentissage.beta.gouv.fr/api/ NEXT_PUBLIC_LA_BONNE_ALTERNANCE_URL=https://labonnealternance-recette.apprentissage.beta.gouv.fr/ diff --git a/cypress/e2e/apprentissage.cy.ts b/cypress/e2e/apprentissage.cy.ts index 2e46d7fd23..9b1152c46b 100644 --- a/cypress/e2e/apprentissage.cy.ts +++ b/cypress/e2e/apprentissage.cy.ts @@ -3,23 +3,24 @@ import { stringify } from 'querystring'; -import { - anAlternanceMatcha, - anAlternanceMatchaBoulanger, - anAlternancePEJobs, -} from '~/server/alternances/domain/alternance.fixture'; +import { anAlternanceFiltre } from '~/server/alternances/domain/alternance.fixture'; +import { aMetier } from '~/server/metiers/domain/metierAlternance.fixture'; -import { aMetier } from '../../src/server/metiers/domain/metierAlternance.fixture'; +import { + mockedRepositoryReturnsASuccessWhenCodeCommuneIsNot12345, +} from '../../src/server/alternances/infra/repositories/mockAlternance.repository'; import { interceptGet } from '../interceptGet'; const aQuery = { - codeCommune: '13043', - codeRomes: 'D1102, D1104', - distanceCommune: 10, - latitudeCommune: 48.859, - libelleCommune: 'Gignac-la-Nerthe (13180)', + codeCommune: '75056', + codePostal: '75001', + codeRomes: 'D1102,D1104', + distanceCommune: '10', + latitudeCommune: '48.859', + libelleCommune: 'Paris (75001)', libelleMetier: 'Boulangerie, pâtisserie, chocolaterie', - longitudeCommune: 2.347, + longitudeCommune: '2.347', + ville: 'Paris', }; describe('Parcours alternance LBA', () => { @@ -32,11 +33,6 @@ describe('Parcours alternance LBA', () => { cy.findByRole('list', { name: /Offres d’alternances/i }).should('not.exist'); }); - it('place le focus sur le premier input du formulaire de recherche', () => { - cy.visit('/apprentissage'); - cy.findByRole('combobox', { name: 'Domaine' }).should('have.focus'); - }); - describe('Quand l’utilisateur cherche un métier', () => { it('tous les métiers sont accessibles mais au maximum 10 sont visibles sans scroll', () => { const listeMetiers = new Array(11).fill(aMetier()); @@ -57,39 +53,46 @@ describe('Parcours alternance LBA', () => { describe('Quand l’utilisateur effectue une recherche', () => { it('affiche les résultats', () => { - const alternances = { - entrepriseList: [], - offreList: [anAlternanceMatcha(), anAlternanceMatchaBoulanger(), anAlternancePEJobs()], - }; - interceptGet({ - actionBeforeWaitTheCall: () => cy.visit(`/apprentissage?${stringify(aQuery)}`), - alias: 'recherche-metiers', - path: '/api/alternances?*', - response: JSON.stringify(alternances), + const filtre = anAlternanceFiltre({ + codeCommune: '75056', + codeRomes: ['D1102', 'D1104'], + distanceCommune: '10', + latitudeCommune: '48.859', + longitudeCommune: '2.347', }); + const query = { + codeCommune: '75056', + codePostal: '75001', + codeRomes: 'D1102,D1104', + distanceCommune: '10', + latitudeCommune: '48.859', + libelleCommune: 'Paris (75001)', + libelleMetier: 'Boulangerie, pâtisserie, chocolaterie', + longitudeCommune: '2.347', + ville: 'Paris', + }; + const expectedResult = mockedRepositoryReturnsASuccessWhenCodeCommuneIsNot12345(filtre); + + cy.visit(`/apprentissage?${stringify(query)}`); cy.findByRole('list', { name: /Offres d’alternances/i }) .children() - .should('have.length', 3); + .should('have.length', expectedResult?.result.offreList.length); }); }); -}); -describe("quand les paramètres de l'url ne respectent pas le schema de validation du controller", () => { - it('retourne une erreur de demande incorrecte', () => { - cy.viewport('iphone-x'); - const query = { - ...aQuery, - 'unwanted-query': 'not-allowed', - }; - - interceptGet({ - actionBeforeWaitTheCall: () => cy.visit(`/apprentissage?${stringify(query)}`), - alias: 'recherche-alternances-failed', - path:'/api/alternances?*', - response: JSON.stringify({ error: "les paramètres dans l'url ne respectent pas le schema de validation" }), - statusCode: 400, + describe('quand la recherche retourne une erreur', () => { + it('affiche l’erreur', () => { + // NOTE (DORO 02-02-2024): Query qui génère une erreur dans le repository mocké + const query = { + ...aQuery, + codeCommune: '12345', + }; + + cy.visit(`/apprentissage?${stringify(query)}`); + + cy.findByText(/Service Indisponible/i).should('be.visible'); }); - cy.findByText(/Erreur - Demande incorrecte/i).should('be.visible'); }); }); + diff --git a/src/client/components/features/Alternance/Rechercher/FormulaireRecherche/FormulaireRechercheAlternance.test.tsx b/src/client/components/features/Alternance/Rechercher/FormulaireRecherche/FormulaireRechercheAlternance.test.tsx index 471dd4457a..72459de569 100644 --- a/src/client/components/features/Alternance/Rechercher/FormulaireRecherche/FormulaireRechercheAlternance.test.tsx +++ b/src/client/components/features/Alternance/Rechercher/FormulaireRecherche/FormulaireRechercheAlternance.test.tsx @@ -13,10 +13,8 @@ import { mockUseRouter } from '~/client/components/useRouter.mock'; import { mockSmallScreen } from '~/client/components/window.mock'; import { DependenciesProvider } from '~/client/context/dependenciesContainer.context'; import { aCommuneQuery } from '~/client/hooks/useCommuneQuery'; -import { anAlternanceService } from '~/client/services/alternance/alternance.service.fixture'; import { aLocalisationService } from '~/client/services/localisation/localisation.service.fixture'; import { aMetier, aMetierService } from '~/client/services/metiers/metier.fixture'; -import { aResultatRechercherMultipleAlternance } from '~/server/alternances/domain/alternance.fixture'; import { createSuccess } from '~/server/errors/either'; import { aListeDeMetierLaBonneAlternance } from '~/server/metiers/domain/metierAlternance.fixture'; @@ -27,14 +25,12 @@ describe('FormulaireRechercheAlternance', () => { describe('quand le composant est affiché sans recherche', () => { it('affiche un formulaire pour la recherche d‘alternance, sans échantillon de résultat', async () => { // GIVEN - const alternanceService = anAlternanceService(); const localisationService = aLocalisationService(); mockUseRouter({}); // WHEN render( @@ -45,12 +41,10 @@ describe('FormulaireRechercheAlternance', () => { // THEN expect(formulaireRechercheAlternance).toBeInTheDocument(); - expect(alternanceService.rechercherAlternance).toHaveBeenCalledTimes(0); }); it('lorsque je séléctionne une commune, affiche le champ rayon', async () => { render( @@ -70,7 +64,7 @@ describe('FormulaireRechercheAlternance', () => { }); describe('lorsqu‘on recherche par localisation et par métier', () => { - it('les informations de la localisatione et du métier sont ajoutées à l’url', async () => { + it('les informations de la localisation et du métier sont ajoutées à l’url', async () => { // Given const routerPush = jest.fn(); mockUseRouter({ push: routerPush }); @@ -80,13 +74,11 @@ describe('FormulaireRechercheAlternance', () => { })]; const localisationService = aLocalisationService(); - const alternanceService = anAlternanceService(aResultatRechercherMultipleAlternance().offreList, aResultatRechercherMultipleAlternance().entrepriseList); const metierService = aMetierService(); jest.spyOn(metierService, 'rechercherMetier').mockResolvedValue(createSuccess(aMetierList)); // When render( @@ -110,15 +102,15 @@ describe('FormulaireRechercheAlternance', () => { await user.click(submitButton); // Then - expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('libelleMetier=Conduite+de+travaux%2C+direction+de+chantier') }, undefined, { shallow: true }); - expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('codeRomes=F1201%2CF1202%2CI1101') }, undefined, { shallow: true }); - expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('libelleCommune=Paris+%2875006%29') }, undefined, { shallow: true }); - expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('codeCommune=75056') }, undefined, { shallow: true }); - expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('latitudeCommune=48.859') }, undefined, { shallow: true }); - expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('longitudeCommune=2.347') }, undefined, { shallow: true }); - expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('codePostal=75006') }, undefined, { shallow: true }); - expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('ville=Paris') }, undefined, { shallow: true }); - expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('distanceCommune=10') }, undefined, { shallow: true }); + expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('libelleMetier=Conduite+de+travaux%2C+direction+de+chantier') }, undefined, { scroll: false }); + expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('codeRomes=F1201%2CF1202%2CI1101') }, undefined, { scroll: false }); + expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('libelleCommune=Paris+%2875006%29') }, undefined, { scroll: false }); + expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('codeCommune=75056') }, undefined, { scroll: false }); + expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('latitudeCommune=48.859') }, undefined, { scroll: false }); + expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('longitudeCommune=2.347') }, undefined, { scroll: false }); + expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('codePostal=75006') }, undefined, { scroll: false }); + expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('ville=Paris') }, undefined, { scroll: false }); + expect(routerPush).toHaveBeenCalledWith({ query: expect.stringContaining('distanceCommune=10') }, undefined, { scroll: false }); }); }); @@ -133,13 +125,11 @@ describe('FormulaireRechercheAlternance', () => { })]; const localisationService = aLocalisationService(); - const alternanceService = anAlternanceService(aResultatRechercherMultipleAlternance().offreList, aResultatRechercherMultipleAlternance().entrepriseList); const metierService = aMetierService(); jest.spyOn(metierService, 'rechercherMetier').mockResolvedValue(createSuccess(aMetierList)); // When render( @@ -172,13 +162,11 @@ describe('FormulaireRechercheAlternance', () => { })]; const localisationService = aLocalisationService(); - const alternanceService = anAlternanceService(aResultatRechercherMultipleAlternance().offreList, aResultatRechercherMultipleAlternance().entrepriseList); const metierService = aMetierService(); jest.spyOn(metierService, 'rechercherMetier').mockResolvedValue(createSuccess(aMetierList)); // When render( @@ -272,4 +260,40 @@ describe('FormulaireRechercheAlternance', () => { libelleMetier: expect.anything(), }); }); + + describe('lorsqu‘on effectue une recherche', () => { + it('appelle la fonction onSubmit', async () => { + // Given + const localisationService = aLocalisationService(); + mockUseRouter({}); + const onSubmit = jest.fn(); + + // When + render( + + + , + ); + + const user = userEvent.setup(); + const inputMetiers = screen.getByRole('combobox', { name: 'Domaine' }); + await user.type(inputMetiers, 'boulang'); + const firstMetierOption = await screen.findByRole('option', { name: aListeDeMetierLaBonneAlternance()[0].label }); + await user.click(firstMetierOption); + + const comboboxCommune = screen.getByRole('combobox', { name: 'Localisation' }); + await user.type(comboboxCommune, 'Pari'); + const localisationOptions = await screen.findAllByRole('option'); + await user.click(localisationOptions[0]); + + const submitButton = screen.getByRole('button', { name: 'Rechercher' }); + await user.click(submitButton); + + // Then + expect(onSubmit).toHaveBeenCalled(); + }); + }); }); diff --git a/src/client/components/features/Alternance/Rechercher/FormulaireRecherche/FormulaireRechercheAlternance.tsx b/src/client/components/features/Alternance/Rechercher/FormulaireRecherche/FormulaireRechercheAlternance.tsx index a52433b5c9..5a19595faf 100644 --- a/src/client/components/features/Alternance/Rechercher/FormulaireRecherche/FormulaireRechercheAlternance.tsx +++ b/src/client/components/features/Alternance/Rechercher/FormulaireRecherche/FormulaireRechercheAlternance.tsx @@ -16,7 +16,16 @@ import { mapToCommune } from '~/client/hooks/useCommuneQuery'; import { MetierService } from '~/client/services/metiers/metier.service'; import { getFormAsQuery } from '~/client/utils/form.util'; -export function FormulaireRechercheAlternance() { +function doNothing() { + return; +} + +interface FormulaireRechercheAlternanceProps { + onSubmit?: () => void; +} + +export function FormulaireRechercheAlternance(props: FormulaireRechercheAlternanceProps) { + const onSubmit = props.onSubmit || doNothing; const queryParams = useAlternanceQuery(); const { libelleMetier, @@ -49,8 +58,9 @@ export function FormulaireRechercheAlternance() { async function updateRechercherAlternanceQueryParams(event: FormEvent) { event.preventDefault(); + onSubmit(); const formEntries = getFormAsQuery(event.currentTarget, queryParams, false); - return router.push({ query: new URLSearchParams(formEntries).toString() }, undefined, { shallow: true }); + return router.push({ query: new URLSearchParams(formEntries).toString() }, undefined, { scroll: false }); } return ( @@ -67,7 +77,6 @@ export function FormulaireRechercheAlternance() { diff --git a/src/client/components/features/Alternance/Rechercher/RechercherAlternance.test.tsx b/src/client/components/features/Alternance/Rechercher/RechercherAlternance.test.tsx index 7fd8d3dfc3..f5860691ac 100644 --- a/src/client/components/features/Alternance/Rechercher/RechercherAlternance.test.tsx +++ b/src/client/components/features/Alternance/Rechercher/RechercherAlternance.test.tsx @@ -9,18 +9,17 @@ import RechercherAlternance from '~/client/components/features/Alternance/Recher import { mockUseRouter } from '~/client/components/useRouter.mock'; import { mockSmallScreen } from '~/client/components/window.mock'; import { DependenciesProvider } from '~/client/context/dependenciesContainer.context'; -import { AlternanceService } from '~/client/services/alternance/alternance.service'; -import { anAlternanceService } from '~/client/services/alternance/alternance.service.fixture'; import { LocalisationService } from '~/client/services/localisation/localisation.service'; import { aLocalisationService } from '~/client/services/localisation/localisation.service.fixture'; import { aMetierService } from '~/client/services/metiers/metier.fixture'; import { MetierService } from '~/client/services/metiers/metier.service'; -import { Alternance, ResultatRechercheAlternance } from '~/server/alternances/domain/alternance'; +import { Alternance } from '~/server/alternances/domain/alternance'; import { anAlternanceEntreprise, anAlternanceEntrepriseSansCandidature, anAlternanceMatcha, anAlternancePEJobs, + aResultatRechercherMultipleAlternance, } from '~/server/alternances/domain/alternance.fixture'; describe('RechercherAlternance', () => { @@ -35,7 +34,6 @@ describe('RechercherAlternance', () => { describe('quand le composant est affiché sans recherche', () => { it('affiche un formulaire pour la recherche de formations, sans échantillon de résultat', async () => { // GIVEN - const alternanceServiceMock = anAlternanceService(); const métierServiceMock = aMetierService(); const localisationServiceMock = aLocalisationService(); mockUseRouter({}); @@ -43,7 +41,6 @@ describe('RechercherAlternance', () => { // WHEN render( @@ -55,16 +52,14 @@ describe('RechercherAlternance', () => { // THEN expect(formulaireRechercheAlternance).toBeInTheDocument(); expect(screen.queryByText(/^[0-9]+ résulat(s)? $/)).not.toBeInTheDocument(); - expect(alternanceServiceMock.rechercherAlternance).toHaveBeenCalledTimes(0); expect(screen.queryByText('Paris (75001)')).not.toBeInTheDocument(); }); }); describe('quand une recherche est effectuée', () => { - let alternanceServiceMock: AlternanceService; let métierServiceMock: MetierService; let localisationServiceMock: LocalisationService; - const alternanceFixture: Array = [ + const alternanceFixture = [ { entreprise: { nom: 'MONSIEUR MICHEL' }, id: 'an-id-matchas', @@ -85,7 +80,7 @@ describe('RechercherAlternance', () => { }, ]; - const entrepriseFixture: Array = [ + const entrepriseFixture = [ { adresse: 'une adresse', candidaturePossible: true, @@ -104,8 +99,12 @@ describe('RechercherAlternance', () => { }, ]; + const resultatFixture = aResultatRechercherMultipleAlternance({ + entrepriseList: entrepriseFixture, + offreList: alternanceFixture, + }); + beforeEach(() => { - alternanceServiceMock = anAlternanceService(alternanceFixture, entrepriseFixture); métierServiceMock = aMetierService(); localisationServiceMock = aLocalisationService(); mockUseRouter({ @@ -120,33 +119,23 @@ describe('RechercherAlternance', () => { }, }); }); + it('uniquement les offres d’alternances sont affichées', async () => { // GIVEN - const expectedQuery = { - codeCommune: '75056', - codeRomes: ['D1102', 'D1104'], - distanceCommune: '10', - latitudeCommune: '48.859', - libelleCommune: 'Paris (75001)', - libelleMetier: 'Boulangerie, pâtisserie, chocolaterie', - longitudeCommune: '2.347', - }; // WHEN render( - + , ); const formulaireRechercheAlternance = screen.getByRole('form'); // THEN expect(formulaireRechercheAlternance).toBeInTheDocument(); - expect(alternanceServiceMock.rechercherAlternance).toHaveBeenCalledWith(expectedQuery); const filtresRecherche = await screen.findAllByText('Paris (75001)'); expect(filtresRecherche.length >= 1).toBe(true); @@ -166,11 +155,10 @@ describe('RechercherAlternance', () => { it('je vois uniquement les entreprises', async () => { render( - + , ); const onglet = await screen.findByRole('tab', { name: 'Entreprises' }); @@ -193,15 +181,17 @@ describe('RechercherAlternance', () => { it('et qu‘il y a des résultats, affiche le nombre de contrats d’alternance', async () => { const user = userEvent.setup(); const offresAlternance = [anAlternanceMatcha(), anAlternancePEJobs()]; - const alternanceServiceMock = anAlternanceService(offresAlternance, []); + const resultats = aResultatRechercherMultipleAlternance({ + entrepriseList: [], + offreList: offresAlternance, + }); render( - + , ); @@ -213,15 +203,17 @@ describe('RechercherAlternance', () => { it('et qu‘il n‘y a pas de résultat, affiche le message sans résultat associé aux contrats d‘alternances', async () => { const user = userEvent.setup(); - const alternanceServiceMock = anAlternanceService([], []); + const resultats = aResultatRechercherMultipleAlternance({ + entrepriseList: [], + offreList: [], + }); render( - + , ); @@ -237,15 +229,17 @@ describe('RechercherAlternance', () => { it('lorsqu‘il y a des résultats, affiche le nombre d’entreprises', async () => { const user = userEvent.setup(); const entrepriseList = [anAlternanceEntreprise(), anAlternanceEntrepriseSansCandidature()]; - const alternanceServiceMock = anAlternanceService([], entrepriseList); + const resultats = aResultatRechercherMultipleAlternance({ + entrepriseList, + offreList: [], + }); render( - + , ); @@ -257,15 +251,17 @@ describe('RechercherAlternance', () => { it('lorsqu‘il n‘y a pas de résultat, affiche le message sans résultat associé aux entreprises', async () => { const user = userEvent.setup(); - const alternanceServiceMock = anAlternanceService([anAlternanceMatcha()], []); + const resultats = aResultatRechercherMultipleAlternance({ + entrepriseList: [], + offreList: [], + }); render( - + , ); @@ -285,11 +281,10 @@ describe('RechercherAlternance', () => { render( - + , ); @@ -305,11 +300,10 @@ describe('RechercherAlternance', () => { render( - + , ); @@ -323,39 +317,4 @@ describe('RechercherAlternance', () => { expect(cardPass).toBeVisible(); expect(cardONISEP).toBeVisible(); }); - - it('n’appelle pas le service avec les query params inconnus', async () => { - // GIVEN - const alternanceServiceMock = anAlternanceService(); - mockUseRouter({ - query: { - codeCommune: '75056', - codeRomes: ['D1102', 'D1104'], - distanceCommune: '10', - latitudeCommune: '48.859', - libelleCommune: 'Paris (75001)', - libelleMetier: 'Boulangerie, pâtisserie, chocolaterie', - longitudeCommune: '2.347', - test: 'test', - }, - }); - - // WHEN - render( - - - , - ); - - await screen.findByRole('heading', { name: /Découvrez des services faits pour vous/i }); - - // THEN - expect(alternanceServiceMock.rechercherAlternance).toHaveBeenCalledWith(expect.not.objectContaining({ - test: 'test', - })); - }); }); diff --git a/src/client/components/features/Alternance/Rechercher/RechercherAlternance.tsx b/src/client/components/features/Alternance/Rechercher/RechercherAlternance.tsx index 9be898163b..2c1301b410 100644 --- a/src/client/components/features/Alternance/Rechercher/RechercherAlternance.tsx +++ b/src/client/components/features/Alternance/Rechercher/RechercherAlternance.tsx @@ -1,5 +1,5 @@ import { useRouter } from 'next/router'; -import React, { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { BanniereApprentissage } from '~/client/components/features/Alternance/Rechercher/BanniereApprentissage'; import { @@ -23,50 +23,31 @@ import { ArticleCard, ArticleCardList } from '~/client/components/ui/Card/Articl import { EnTete } from '~/client/components/ui/EnTete/EnTete'; import { NoResultErrorMessage } from '~/client/components/ui/ErrorMessage/NoResultErrorMessage'; import { TagList } from '~/client/components/ui/Tag/TagList'; -import { useDependency } from '~/client/context/dependenciesContainer.context'; import { useAlternanceQuery } from '~/client/hooks/useAlternanceQuery'; -import { AlternanceService } from '~/client/services/alternance/alternance.service'; -import empty from '~/client/utils/empty'; import { formatRechercherSolutionDocumentTitle } from '~/client/utils/formatRechercherSolutionDocumentTitle.util'; import { ResultatRechercheAlternance } from '~/server/alternances/domain/alternance'; import { Erreur } from '~/server/errors/erreur.types'; const PREFIX_TITRE_PAGE = 'Rechercher une alternance'; -export default function RechercherAlternance() { - const router = useRouter(); +export type RechercherAlternanceProps = { + erreurRecherche?: Erreur + resultats?: ResultatRechercheAlternance +} +export default function RechercherAlternance(props: RechercherAlternanceProps) { const alternanceQuery = useAlternanceQuery(); + const router = useRouter(); - const alternanceService = useDependency('alternanceService'); - const [title, setTitle] = useState(`${PREFIX_TITRE_PAGE} | 1jeune1solution`); - const [alternanceList, setAlternanceList] = useState({ - entrepriseList: [], - offreList: [], - }); - const [isLoading, setIsLoading] = useState(false); - const [erreurRecherche, setErreurRecherche] = useState(undefined); - - useEffect(() => { - if (!empty(alternanceQuery)) { - setIsLoading(true); - setErreurRecherche(undefined); - - alternanceService.rechercherAlternance(alternanceQuery) - .then((response) => { - if (response.instance === 'success') { - const numberResult = response.result.offreList.length + response.result.entrepriseList.length; - setTitle(formatRechercherSolutionDocumentTitle(`${PREFIX_TITRE_PAGE}${numberResult === 0 ? ' - Aucun résultat' : ''}`)); - setAlternanceList(response.result); - } else { - setTitle(formatRechercherSolutionDocumentTitle(PREFIX_TITRE_PAGE, response.errorType)); - setErreurRecherche(response.errorType); - } - setIsLoading(false); - }); - } - }, [alternanceQuery, alternanceService]); - + const nombreResultatsEntreprises = props.resultats?.entrepriseList?.length || 0; + const nombreResultatsOffres = props.resultats?.offreList?.length || 0; + const nombreResultats = nombreResultatsEntreprises + nombreResultatsOffres; + const title = formatRechercherSolutionDocumentTitle(`${PREFIX_TITRE_PAGE}${nombreResultats === 0 ? ' - Aucun résultat' : ''}`); + const alternanceList = { + entrepriseList: props.resultats?.entrepriseList || [], + offreList: props.resultats?.offreList || [], + }; + const erreurRecherche = props.erreurRecherche; function getMessageResultatRecherche(nombreResultats: number) { const messageResultatRechercheSplit: string[] = [`${nombreResultats}`]; @@ -77,19 +58,29 @@ export default function RechercherAlternance() { } else { return ''; } - if (router.query.libelleMetier) { - messageResultatRechercheSplit.push(`pour ${router.query.libelleMetier}`); + if (alternanceQuery.libelleMetier) { + messageResultatRechercheSplit.push(`pour ${alternanceQuery.libelleMetier}`); } return messageResultatRechercheSplit.join(' '); } const étiquettesRecherche = useMemo(() => { - if (router.query.libelleCommune) { - return ; + if (alternanceQuery.libelleCommune) { + return ; } else { return undefined; } - }, [router.query.libelleCommune]); + }, [alternanceQuery.libelleCommune]); + + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + setIsLoading(false); + }, [router]); + + function onSubmit() { + setIsLoading(true); + } return <> } erreurRecherche={erreurRecherche} étiquettesRecherche={étiquettesRecherche} - formulaireRecherche={} + formulaireRecherche={} isLoading={isLoading} listeSolutionElementTab={[{ label: 'Contrats d‘alternance', diff --git a/src/client/components/features/JobEte/FormulaireRecherche/FormulaireRechercheJobEte.tsx b/src/client/components/features/JobEte/FormulaireRecherche/FormulaireRechercheJobEte.tsx index f561f73963..59b3d81a30 100644 --- a/src/client/components/features/JobEte/FormulaireRecherche/FormulaireRechercheJobEte.tsx +++ b/src/client/components/features/JobEte/FormulaireRecherche/FormulaireRechercheJobEte.tsx @@ -50,7 +50,6 @@ export function FormulaireRechercheJobEte() { label="Métier, mot-clé" value={inputMotCle} name="motCle" - autoFocus placeholder="Exemples : serveur, tourisme..." onChange={(event: ChangeEvent) => setInputMotCle(event.currentTarget.value)} /> diff --git "a/src/client/components/features/Job\303\211tudiant/FormulaireRecherche/FormulaireRechercheJob\303\211tudiant.tsx" "b/src/client/components/features/Job\303\211tudiant/FormulaireRecherche/FormulaireRechercheJob\303\211tudiant.tsx" index 0440e634f0..228ea0d458 100644 --- "a/src/client/components/features/Job\303\211tudiant/FormulaireRecherche/FormulaireRechercheJob\303\211tudiant.tsx" +++ "b/src/client/components/features/Job\303\211tudiant/FormulaireRecherche/FormulaireRechercheJob\303\211tudiant.tsx" @@ -51,7 +51,6 @@ export function FormulaireRechercheJobÉtudiant() { label="Métier, mot-clé" value={inputMotCle} name="motCle" - autoFocus placeholder="Exemples : serveur, tourisme..." onChange={(event: ChangeEvent) => setInputMotCle(event.currentTarget.value)} /> diff --git a/src/client/dependencies.container.ts b/src/client/dependencies.container.ts index 05d5961cdc..f8aa6df8e3 100644 --- a/src/client/dependencies.container.ts +++ b/src/client/dependencies.container.ts @@ -3,7 +3,6 @@ import { SearchClient } from 'algoliasearch-helper/types/algoliasearch'; import singletonRouter from 'next/router'; import { createInstantSearchRouterNext } from 'react-instantsearch-router-nextjs'; -import { AlternanceService } from '~/client/services/alternance/alternance.service'; import { ManualAnalyticsService } from '~/client/services/analytics/analytics.service'; import { EulerianAnalyticsService } from '~/client/services/analytics/eulerian/eulerian.analytics.service'; import { MatomoAnalyticsService } from '~/client/services/analytics/matomo/matomo.analytics.service'; @@ -41,16 +40,13 @@ import { RoutingService } from '~/client/services/routing/routing.service'; import { BffStageService } from '~/client/services/stage/bff.stage.service'; import { StageService } from '~/client/services/stage/stage.service'; import { BffStage3eEt2deService } from '~/client/services/stage3eEt2de/bff.stage3eEt2de.service'; -import { - BffStage3eEt2deMetierService, -} from '~/client/services/stage3eEt2de/metier/bff.stage3eEt2deMetier.service'; +import { BffStage3eEt2deMetierService } from '~/client/services/stage3eEt2de/metier/bff.stage3eEt2deMetier.service'; import { Stage3eEt2deService } from '~/client/services/stage3eEt2de/stage3eEt2de.service'; import { VideoService } from '~/client/services/video/video.service'; import { YoutubeVideoService } from '~/client/services/video/youtube/youtube.video.service'; export type Dependency = Dependencies[keyof Dependencies]; export type Dependencies = { - alternanceService: AlternanceService cookiesService: CookiesService analyticsService: ManualAnalyticsService demandeDeContactService: DemandeDeContactService @@ -81,7 +77,6 @@ class DependencyInitException extends Error { export default function dependenciesContainer(sessionId: string): Dependencies { const loggerService = new LoggerService(sessionId); const httpClientService = new HttpClientService(sessionId, loggerService); - const alternanceService = new AlternanceService(httpClientService); const metierLbaService = new BffAlternanceMetierService(httpClientService); const metierStage3eEt2deService = new BffStage3eEt2deMetierService(httpClientService); const formationService = new FormationService(httpClientService); @@ -132,7 +127,6 @@ export default function dependenciesContainer(sessionId: string): Dependencies { const routingService = new RoutingService(createInstantSearchRouterNext({ singletonRouter })); return { - alternanceService, analyticsService, cookiesService, dateService, diff --git a/src/client/services/alternance/alternance.service.fixture.ts b/src/client/services/alternance/alternance.service.fixture.ts deleted file mode 100644 index e248bd7bc5..0000000000 --- a/src/client/services/alternance/alternance.service.fixture.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - aResultatRechercherMultipleAlternance, -} from '~/server/alternances/domain/alternance.fixture'; -import { createSuccess } from '~/server/errors/either'; - -import { AlternanceService } from './alternance.service'; - -export function anAlternanceService( - rechercherAlternanceOffreListValue = aResultatRechercherMultipleAlternance().offreList, - rechercherAlternanceEntrepriseListValue = aResultatRechercherMultipleAlternance().entrepriseList, -): AlternanceService { - return { - rechercherAlternance: jest.fn().mockResolvedValue(createSuccess({ - entrepriseList: rechercherAlternanceEntrepriseListValue, - offreList: rechercherAlternanceOffreListValue, - })), - } as unknown as AlternanceService; -} diff --git a/src/client/services/alternance/alternance.service.test.ts b/src/client/services/alternance/alternance.service.test.ts deleted file mode 100644 index a389d0ab16..0000000000 --- a/src/client/services/alternance/alternance.service.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { AlternanceQueryParams } from '~/client/hooks/useAlternanceQuery'; -import { aCommuneQuery } from '~/client/hooks/useCommuneQuery'; -import { AlternanceService } from '~/client/services/alternance/alternance.service'; -import { anHttpClientService } from '~/client/services/httpClientService.fixture'; - -describe('AlternanceService', () => { - describe('rechercherAlternance', () => { - it('appelle alternance avec la query', async () => { - const httpClientService = anHttpClientService(); - const alternanceService = new AlternanceService(httpClientService); - const alternanceQuery = { - codeRomes: ['D123', 'D122'], - distanceCommune: '30', - libelleMetier: 'Boulangerie, pâtisserie, chocolaterie', - ...aCommuneQuery({ - codeCommune: '13180', - latitudeCommune: '2.37', - longitudeCommune: '15.845', - }), - }; - - await alternanceService.rechercherAlternance(alternanceQuery); - - expect(httpClientService.get).toHaveBeenCalledWith(expect.stringContaining('alternances')); - expect(httpClientService.get).toHaveBeenCalledWith(expect.stringContaining('codeCommune=13180')); - expect(httpClientService.get).toHaveBeenCalledWith(expect.stringContaining('codeRomes=D123%2CD122')); - expect(httpClientService.get).toHaveBeenCalledWith(expect.stringContaining('distanceCommune=30')); - expect(httpClientService.get).toHaveBeenCalledWith(expect.stringContaining('latitudeCommune=2.37')); - expect(httpClientService.get).toHaveBeenCalledWith(expect.stringContaining('longitudeCommune=15.845')); - }); - it('filtre les queries à undefined', async () => { - const httpClientService = anHttpClientService(); - const alternanceService = new AlternanceService(httpClientService); - const alternanceQuery = { - codeRomes: ['D123', 'D122'], - distanceCommune: '30', - libelleMetier: 'Boulangerie, pâtisserie, chocolaterie', - ...aCommuneQuery({ - codeCommune: undefined, - latitudeCommune: '2.37', - longitudeCommune: '15.845', - }), - }; - - await alternanceService.rechercherAlternance(alternanceQuery); - - expect(httpClientService.get).not.toHaveBeenCalledWith(expect.stringContaining('codeCommune')); - }); - it('filtres les queries d’affichage', async () => { - const httpClientService = anHttpClientService(); - const alternanceService = new AlternanceService(httpClientService); - const alternanceQuery: AlternanceQueryParams = { - codeRomes: ['D123', 'D122'], - distanceCommune: '30', - libelleMetier: 'Boulangerie, pâtisserie, chocolaterie', - ...aCommuneQuery({ - libelleCommune: 'Paris (75001)', - }), - }; - - await alternanceService.rechercherAlternance(alternanceQuery); - - expect(httpClientService.get).not.toHaveBeenCalledWith(expect.stringContaining('libelleCommune')); - }); - }); -}); diff --git a/src/client/services/alternance/alternance.service.ts b/src/client/services/alternance/alternance.service.ts deleted file mode 100644 index 03a302e017..0000000000 --- a/src/client/services/alternance/alternance.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ParsedUrlQuery, stringify } from 'querystring'; - -import { AlternanceQueryParams } from '~/client/hooks/useAlternanceQuery'; -import { HttpClientService } from '~/client/services/httpClient.service'; -import { ResultatRechercheAlternance } from '~/server/alternances/domain/alternance'; -import { Either } from '~/server/errors/either'; -import { removeUndefinedKeys } from '~/server/removeUndefinedKeys.utils'; - - -interface AlternanceQueryFiltre extends ParsedUrlQuery { - codeRomes?: string - codeCommune?: string - distanceCommune?: string - latitudeCommune?: string - longitudeCommune?: string -} - -export class AlternanceService { - constructor(private httpClientService: HttpClientService) {} - - async rechercherAlternance(query: AlternanceQueryParams): Promise> { - const filteredQuery = this.filtrerQuery(query); - const sanitizedQuery = removeUndefinedKeys(filteredQuery); - const queryString = stringify(sanitizedQuery); - return this.httpClientService.get(`alternances?${queryString}`); - } - - private filtrerQuery(query: AlternanceQueryParams): AlternanceQueryFiltre { - return { - codeCommune: query.codeCommune, - // FIXME (GAFI 28-08-2023): Idéalement on aimerait ne pas maltraiter les query params : - // devrait être `?codeRomes=A1234&codeRomes=B5678` - // actuellement géré en back avec le format `?codeRomes=A1234,B5678` (en décodé) - codeRomes: query.codeRomes?.toString(), - distanceCommune: query.distanceCommune, - latitudeCommune: query.latitudeCommune, - longitudeCommune: query.longitudeCommune, - }; - } -} diff --git a/src/pages/api/alternances/index.controller.test.ts b/src/pages/api/alternances/index.controller.test.ts deleted file mode 100644 index 84d22b1802..0000000000 --- a/src/pages/api/alternances/index.controller.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { testApiHandler } from 'next-test-api-route-handler'; -import nock from 'nock'; - -import { rechercherAlternanceHandler } from '~/pages/api/alternances/index.controller'; -import { ErrorHttpResponse } from '~/pages/api/utils/response/response.type'; -import { ResultatRechercheAlternance } from '~/server/alternances/domain/alternance'; -import { - aLaBonneAlternanceApiJobsResponse, -} from '~/server/alternances/infra/repositories/apiLaBonneAlternance.fixture'; - -describe('rechercher alternance', () => { - it("retourne une liste d'alternance", async () => { - const codeRomes = 'D123,D122'; - const caller = '1jeune1solution'; - const sources = 'matcha,offres,lba'; - const radius = '30'; - const codeCommune = '13180'; - const longitudeCommune = '15.845'; - const latitudeCommune = '2.37'; - - nock('https://labonnealternance-recette.apprentissage.beta.gouv.fr/api/').get( - `/v1/jobs?caller=${caller}&romes=${codeRomes}&sources=${sources}&insee=${codeCommune}&longitude=${longitudeCommune}&latitude=${latitudeCommune}&radius=${radius}`, - ).reply(200, aLaBonneAlternanceApiJobsResponse()); - - await testApiHandler({ - pagesHandler: (req, res) => rechercherAlternanceHandler(req, res), - test: async ({ fetch }) => { - const res = await fetch({ method: 'GET' }); - const json = await res.json(); - expect(json).toEqual( - { - entrepriseList: [{ - adresse: '18 RUE EMILE LANDRIN, 75020 Paris', - candidaturePossible: true, - id: '52352551700026', - nom: 'CLUB VET', - secteurs: ['Autres intermédiaires du commerce en produits divers', 'Développement informatique'], - tags: ['Paris', '0 à 9 salariés', 'Candidature spontanée'], - ville: 'Paris', - }], - offreList: [{ - entreprise: { nom: 'une entreprise' }, - id: 'id', - source: 0, - tags: ['paris', 'Apprentissage', 'CDI', 'débutant'], - titre: 'un titre', - }, - { - entreprise: { nom: 'SARL HUGUE-DEBRIX' }, - id: 'id-boucher', - source: 0, - tags: ['Apprentissage', 'Cap, autres formations niveau (Infrabac)'], - titre: 'Boucher-charcutier / Bouchère-charcutière', - }, - { - entreprise: { nom: 'MONSIEUR MICHEL' }, - id: 'id-boulanger', - source: 0, - tags: ['Apprentissage', 'CDD', 'Cap, autres formations niveau (Infrabac)'], - titre: 'Ouvrier boulanger / Ouvrière boulangère', - }, - { - entreprise: { nom: 'une entreprise' }, - id: 'alternance-pejob', - source: 1, - tags: ['paris', 'Contrat d‘alternance', 'CDD'], - titre: 'un titre', - }], - }, - ); - }, - url: `/alternances?codeRomes=${codeRomes}&codeCommune=${codeCommune}&longitudeCommune=${longitudeCommune}&latitudeCommune=${latitudeCommune}&distanceCommune=${radius}`, - }); - }); -}); diff --git a/src/pages/api/alternances/index.controller.ts b/src/pages/api/alternances/index.controller.ts deleted file mode 100644 index 3608b28a2e..0000000000 --- a/src/pages/api/alternances/index.controller.ts +++ /dev/null @@ -1,39 +0,0 @@ -import Joi from 'joi'; -import { NextApiRequest, NextApiResponse } from 'next'; - -import { withMonitoring } from '~/pages/api/middlewares/monitoring/monitoring.middleware'; -import { withValidation } from '~/pages/api/middlewares/validation/validation.middleware'; -import { queryToArray } from '~/pages/api/utils/queryToArray.util'; -import { ErrorHttpResponse } from '~/pages/api/utils/response/response.type'; -import { handleResponse } from '~/pages/api/utils/response/response.util'; -import { - AlternanceFiltre, - ResultatRechercheAlternance, -} from '~/server/alternances/domain/alternance'; -import { dependencies } from '~/server/start'; - -export const alternancesQuerySchema = Joi.object({ - codeCommune: Joi.string().required(), - codeRomes: Joi.string().required(), - distanceCommune: Joi.string().required(), - latitudeCommune: Joi.string().required(), - longitudeCommune: Joi.string().required(), -}); - -export async function rechercherAlternanceHandler(req: NextApiRequest, res: NextApiResponse) { - const résultatsRechercheAlternance = await dependencies.alternanceDependencies.rechercherAlternance.handle(alternanceFiltreMapper(req)); - return handleResponse(résultatsRechercheAlternance, res); -} - -export default withMonitoring(withValidation({ query: alternancesQuerySchema }, rechercherAlternanceHandler)); - -export function alternanceFiltreMapper(request: NextApiRequest): AlternanceFiltre { - const { query } = request; - return { - codeCommune: query.codeCommune ? String(query.codeCommune) : '', - codeRomes: query.codeRomes ? queryToArray(query.codeRomes) : [], - distanceCommune: query.distanceCommune ? String(query.distanceCommune) : '', - latitudeCommune: query.latitudeCommune ? String(query.latitudeCommune) : '', - longitudeCommune: query.longitudeCommune ? String(query.longitudeCommune) : '', - }; -} diff --git a/src/pages/apprentissage/index.page.test.tsx b/src/pages/apprentissage/index.page.test.tsx index 0087280924..9175d3ea06 100644 --- a/src/pages/apprentissage/index.page.test.tsx +++ b/src/pages/apprentissage/index.page.test.tsx @@ -4,54 +4,41 @@ import '~/test-utils'; import { render, screen } from '@testing-library/react'; +import { GetServerSidePropsContext } from 'next'; import { mockUseRouter } from '~/client/components/useRouter.mock'; import { mockSmallScreen } from '~/client/components/window.mock'; import { DependenciesProvider } from '~/client/context/dependenciesContainer.context'; -import { anAlternanceService } from '~/client/services/alternance/alternance.service.fixture'; import { aManualAnalyticsService } from '~/client/services/analytics/analytics.service.fixture'; import { aLocalisationService } from '~/client/services/localisation/localisation.service.fixture'; import { aMetierService } from '~/client/services/metiers/metier.fixture'; -import RechercherAlternancePage from '~/pages/apprentissage/index.page'; -import { Alternance } from '~/server/alternances/domain/alternance'; -import { anAlternanceMatchaBoulanger, anAlternancePEJobs } from '~/server/alternances/domain/alternance.fixture'; +import RechercherAlternancePage, { getServerSideProps } from '~/pages/apprentissage/index.page'; +import { Alternance, ResultatRechercheAlternance } from '~/server/alternances/domain/alternance'; +import { + anAlternanceMatchaBoulanger, + anAlternancePEJobs, + aResultatRechercherMultipleAlternance, +} from '~/server/alternances/domain/alternance.fixture'; +import { createFailure, createSuccess } from '~/server/errors/either'; +import { ErreurMetier } from '~/server/errors/erreurMetier.types'; +import { dependencies } from '~/server/start'; -describe('Page rechercher une alternance', () => { - beforeEach(() => { - mockSmallScreen(); - }); +jest.mock('~/server/start', () => ({ + dependencies: { + alternanceDependencies: { + rechercherAlternance: { + handle: jest.fn(), + }, + }, + }, +})); - describe('quand le feature flip n‘est pas actif', () => { +describe('Page rechercher une alternance', () => { + describe('', () => { beforeEach(() => { - process.env = { - ...process.env, - NEXT_PUBLIC_ALTERNANCE_LBA_FEATURE: '0', - }; - }); - - it('ne retourne rien', async () => { - const alternanceServiceMock = anAlternanceService(); - const localisationServiceMock = aLocalisationService(); - const métiersServiceMock = aMetierService(); - mockUseRouter({ query: { page: '1' } }); - - render( - - - , - ); - - const serviceIndisponible = screen.queryByText('Service Indisponible'); - expect(serviceIndisponible).toBeInTheDocument(); + mockSmallScreen(); }); - }); - describe('quand le feature flip est actif', () => { beforeEach(() => { process.env = { ...process.env, @@ -64,35 +51,38 @@ describe('Page rechercher une alternance', () => { anAlternanceMatchaBoulanger(), anAlternancePEJobs(), ]; - const alternanceServiceMock = anAlternanceService(alternanceFixture); + const resultats: ResultatRechercheAlternance = { + entrepriseList: [], + offreList: alternanceFixture, + }; const localisationServiceMock = aLocalisationService(); const métiersServiceMock = aMetierService(); - mockUseRouter({ query: { - codeCommune: '75056', - codeRomes: 'D1102%2CD1104', - distanceCommune: '10', - latitudeCommune: '48.859', - libelleCommune: 'Paris (75001)', - libelleMetier: 'Boulangerie, pâtisserie, chocolaterie', - longitudeCommune: '2.347', - page: '1', - } }); + mockUseRouter({ + query: { + codeCommune: '75056', + codeRomes: 'D1102%2CD1104', + distanceCommune: '10', + latitudeCommune: '48.859', + libelleCommune: 'Paris (75001)', + libelleMetier: 'Boulangerie, pâtisserie, chocolaterie', + longitudeCommune: '2.347', + page: '1', + }, + }); const { container } = render( - + , ); - await screen.findByText(`${alternanceFixture.length} résultats pour Boulangerie, pâtisserie, chocolaterie` ); + await screen.findByText(`${alternanceFixture.length} résultats pour Boulangerie, pâtisserie, chocolaterie`); await expect(container).toBeAccessible(); }); it('affiche le titre propre à la bonne alternance', async () => { - const alternanceServiceMock = anAlternanceService(); const localisationServiceMock = aLocalisationService(); const métiersServiceMock = aMetierService(); mockUseRouter({ query: { page: '1' } }); @@ -100,7 +90,6 @@ describe('Page rechercher une alternance', () => { @@ -112,7 +101,6 @@ describe('Page rechercher une alternance', () => { }); it('envoie les analytics de la page à son affichage', async () => { - const alternanceServiceMock = anAlternanceService(); const localisationServiceMock = aLocalisationService(); const métiersServiceMock = aMetierService(); const analyticsService = aManualAnalyticsService(); @@ -122,7 +110,6 @@ describe('Page rechercher une alternance', () => { @@ -138,4 +125,147 @@ describe('Page rechercher une alternance', () => { }); }); }); + + describe('getServerSideProps', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('lorsque le feature flipping est désactivé', () => { + beforeEach(() => { + process.env = { + ...process.env, + NEXT_PUBLIC_ALTERNANCE_LBA_FEATURE: '0', + }; + }); + it('redirige vers la page 404', async () => { + // GIVEN + const context = {} as unknown as GetServerSidePropsContext; + + // WHEN + const result = await getServerSideProps(context); + + // THEN + expect(result).toEqual({ notFound: true }); + }); + }); + describe('lorsque la recherche est lancée avec des query params', () => { + beforeEach(() => { + process.env = { + ...process.env, + NEXT_PUBLIC_ALTERNANCE_LBA_FEATURE: '1', + }; + }); + describe('lorsque la page est affichée sans query params', () => { + it('retourne des props vide pour que le composant front n’affiche pas de résultat', async () => { + // GIVEN + const context = { + query: {}, + } as GetServerSidePropsContext; + + // WHEN + const result = await getServerSideProps(context); + + // THEN + expect(result).toEqual({ + props: {}, + }); + }); + }); + + describe('lorsque la page est affichée avec des query params', () => { + describe('lorsque les query params sont invalides', () => { + it('retourne une erreur et ne fait pas de recherche', async () => { + // GIVEN + const context = { + query: { + queryInvalide: '75056', + }, + } as unknown as GetServerSidePropsContext; + + // WHEN + const result = await getServerSideProps(context); + + // THEN + expect(result).toEqual({ + props: { + erreurRecherche: 'DEMANDE_INCORRECTE', + }, + }); + expect(dependencies.alternanceDependencies.rechercherAlternance.handle).not.toHaveBeenCalled(); + }); + }); + + describe('lorsque les query params sont valides', () => { + describe('lorsque la recherche retourne une erreur', () => { + it('retourne l’erreur reçue', async () => { + // GIVEN + jest.spyOn(dependencies.alternanceDependencies.rechercherAlternance, 'handle').mockResolvedValue(createFailure(ErreurMetier.SERVICE_INDISPONIBLE)); + + const context = { + query: { + codeCommune: '75056', + codeRomes: 'D1102', + distanceCommune: '10', + latitudeCommune: '48.859', + longitudeCommune: '2.347', + }, + } as unknown as GetServerSidePropsContext; + + // WHEN + const result = await getServerSideProps(context); + + // THEN + expect(result).toEqual({ + props: { + erreurRecherche: ErreurMetier.SERVICE_INDISPONIBLE, + }, + }); + expect(dependencies.alternanceDependencies.rechercherAlternance.handle).toHaveBeenCalledWith({ + codeCommune: '75056', + codeRomes: ['D1102'], + distanceCommune: '10', + latitudeCommune: '48.859', + longitudeCommune: '2.347', + }); + }); + }); + + describe('lorsque la recherche retourne un résultat', () => { + it('retourne le résultat', async () => { + // GIVEN + jest.spyOn(dependencies.alternanceDependencies.rechercherAlternance, 'handle').mockResolvedValue(createSuccess(aResultatRechercherMultipleAlternance())); + + const context = { + query: { + codeCommune: '75056', + codeRomes: 'D1102', + distanceCommune: '10', + latitudeCommune: '48.859', + longitudeCommune: '2.347', + }, + } as unknown as GetServerSidePropsContext; + + // WHEN + const result = await getServerSideProps(context); + + // THEN + expect(result).toEqual({ + props: { + resultats: aResultatRechercherMultipleAlternance(), + }, + }); + expect(dependencies.alternanceDependencies.rechercherAlternance.handle).toHaveBeenCalledWith({ + codeCommune: '75056', + codeRomes: ['D1102'], + distanceCommune: '10', + latitudeCommune: '48.859', + longitudeCommune: '2.347', + }); + }); + }); + }); + }); + }); + }); }); diff --git a/src/pages/apprentissage/index.page.tsx b/src/pages/apprentissage/index.page.tsx index 6afc16fc05..748c9b780c 100644 --- a/src/pages/apprentissage/index.page.tsx +++ b/src/pages/apprentissage/index.page.tsx @@ -1,33 +1,79 @@ -import { useRouter } from 'next/router'; -import { stringify } from 'querystring'; -import React, { useEffect } from 'react'; +import Joi from 'joi'; +import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next'; +import { ParsedUrlQuery } from 'querystring'; +import React from 'react'; -import RechercherAlternance from '~/client/components/features/Alternance/Rechercher/RechercherAlternance'; -import ErrorUnavailableService from '~/client/components/layouts/Error/ErrorUnavailableService'; +import RechercherAlternance, { + RechercherAlternanceProps, +} from '~/client/components/features/Alternance/Rechercher/RechercherAlternance'; import useAnalytics from '~/client/hooks/useAnalytics'; +import empty from '~/client/utils/empty'; +import { queryToArray } from '~/pages/api/utils/queryToArray.util'; import analytics from '~/pages/apprentissage/index.analytics'; +import { AlternanceFiltre } from '~/server/alternances/domain/alternance'; +import { ErreurMetier } from '~/server/errors/erreurMetier.types'; +import { dependencies } from '~/server/start'; -export default function RechercherAlternancePage() { - const router = useRouter(); - const displayRechercherAlternanceLBA = process.env.NEXT_PUBLIC_ALTERNANCE_LBA_FEATURE === '1'; +type RechercherAlternancePageProps = RechercherAlternanceProps; +export default function RechercherAlternancePage(props: RechercherAlternancePageProps) { useAnalytics(analytics); - useEffect(() => { - if (!displayRechercherAlternanceLBA && router.isReady) { - const queryString = stringify(router.query); - if (queryString.length === 0) router.replace({ query: 'page=1' }, undefined, { shallow: true }); - } - }, [router, displayRechercherAlternanceLBA]); + return ; +} - if (!displayRechercherAlternanceLBA) return ; +export const alternancesQuerySchema = Joi.object({ + codeCommune: Joi.string().required(), + codeRomes: Joi.string().required(), + distanceCommune: Joi.string().required(), + latitudeCommune: Joi.string().required(), + longitudeCommune: Joi.string().required(), +}).options({ allowUnknown: true }); - return ; +export function alternanceFiltreMapper(query: ParsedUrlQuery): AlternanceFiltre { + return { + codeCommune: query.codeCommune ? String(query.codeCommune) : '', + codeRomes: query.codeRomes ? queryToArray(query.codeRomes) : [], + distanceCommune: query.distanceCommune ? String(query.distanceCommune) : '', + latitudeCommune: query.latitudeCommune ? String(query.latitudeCommune) : '', + longitudeCommune: query.longitudeCommune ? String(query.longitudeCommune) : '', + }; } -// NOTE (GAFI 08-08-2023): Rend le composant server-side -export function getServerSideProps() { +export async function getServerSideProps(context: GetServerSidePropsContext): Promise> { + const isFeatureActive = process.env.NEXT_PUBLIC_ALTERNANCE_LBA_FEATURE === '1'; + if (!isFeatureActive) { + return { notFound: true }; + } + + const { query } = context; + + if (empty(query)) { + return { + props: {}, + }; + } + + if (alternancesQuerySchema.validate(query).error) { + return { + props: { + erreurRecherche: ErreurMetier.DEMANDE_INCORRECTE, + }, + }; + } + const filtre = alternanceFiltreMapper(query); + + const resultats = await dependencies.alternanceDependencies.rechercherAlternance.handle(filtre); + if (resultats.instance === 'failure') { + return { + props: { + erreurRecherche: resultats.errorType, + }, + }; + } return { - props: {}, + props: { + resultats: JSON.parse(JSON.stringify(resultats.result)), + }, }; } diff --git a/src/pages/emplois/index.page.test.tsx b/src/pages/emplois/index.page.test.tsx index 45c82a3fbe..253efdc39f 100644 --- a/src/pages/emplois/index.page.test.tsx +++ b/src/pages/emplois/index.page.test.tsx @@ -99,7 +99,7 @@ describe('Page Emploi', () => { describe('lorsque la recherche est lancée avec des query params', () => { it('filtre les offres et retourne le résultat', async () => { // GIVEN - (dependencies.offreEmploiDependencies.rechercherOffreEmploi.handle as jest.Mock).mockReturnValue(createSuccess(aRésultatsRechercheOffre())); + jest.spyOn(dependencies.offreEmploiDependencies.rechercherOffreEmploi, 'handle').mockResolvedValue(createSuccess(aRésultatsRechercheOffre())); const context = { query: { @@ -124,7 +124,7 @@ describe('Page Emploi', () => { describe('lorsque la recherche retourne une erreur', () => { it('retourne une erreur de service indisponible', async () => { // GIVEN - (dependencies.offreEmploiDependencies.rechercherOffreEmploi.handle as jest.Mock).mockReturnValue(createFailure(ErreurMetier.SERVICE_INDISPONIBLE)); + jest.spyOn(dependencies.offreEmploiDependencies.rechercherOffreEmploi, 'handle').mockResolvedValue(createFailure(ErreurMetier.SERVICE_INDISPONIBLE)); const context = { query: { page: 1, diff --git a/src/server/alternances/domain/alternance.fixture.ts b/src/server/alternances/domain/alternance.fixture.ts index 82ca37363f..39ec5fe948 100644 --- a/src/server/alternances/domain/alternance.fixture.ts +++ b/src/server/alternances/domain/alternance.fixture.ts @@ -1,4 +1,4 @@ -import { Alternance, ResultatRechercheAlternance } from './alternance'; +import { Alternance, AlternanceFiltre, ResultatRechercheAlternance } from './alternance'; export const anAlternanceMatcha = (override?: Partial): Alternance => { return { @@ -91,9 +91,21 @@ export const anAlternanceEntrepriseSansCandidature = (): ResultatRechercheAltern }; }; -export const aResultatRechercherMultipleAlternance = (): ResultatRechercheAlternance => { +export const aResultatRechercherMultipleAlternance = (override?: Partial): ResultatRechercheAlternance => { return { entrepriseList: [anAlternanceEntreprise(), anAlternanceEntrepriseSansCandidature()], offreList: [anAlternanceMatcha(), anAlternanceMatchaBoucher(), anAlternanceMatchaBoulanger(), anAlternancePEJobs()], + ...override, }; }; + +export function anAlternanceFiltre(override?: Partial): AlternanceFiltre { + return { + codeCommune: '12345', + codeRomes: ['A1234', 'B1234'], + distanceCommune: '10', + latitudeCommune: '1.234', + longitudeCommune: '2.345', + ...override, + }; +} diff --git a/src/server/alternances/infra/repositories/mockAlternance.repository.ts b/src/server/alternances/infra/repositories/mockAlternance.repository.ts new file mode 100644 index 0000000000..88c43d2d17 --- /dev/null +++ b/src/server/alternances/infra/repositories/mockAlternance.repository.ts @@ -0,0 +1,30 @@ +import { Alternance, AlternanceFiltre, ResultatRechercheAlternance } from '~/server/alternances/domain/alternance'; +import { anAlternanceMatcha, aResultatRechercherMultipleAlternance } from '~/server/alternances/domain/alternance.fixture'; +import { AlternanceRepository } from '~/server/alternances/domain/alternance.repository'; +import { createFailure, createSuccess, Either, Success } from '~/server/errors/either'; +import { ErreurMetier } from '~/server/errors/erreurMetier.types'; + + +export function mockedRepositoryReturnsASuccessWhenCodeCommuneIsNot12345(filtre: AlternanceFiltre): Success | undefined { + if (filtre.codeCommune !== '12345') { + return createSuccess(aResultatRechercherMultipleAlternance()); + } +} + +export function searchAlternanceRepositoryMockResults(): Either { + return createFailure(ErreurMetier.SERVICE_INDISPONIBLE); +} + +export function getAlternanceRepositoryMockResults(): Either { + return createSuccess(anAlternanceMatcha()); +} + +export class MockAlternanceRepository implements AlternanceRepository { + async search(filtre: AlternanceFiltre): Promise> { + return mockedRepositoryReturnsASuccessWhenCodeCommuneIsNot12345(filtre) ?? searchAlternanceRepositoryMockResults(); + } + + async get(): Promise> { + return getAlternanceRepositoryMockResults(); + } +} diff --git a/src/server/configuration/dependencies.container.ts b/src/server/configuration/dependencies.container.ts index 1d19c6040c..c4b7a9f3b6 100644 --- a/src/server/configuration/dependencies.container.ts +++ b/src/server/configuration/dependencies.container.ts @@ -11,6 +11,7 @@ import { import { ApiLaBonneAlternanceErrorManagementServiceGet, ApiLaBonneAlternanceErrorManagementServiceSearch, } from '~/server/alternances/infra/repositories/apiLaBonneAlternanceErrorManagement.service'; +import { MockAlternanceRepository } from '~/server/alternances/infra/repositories/mockAlternance.repository'; import { CampagneApprentissageDependencies, campagneApprentissageDependenciesContainer, @@ -292,7 +293,9 @@ export function dependenciesContainer(): Dependencies { const apiLaBonneAlternanceFormationRepository = new ApiLaBonneAlternanceFormationRepository(laBonneAlternanceClientService, apiLaBonneAlternanceCaller, defaultErrorManagementService); const apiLaBonneAlternanceMétierRepository = new ApiLaBonneAlternanceMétierRepository(laBonneAlternanceClientService, defaultErrorManagementService); - const alternanceDependencies = alternancesDependenciesContainer(apiLaBonneAlternanceRepository); + const alternanceDependencies = serverConfigurationService.getConfiguration().API_LA_BONNE_ALTERNANCE_IS_ALTERNANCE_MOCK_ACTIVE + ? alternancesDependenciesContainer(new MockAlternanceRepository()) + : alternancesDependenciesContainer(apiLaBonneAlternanceRepository); const trajectoiresProHttpClientService = new AuthenticatedHttpClientService(getApiTrajectoiresProConfig(serverConfigurationService), loggerService); const geoHttpClientService = new CachedHttpClientService(getApiGeoGouvConfig(serverConfigurationService)); diff --git a/src/server/offres/infra/repositories/mockOffre.repository.ts b/src/server/offres/infra/repositories/mockOffre.repository.ts index b81e2b290c..e003854596 100644 --- a/src/server/offres/infra/repositories/mockOffre.repository.ts +++ b/src/server/offres/infra/repositories/mockOffre.repository.ts @@ -29,11 +29,11 @@ export function getOffreRepositoryMockResults(): Either { export class MockOffreRepository implements OffreRepository { paramètreParDéfaut: string | undefined; - get(): Promise> { - return Promise.resolve(getOffreRepositoryMockResults()); + async get(): Promise> { + return getOffreRepositoryMockResults(); } - search(filtre: OffreFiltre): Promise> { - return Promise.resolve(searchOffreRepositoryMockResults(filtre)); + async search(filtre: OffreFiltre): Promise> { + return searchOffreRepositoryMockResults(filtre); } } diff --git a/src/server/services/configuration.service.fixture.ts b/src/server/services/configuration.service.fixture.ts index a6cd840af9..474a8ab79f 100644 --- a/src/server/services/configuration.service.fixture.ts +++ b/src/server/services/configuration.service.fixture.ts @@ -20,6 +20,7 @@ export class ConfigurationServiceFixture implements ConfigurationService { API_IMMERSION_FACILE_STAGE_3EME_API_KEY: 'API_IMMERSION_FACILE_STAGE_3EME_API_KEY', API_IMMERSION_FACILE_STAGE_3EME_URL: 'https://api.immersion-facile.beta.gouv.fr/v1/', API_LA_BONNE_ALTERNANCE_CALLER: '1jeune-1solution-test', + API_LA_BONNE_ALTERNANCE_IS_ALTERNANCE_MOCK_ACTIVE: false, API_LA_BONNE_ALTERNANCE_URL: 'https://labonnealternance-recette.beta.gouv.fr/api/', API_LES_ENTREPRISES_SENGAGENT_URL: 'https://staging.lesentreprises-sengagent.local', API_ONISEP_ACCOUNT_EMAIL: 'email@example.com', diff --git a/src/server/services/serverConfiguration.service.ts b/src/server/services/serverConfiguration.service.ts index a03a12d709..478030271f 100644 --- a/src/server/services/serverConfiguration.service.ts +++ b/src/server/services/serverConfiguration.service.ts @@ -13,6 +13,7 @@ export default class ServerConfigurationService implements ConfigurationService API_IMMERSION_FACILE_STAGE_3EME_API_KEY: ServerConfigurationService.getOrThrowError('API_IMMERSION_FACILE_STAGE_3EME_API_KEY'), API_IMMERSION_FACILE_STAGE_3EME_URL: ServerConfigurationService.getOrThrowError('API_IMMERSION_FACILE_STAGE_3EME_URL'), API_LA_BONNE_ALTERNANCE_CALLER: ServerConfigurationService.getOrThrowError('API_LA_BONNE_ALTERNANCE_CALLER'), + API_LA_BONNE_ALTERNANCE_IS_ALTERNANCE_MOCK_ACTIVE: Boolean(Number(ServerConfigurationService.getOrDefault('API_LA_BONNE_ALTERNANCE_IS_ALTERNANCE_MOCK_ACTIVE', '0'))), API_LA_BONNE_ALTERNANCE_URL: ServerConfigurationService.getOrThrowError('API_LA_BONNE_ALTERNANCE_URL'), API_LES_ENTREPRISES_SENGAGENT_URL: ServerConfigurationService.getOrThrowError('API_LES_ENTREPRISES_SENGAGENT_URL'), API_ONISEP_ACCOUNT_EMAIL: ServerConfigurationService.getOrThrowError('API_ONISEP_ACCOUNT_EMAIL'), @@ -103,6 +104,7 @@ export interface EnvironmentVariables { readonly API_IMMERSION_FACILE_STAGE_3EME_API_KEY: string readonly API_IMMERSION_FACILE_STAGE_3EME_URL: string readonly API_LA_BONNE_ALTERNANCE_CALLER: string + readonly API_LA_BONNE_ALTERNANCE_IS_ALTERNANCE_MOCK_ACTIVE: boolean readonly API_LA_BONNE_ALTERNANCE_URL: string readonly API_LES_ENTREPRISES_SENGAGENT_URL: string readonly API_ONISEP_BASE_URL: string