From fe967a67a08b3af76cef04ce14d39fd54c3e352f Mon Sep 17 00:00:00 2001 From: daproclaima Date: Wed, 4 Sep 2024 23:23:13 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A5=85(frontend)=20improve=20error=20catc?= =?UTF-8?q?hing=20in=20forms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rename CreateMailboxForm into ModalCreateMailbox, and useCreateMailDomain into useAddMailDomain - use useAPIError hook in ModalCreateMailbox.tsx and ModalAddMailDomain - update translations and tests (include removal of e2e test able to be asserted by component tests) --- CHANGELOG.md | 6 +- .../apps/desk/src/api/parseAPIError.ts | 4 +- ...ateMailDomain.tsx => useAddMailDomain.tsx} | 21 +- .../mail-domains/api/useCreateMailbox.tsx | 5 +- .../components/MailDomainsContent.tsx | 4 +- .../components/ModalAddMailDomain.tsx | 144 ++++---- ...MailboxForm.tsx => ModalCreateMailbox.tsx} | 93 +++-- .../__tests__/ModalAddMailDomain.test.tsx | 241 +++++++++++++ .../__tests__/ModalCreateMailbox.test.tsx | 333 ++++++++++++++++++ .../apps/desk/src/i18n/translations.json | 4 + .../apps/desk/src/pages/mail-domains/add.tsx | 4 +- .../mail-domain-create-mailbox.spec.ts | 153 -------- .../app-desk/mail-domains-add.spec.ts | 136 ------- 13 files changed, 737 insertions(+), 411 deletions(-) rename src/frontend/apps/desk/src/features/mail-domains/api/{useCreateMailDomain.tsx => useAddMailDomain.tsx} (71%) rename src/frontend/apps/desk/src/features/mail-domains/components/{forms/CreateMailboxForm.tsx => ModalCreateMailbox.tsx} (72%) create mode 100644 src/frontend/apps/desk/src/features/mail-domains/components/__tests__/ModalAddMailDomain.test.tsx create mode 100644 src/frontend/apps/desk/src/features/mail-domains/components/__tests__/ModalCreateMailbox.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index cdafe3785..1ab90db43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to ### Added - 📈(monitoring) configure sentry monitoring #378 +- 🥅(frontend) improve api error handling #355 ### Fixed @@ -18,6 +19,7 @@ and this project adheres to - 💬(frontend) fix group member removal text #382 - 💬(frontend) fix add mail domain text #382 - 🐛(frontend) fix keyboard navigation #379 +- 🐛(frontend) fix add mail domain form submission #355 ## [1.0.2] - 2024-08-30 @@ -30,10 +32,6 @@ and this project adheres to - 👽️(mailboxes) fix mailbox creation after dimail api improvement (#360) -### Fixed - -- 🐛(frontend) user can submit form to add mail domain by pressing "Enter" key - ## [1.0.1] - 2024-08-19 ### Fixed diff --git a/src/frontend/apps/desk/src/api/parseAPIError.ts b/src/frontend/apps/desk/src/api/parseAPIError.ts index e9b52bcf6..19b0b6616 100644 --- a/src/frontend/apps/desk/src/api/parseAPIError.ts +++ b/src/frontend/apps/desk/src/api/parseAPIError.ts @@ -48,7 +48,9 @@ export const parseAPIErrorCause = ({ }): string[] => causes.reduce((arrayCauses, cause) => { const foundErrorParams = Object.values(errorParams).find((params) => - params.causes.find((knownCause) => knownCause.match(cause)), + params.causes.find((knownCause) => + new RegExp(knownCause, 'i').test(cause), + ), ); if (!foundErrorParams) { diff --git a/src/frontend/apps/desk/src/features/mail-domains/api/useCreateMailDomain.tsx b/src/frontend/apps/desk/src/features/mail-domains/api/useAddMailDomain.tsx similarity index 71% rename from src/frontend/apps/desk/src/features/mail-domains/api/useCreateMailDomain.tsx rename to src/frontend/apps/desk/src/features/mail-domains/api/useAddMailDomain.tsx index fe1f721f6..60c81c501 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/api/useCreateMailDomain.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/api/useAddMailDomain.tsx @@ -5,7 +5,13 @@ import { MailDomain } from '@/features/mail-domains'; import { KEY_LIST_MAIL_DOMAIN } from './useMailDomains'; -export const createMailDomain = async (name: string): Promise => { +export interface AddMailDomainParams { + name: string; +} + +export const addMailDomain = async ( + name: AddMailDomainParams['name'], +): Promise => { const response = await fetchAPI(`mail-domains/`, { method: 'POST', body: JSON.stringify({ @@ -23,19 +29,24 @@ export const createMailDomain = async (name: string): Promise => { return response.json() as Promise; }; -export function useCreateMailDomain({ +export const useAddMailDomain = ({ onSuccess, + onError, }: { onSuccess: (data: MailDomain) => void; -}) { + onError: (error: APIError) => void; +}) => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: createMailDomain, + mutationFn: addMailDomain, onSuccess: (data) => { void queryClient.invalidateQueries({ queryKey: [KEY_LIST_MAIL_DOMAIN], }); onSuccess(data); }, + onError: (error) => { + onError(error); + }, }); -} +}; diff --git a/src/frontend/apps/desk/src/features/mail-domains/api/useCreateMailbox.tsx b/src/frontend/apps/desk/src/features/mail-domains/api/useCreateMailbox.tsx index 86a19a21a..d9686db81 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/api/useCreateMailbox.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/api/useCreateMailbox.tsx @@ -26,7 +26,6 @@ export const createMailbox = async ({ }); if (!response.ok) { - // TODO: extend errorCauses to return the name of the invalid field names to highlight in the form? throw new APIError( 'Failed to create the mailbox', await errorCauses(response), @@ -40,7 +39,7 @@ type UseCreateMailboxParams = { mailDomainSlug: string } & UseMutationOptions< CreateMailboxParams >; -export function useCreateMailbox(options: UseCreateMailboxParams) { +export const useCreateMailbox = (options: UseCreateMailboxParams) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: createMailbox, @@ -61,4 +60,4 @@ export function useCreateMailbox(options: UseCreateMailboxParams) { } }, }); -} +}; diff --git a/src/frontend/apps/desk/src/features/mail-domains/components/MailDomainsContent.tsx b/src/frontend/apps/desk/src/features/mail-domains/components/MailDomainsContent.tsx index 5d8ecb7b1..186c2a4bf 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/components/MailDomainsContent.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/components/MailDomainsContent.tsx @@ -19,7 +19,7 @@ import { default as MailDomainsLogo } from '../assets/mail-domains-logo.svg'; import { PAGE_SIZE } from '../conf'; import { MailDomain, MailDomainMailbox } from '../types'; -import { CreateMailboxForm } from './forms/CreateMailboxForm'; +import { ModalCreateMailbox } from './ModalCreateMailbox'; export type ViewMailbox = { name: string; @@ -87,7 +87,7 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) { ) : ( <> {isCreateMailboxFormVisible && mailDomain ? ( - setIsCreateMailboxFormVisible(false)} /> diff --git a/src/frontend/apps/desk/src/features/mail-domains/components/ModalAddMailDomain.tsx b/src/frontend/apps/desk/src/features/mail-domains/components/ModalAddMailDomain.tsx index d7f001852..e0f2070fb 100644 --- a/src/frontend/apps/desk/src/features/mail-domains/components/ModalAddMailDomain.tsx +++ b/src/frontend/apps/desk/src/features/mail-domains/components/ModalAddMailDomain.tsx @@ -1,91 +1,27 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { Button, Input, Loader, ModalSize } from '@openfun/cunningham-react'; import { useRouter } from 'next/navigation'; -import React from 'react'; -import { - Controller, - FormProvider, - UseFormReturn, - useForm, -} from 'react-hook-form'; +import React, { useState } from 'react'; +import { Controller, FormProvider, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; -import { APIError } from '@/api'; +import { parseAPIError } from '@/api/parseAPIError'; import { Box, Text, TextErrors } from '@/components'; import { Modal } from '@/components/Modal'; -import { useCreateMailDomain } from '@/features/mail-domains'; +import { useAddMailDomain } from '@/features/mail-domains'; import { default as MailDomainsLogo } from '../assets/mail-domains-logo.svg'; const FORM_ID = 'form-add-mail-domain'; -const useAddMailDomainApiError = ({ - error, - methods, -}: { - error: APIError | null; - methods: UseFormReturn<{ name: string }> | null; -}): string[] | undefined => { - const [errorCauses, setErrorCauses] = React.useState( - undefined, - ); - const { t } = useTranslation(); - - React.useEffect(() => { - if (methods && t && error) { - let causes = undefined; - - if (error.cause?.length) { - const parseCauses = (causes: string[]) => - causes.reduce((arrayCauses, cause) => { - switch (cause) { - case 'Mail domain with this name already exists.': - case 'Mail domain with this Slug already exists.': - methods.setError('name', { - type: 'manual', - message: t( - 'This mail domain is already used. Please, choose another one.', - ), - }); - break; - default: - arrayCauses.push(cause); - } - - return arrayCauses; - }, [] as string[]); - - causes = parseCauses(error.cause); - } - - if (error.status === 500 || !error.cause) { - causes = [ - t( - 'Your request cannot be processed because the server is experiencing an error. If the problem ' + - 'persists, please contact our support to resolve the issue: suiteterritoriale@anct.gouv.fr.', - ), - ]; - } - - setErrorCauses(causes); - } - }, [methods, t, error]); - - React.useEffect(() => { - if (errorCauses && methods) { - methods.setFocus('name'); - } - }, [methods, errorCauses]); - - return errorCauses; -}; - export const ModalAddMailDomain = () => { const { t } = useTranslation(); const router = useRouter(); - const createMailDomainValidationSchema = z.object({ + const [errorCauses, setErrorCauses] = useState([]); + + const addMailDomainValidationSchema = z.object({ name: z.string().min(1, t('Example: saint-laurent.fr')), }); @@ -96,26 +32,62 @@ export const ModalAddMailDomain = () => { }, mode: 'onChange', reValidateMode: 'onChange', - resolver: zodResolver(createMailDomainValidationSchema), + resolver: zodResolver(addMailDomainValidationSchema), }); - const { - mutate: createMailDomain, - isPending, - error, - } = useCreateMailDomain({ + const { mutate: addMailDomain, isPending } = useAddMailDomain({ onSuccess: (mailDomain) => { router.push(`/mail-domains/${mailDomain.slug}`); }, + onError: (error) => { + const unhandledCauses = parseAPIError({ + error, + errorParams: { + name: { + causes: [ + 'Mail domain with this name already exists.', + 'Mail domain with this Slug already exists.', + ], + handleError: () => { + if (methods.formState.errors.name) { + return; + } + + methods.setError('name', { + type: 'manual', + message: t( + 'This mail domain is already used. Please, choose another one.', + ), + }); + methods.setFocus('name'); + }, + }, + }, + serverErrorParams: { + handleError: () => { + methods.setFocus('name'); + }, + defaultMessage: t( + 'Your request cannot be processed because the server is experiencing an error. If the problem ' + + 'persists, please contact our support to resolve the issue: suiteterritoriale@anct.gouv.fr', + ), + }, + }); + + setErrorCauses((prevState) => + unhandledCauses && + JSON.stringify(unhandledCauses) !== JSON.stringify(prevState) + ? unhandledCauses + : prevState, + ); + }, }); - const errorCauses = useAddMailDomainApiError({ error, methods }); - const onSubmitCallback = (event: React.FormEvent) => { event.preventDefault(); void methods.handleSubmit(({ name }) => { - void createMailDomain(name); + void addMailDomain(name); })(); }; @@ -139,7 +111,11 @@ export const ModalAddMailDomain = () => { @@ -163,7 +139,11 @@ export const ModalAddMailDomain = () => { ) : null} -
+ ([]); + const messageInvalidMinChar = t('You must have minimum 1 character'); const createMailboxValidationSchema = z.object({ @@ -77,7 +80,7 @@ export const CreateMailboxForm = ({ resolver: zodResolver(createMailboxValidationSchema), }); - const { mutate: createMailbox, error } = useCreateMailbox({ + const { mutate: createMailbox, isPending } = useCreateMailbox({ mailDomainSlug: mailDomain.slug, onSuccess: () => { toast(t('Mailbox created!'), VariantType.SUCCESS, { @@ -86,6 +89,52 @@ export const CreateMailboxForm = ({ closeModal(); }, + onError: (error) => { + const unhandledCauses = parseAPIError({ + error, + errorParams: { + local_part: { + causes: ['Mailbox with this Local_part and Domain already exists.'], + handleError: () => { + methods.setError('local_part', { + type: 'manual', + message: t('This email prefix is already used.'), + }); + methods.setFocus('local_part'); + }, + }, + secret: { + causes: [ + "Please configure your domain's secret before creating any mailbox.", + `Secret not valid for this domain`, + ], + causeShown: t( + 'The mail domain secret is misconfigured. Please, contact ' + + 'our support team to solve the issue: suiteterritoriale@anct.gouv.fr', + ), + handleError: () => { + methods.setFocus('first_name'); + }, + }, + }, + serverErrorParams: { + handleError: () => { + methods.setFocus('first_name'); + }, + defaultMessage: t( + 'Your request cannot be processed because the server is experiencing an error. If the problem ' + + 'persists, please contact our support to resolve the issue: suiteterritoriale@anct.gouv.fr', + ), + }, + }); + + setErrorCauses((prevState) => + unhandledCauses && + JSON.stringify(unhandledCauses) !== JSON.stringify(prevState) + ? unhandledCauses + : prevState, + ); + }, }); const onSubmitCallback = (event: React.FormEvent) => { @@ -95,20 +144,6 @@ export const CreateMailboxForm = ({ )(); }; - const causes = error?.cause?.filter((cause) => { - const isFound = - cause === 'Mailbox with this Local_part and Domain already exists.'; - - if (isFound) { - methods.setError('local_part', { - type: 'manual', - message: t('This email prefix is already used.'), - }); - } - - return !isFound; - }); - return ( {t('Create the mailbox')} @@ -152,8 +191,12 @@ export const CreateMailboxForm = ({ > - {!!causes?.length && ( - + {!!errorCauses?.length && ( + )} + ({ + useRouter: jest.fn().mockImplementation(() => ({ + push: mockPush, + })), +})); + +describe('ModalAddMailDomain', () => { + const getElements = () => ({ + modalElement: screen.getByText('Add a mail domain'), + formTag: screen.getByTitle('Mail domain addition form'), + inputName: screen.getByLabelText(/Domain name/i), + buttonCancel: screen.getByRole('button', { name: /Cancel/i, hidden: true }), + buttonSubmit: screen.getByRole('button', { + name: /Add the domain/i, + hidden: true, + }), + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + fetchMock.restore(); + }); + + it('renders all the elements', () => { + render(, { wrapper: AppWrapper }); + + const { modalElement, formTag, inputName, buttonCancel, buttonSubmit } = + getElements(); + + expect(modalElement).toBeVisible(); + expect(formTag).toBeVisible(); + expect(inputName).toBeVisible(); + expect(screen.getByText('Example: saint-laurent.fr')).toBeVisible(); + expect(buttonCancel).toBeVisible(); + expect(buttonSubmit).toBeVisible(); + }); + + it('should disable submit button when no field is filled', () => { + render(, { wrapper: AppWrapper }); + + const { buttonSubmit } = getElements(); + + expect(buttonSubmit).toBeDisabled(); + }); + + it('displays validation error on empty submit', async () => { + fetchMock.mock(`end:mail-domains/`, 201); + + const user = userEvent.setup(); + + render(, { wrapper: AppWrapper }); + + const { inputName, buttonSubmit } = getElements(); + + await user.type(inputName, 'domain.fr'); + await user.clear(inputName); + + await user.click(buttonSubmit); + + await waitFor(() => { + expect( + screen.getByText(/Example: saint-laurent.fr/i), + ).toBeInTheDocument(); + }); + + expect(fetchMock.lastUrl()).toBeFalsy(); + }); + + it('submits the form when validation passes', async () => { + fetchMock.mock(`end:mail-domains/`, { + status: 201, + body: { + name: 'domain.fr', + id: '456ac6ca-0402-4615-8005-69bc1efde43f', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + slug: 'domainfr', + status: 'enabled', + abilities: { + get: true, + patch: true, + put: true, + post: true, + delete: true, + manage_accesses: true, + }, + }, + }); + + const user = userEvent.setup(); + + render(, { wrapper: AppWrapper }); + + const { inputName, buttonSubmit } = getElements(); + + await user.type(inputName, 'domain.fr'); + + await user.click(buttonSubmit); + + expect(fetchMock.lastUrl()).toContain('/mail-domains/'); + expect(fetchMock.lastOptions()).toEqual({ + body: JSON.stringify({ + name: 'domain.fr', + }), + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + }); + + expect(mockPush).toHaveBeenCalledWith(`/mail-domains/domainfr`); + }); + + it('submits the form on key enter press', async () => { + fetchMock.mock(`end:mail-domains/`, 201); + + const user = userEvent.setup(); + + render(, { wrapper: AppWrapper }); + + const { inputName } = getElements(); + + await user.type(inputName, 'domain.fr'); + await user.type(inputName, '{enter}'); + + expect(fetchMock.lastUrl()).toContain('/mail-domains/'); + expect(fetchMock.lastOptions()).toEqual({ + body: JSON.stringify({ + name: 'domain.fr', + }), + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + }); + }); + + it('displays right error message error when maildomain name is already used', async () => { + fetchMock.mock(`end:mail-domains/`, { + status: 400, + body: { + name: 'Mail domain with this name already exists.', + }, + }); + + const user = userEvent.setup(); + + render(, { wrapper: AppWrapper }); + + const { inputName, buttonSubmit } = getElements(); + + await user.type(inputName, 'domain.fr'); + + await user.click(buttonSubmit); + + await waitFor(() => { + expect( + screen.getByText( + /This mail domain is already used. Please, choose another one./i, + ), + ).toBeInTheDocument(); + }); + + expect(inputName).toHaveFocus(); + + await user.type(inputName, 'domain2.fr'); + expect(buttonSubmit).toBeEnabled(); + }); + + it('displays right error message error when maildomain slug is already used', async () => { + fetchMock.mock(`end:mail-domains/`, { + status: 400, + body: { + name: 'Mail domain with this Slug already exists.', + }, + }); + + const user = userEvent.setup(); + + render(, { wrapper: AppWrapper }); + + const { inputName, buttonSubmit } = getElements(); + + await user.type(inputName, 'domainfr'); + + await user.click(buttonSubmit); + + await waitFor(() => { + expect( + screen.getByText( + /This mail domain is already used. Please, choose another one./i, + ), + ).toBeInTheDocument(); + }); + + expect(inputName).toHaveFocus(); + + await user.type(inputName, 'domain2fr'); + + expect(buttonSubmit).toBeEnabled(); + }); + + it('displays right error message error when error 500 is received', async () => { + fetchMock.mock(`end:mail-domains/`, { + status: 500, + }); + + const user = userEvent.setup(); + + render(, { wrapper: AppWrapper }); + + const { inputName, buttonSubmit } = getElements(); + + await user.type(inputName, 'domain.fr'); + + await user.click(buttonSubmit); + + await waitFor(() => { + expect( + screen.getByText( + 'Your request cannot be processed because the server is experiencing an error. If the problem ' + + 'persists, please contact our support to resolve the issue: suiteterritoriale@anct.gouv.fr', + ), + ).toBeInTheDocument(); + }); + + expect(inputName).toHaveFocus(); + expect(buttonSubmit).toBeEnabled(); + }); +}); diff --git a/src/frontend/apps/desk/src/features/mail-domains/components/__tests__/ModalCreateMailbox.test.tsx b/src/frontend/apps/desk/src/features/mail-domains/components/__tests__/ModalCreateMailbox.test.tsx new file mode 100644 index 000000000..699cf91c5 --- /dev/null +++ b/src/frontend/apps/desk/src/features/mail-domains/components/__tests__/ModalCreateMailbox.test.tsx @@ -0,0 +1,333 @@ +import { useMutation } from '@tanstack/react-query'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import fetchMock from 'fetch-mock'; +import React from 'react'; + +import { APIError } from '@/api'; +import { AppWrapper } from '@/tests/utils'; + +import { CreateMailboxParams } from '../../api'; +import { MailDomain } from '../../types'; +import { ModalCreateMailbox } from '../ModalCreateMailbox'; + +const mockMailDomain: MailDomain = { + name: 'domain.fr', + id: '456ac6ca-0402-4615-8005-69bc1efde43f', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + slug: 'domainfr', + status: 'enabled', + abilities: { + get: true, + patch: true, + put: true, + post: true, + delete: true, + manage_accesses: true, + }, +}; + +const mockOnSuccess = jest.fn(); +jest.mock('../../api/useCreateMailbox', () => { + const { createMailbox } = jest.requireActual('../../api/useCreateMailbox'); + + return { + useCreateMailbox: jest.fn().mockImplementation(({ onError }) => + useMutation({ + mutationFn: createMailbox, + onSuccess: mockOnSuccess, + onError: (error) => onError(error), + }), + ), + }; +}); + +describe('ModalCreateMailbox', () => { + const mockCloseModal = jest.fn(); + const renderModalCreateMailbox = () => { + return render( + , + { wrapper: AppWrapper }, + ); + }; + + const getFormElements = () => ({ + formTag: screen.getByTitle('Mailbox creation form'), + inputFirstName: screen.getByLabelText(/First name/i), + inputLastName: screen.getByLabelText(/Last name/i), + inputLocalPart: screen.getByLabelText(/Email address prefix/i), + inputEmailAddress: screen.getByLabelText(/Secondary email address/i), + buttonCancel: screen.getByRole('button', { name: /Cancel/i, hidden: true }), + buttonSubmit: screen.getByRole('button', { + name: /Create the mailbox/i, + hidden: true, + }), + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + fetchMock.restore(); + }); + + it('renders all the elements', () => { + renderModalCreateMailbox(); + const { + formTag, + inputFirstName, + inputLastName, + inputLocalPart, + inputEmailAddress, + buttonCancel, + buttonSubmit, + } = getFormElements(); + + expect(formTag).toBeVisible(); + expect(inputFirstName).toBeVisible(); + expect(inputLastName).toBeVisible(); + expect(inputLocalPart).toBeVisible(); + expect(screen.getByText(`@${mockMailDomain.name}`)).toBeVisible(); + expect(inputEmailAddress).toBeVisible(); + expect(buttonCancel).toBeVisible(); + expect(buttonSubmit).toBeVisible(); + }); + + it('clicking on cancel button closes modal', async () => { + const user = userEvent.setup(); + + renderModalCreateMailbox(); + + const { buttonCancel } = getFormElements(); + + expect(buttonCancel).toBeVisible(); + + await user.click(buttonCancel); + + expect(mockCloseModal).toHaveBeenCalled(); + }); + + it('displays validation errors on empty submit', async () => { + const user = userEvent.setup(); + + renderModalCreateMailbox(); + + const { + inputFirstName, + inputLastName, + inputLocalPart, + inputEmailAddress, + buttonSubmit, + } = getFormElements(); + + // To bypass html form validation we need to fill and clear the fields + await user.type(inputFirstName, 'John'); + await user.type(inputLastName, 'Doe'); + await user.type(inputLocalPart, 'john.doe'); + await user.type(inputEmailAddress, 'john.doe@mail.com'); + + await user.clear(inputFirstName); + await user.clear(inputLastName); + await user.clear(inputLocalPart); + await user.clear(inputEmailAddress); + + await user.click(buttonSubmit); + + expect(screen.getByText(`@${mockMailDomain.name}`)).toBeVisible(); + + await waitFor(() => { + expect( + screen.getByText(/Please enter your first name/i), + ).toBeInTheDocument(); + }); + await waitFor(() => { + expect( + screen.getByText(/Please enter your last name/i), + ).toBeInTheDocument(); + }); + + await waitFor(() => { + expect( + screen.getByText(/You must have minimum 1 character/i), + ).toBeInTheDocument(); + }); + + expect(fetchMock.lastUrl()).toBeFalsy(); + expect(buttonSubmit).toBeDisabled(); + }); + + it('submits the form when validation passes', async () => { + fetchMock.mock(`end:mail-domains/${mockMailDomain.slug}/mailboxes/`, 201); + + const user = userEvent.setup(); + + renderModalCreateMailbox(); + + const { + inputFirstName, + inputLastName, + inputLocalPart, + inputEmailAddress, + buttonSubmit, + } = getFormElements(); + + await user.type(inputFirstName, 'John'); + await user.type(inputLastName, 'Doe'); + await user.type(inputLocalPart, 'john.doe'); + await user.type(inputEmailAddress, 'john.doe@mail.com'); + + await user.click(buttonSubmit); + + await waitFor(() => { + expect( + screen.queryByText(/Please enter your first name/i), + ).not.toBeInTheDocument(); + }); + await waitFor(() => { + expect( + screen.queryByText(/Please enter your last name/i), + ).not.toBeInTheDocument(); + }); + + await waitFor(() => { + expect( + screen.queryByText(/You must have minimum 1 character/i), + ).not.toBeInTheDocument(); + }); + + expect(fetchMock.lastOptions()).toEqual({ + body: JSON.stringify({ + first_name: 'John', + last_name: 'Doe', + local_part: 'john.doe', + secondary_email: 'john.doe@mail.com', + }), + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + }); + + expect(mockOnSuccess).toHaveBeenCalled(); + }); + + it('submits the form on key enter press', async () => { + fetchMock.mock(`end:mail-domains/${mockMailDomain.slug}/mailboxes/`, 201); + const user = userEvent.setup(); + + renderModalCreateMailbox(); + + const { + inputFirstName, + inputLastName, + inputLocalPart, + inputEmailAddress, + buttonSubmit, + } = getFormElements(); + + await user.type(inputFirstName, 'John'); + await user.type(inputLastName, 'Doe'); + await user.type(inputLocalPart, 'john.doe'); + + await user.type(inputEmailAddress, 'john.doe@mail.com'); + + await user.type(buttonSubmit, '{enter}'); + + expect(fetchMock.lastOptions()).toEqual({ + body: JSON.stringify({ + first_name: 'John', + last_name: 'Doe', + local_part: 'john.doe', + secondary_email: 'john.doe@mail.com', + }), + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + }); + + expect(mockOnSuccess).toHaveBeenCalled(); + }); + + it('displays right error message error when mailbox prefix is already used', async () => { + // mockCreateMailbox.mockRejectedValueOnce( + // new APIError('Failed to create the mailbox', { + // status: 400, + // cause: ['Mailbox with this Local_part and Domain already exists.'], + // }), + // ); + fetchMock.mock(`end:mail-domains/${mockMailDomain.slug}/mailboxes/`, { + status: 400, + body: { + local_part: 'Mailbox with this Local_part and Domain already exists.', + }, + }); + + const user = userEvent.setup(); + + renderModalCreateMailbox(); + + const { + inputFirstName, + inputLastName, + inputLocalPart, + inputEmailAddress, + buttonSubmit, + } = getFormElements(); + + await user.type(inputFirstName, 'John'); + await user.type(inputLastName, 'Doe'); + await user.type(inputLocalPart, 'john.doe'); + await user.type(inputEmailAddress, 'john.doe@mail.com'); + + await user.click(buttonSubmit); + + await waitFor(() => { + expect( + screen.getByText(/This email prefix is already used./i), + ).toBeInTheDocument(); + }); + + expect(inputLocalPart).toHaveFocus(); + }); + + it('displays right error message error when error 500 is received', async () => { + fetchMock.mock(`end:mail-domains/${mockMailDomain.slug}/mailboxes/`, { + status: 500, + }); + + const user = userEvent.setup(); + + renderModalCreateMailbox(); + + const { + inputFirstName, + inputLastName, + inputLocalPart, + inputEmailAddress, + buttonSubmit, + } = getFormElements(); + + await user.type(inputFirstName, 'John'); + await user.type(inputLastName, 'Doe'); + await user.type(inputLocalPart, 'john.doe'); + await user.type(inputEmailAddress, 'john.doe@mail.com'); + + await user.click(buttonSubmit); + + await waitFor(() => { + expect( + screen.getByText( + 'Your request cannot be processed because the server is experiencing an error. If the problem ' + + 'persists, please contact our support to resolve the issue: suiteterritoriale@anct.gouv.fr', + ), + ).toBeInTheDocument(); + }); + + expect(inputFirstName).toHaveFocus(); + expect(buttonSubmit).toBeEnabled(); + }); +}); diff --git a/src/frontend/apps/desk/src/i18n/translations.json b/src/frontend/apps/desk/src/i18n/translations.json index 403680d85..8acf3f8fd 100644 --- a/src/frontend/apps/desk/src/i18n/translations.json +++ b/src/frontend/apps/desk/src/i18n/translations.json @@ -83,8 +83,10 @@ "List members card": "Carte liste des membres", "Logout": "Se déconnecter", "Mail Domains": "Domaines de messagerie", + "Mail domain addition form": "Formulaire d'ajout de domaine de messagerie", "Mail domains panel": "Panel des domaines de messagerie", "Mailbox created!": "Boîte mail créée !", + "Mailbox creation form": "Formulaire de création de boite mail", "Mailboxes list": "Liste des boîtes mail", "Marianne Logo": "Logo Marianne", "Member": "Membre", @@ -134,6 +136,7 @@ "Teams": "Équipes", "The National Agency for Territorial Cohesion undertakes to make its\n service accessible, in accordance with article 47 of law no. 2005-102\n of February 11, 2005.": "L'Agence Nationale de la Cohésion des Territoires s’engage à rendre son service accessible, conformément à l’article 47 de la loi n° 2005-102 du 11 février 2005.", "The domain name encounters an error. Please contact our support team to solve the problem:": "Le nom de domaine rencontre une erreur. Veuillez contacter notre support pour résoudre le problème :", + "The mail domain secret is misconfigured. Please, contact our support team to solve the issue: suiteterritoriale@anct.gouv.fr": "Le secret du domaine de messagerie est mal configuré. Veuillez contacter notre support pour résoudre le problème : suiteterritoriale@anct.gouv.fr", "The member has been removed from the team": "Le membre a été supprimé de votre groupe", "The role has been updated": "Le rôle a bien été mis à jour", "The team has been removed.": "Le groupe a été supprimé.", @@ -164,6 +167,7 @@ "You cannot update the role of other owner.": "Vous ne pouvez pas mettre à jour les rôles d'autre propriétaire.", "You must have minimum 1 character": "Vous devez entrer au moins 1 caractère", "Your domain name is being validated. You will not be able to create mailboxes until your domain name has been validated by our team.": "Votre nom de domaine est en cours de validation. Vous ne pourrez créer de boîtes mail que lorsque votre nom de domaine sera validé par notre équipe.", + "Your request cannot be processed because the server is experiencing an error. If the problem persists, please contact our support to resolve the issue: suiteterritoriale@anct.gouv.fr": "Votre demande ne peut pas être traitée car le serveur rencontre une erreur. Si le problème persiste, veuillez contacter notre support pour résoudre le problème : suiteterritoriale@anct.gouv.fr", "[disabled]": "[désactivé]", "[enabled]": "[actif]", "[failed]": "[erroné]", diff --git a/src/frontend/apps/desk/src/pages/mail-domains/add.tsx b/src/frontend/apps/desk/src/pages/mail-domains/add.tsx index 2efb6073d..1e4227b95 100644 --- a/src/frontend/apps/desk/src/pages/mail-domains/add.tsx +++ b/src/frontend/apps/desk/src/pages/mail-domains/add.tsx @@ -2,13 +2,13 @@ import React, { ReactElement } from 'react'; import { Box } from '@/components'; import { MailDomainsLayout } from '@/features/mail-domains'; -import { ModalCreateMailDomain } from '@/features/mail-domains/components/ModalAddMailDomain'; +import { ModalAddMailDomain } from '@/features/mail-domains/components/ModalAddMailDomain'; import { NextPageWithLayout } from '@/types/next'; const Page: NextPageWithLayout = () => { return ( - + ); }; diff --git a/src/frontend/apps/e2e/__tests__/app-desk/mail-domain-create-mailbox.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/mail-domain-create-mailbox.spec.ts index 646acbd8e..53c1cfa2c 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/mail-domain-create-mailbox.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/mail-domain-create-mailbox.spec.ts @@ -314,157 +314,4 @@ test.describe('Mail domain create mailbox', () => { page.getByRole('button', { name: 'Create a mailbox' }), ).not.toBeInViewport(); }); - - test('checks client invalidation messages are displayed and no mailbox creation request is sent when fields are not properly filled', async ({ - page, - }) => { - let isCreateMailboxRequestSent = false; - page.on( - 'request', - (request) => - (isCreateMailboxRequestSent = - request.url().includes('/mail-domains/domainfr/mailboxes/') && - request.method() === 'POST'), - ); - - void interceptCommonApiRequests(page); - - await navigateToMailboxCreationFormForMailDomainFr(page); - - const inputFirstName = page.getByLabel('First name'); - const inputLastName = page.getByLabel('Last name'); - const inputLocalPart = page.getByLabel('Email address prefix'); - const inputSecondaryEmailAddress = page.getByLabel( - 'Secondary email address', - ); - const textInvalidLocalPart = page.getByText( - 'It must not contain spaces, accents or special characters (except "." or "-"). E.g.: jean.dupont', - ); - const textInvalidSecondaryEmailAddress = page.getByText( - 'Please enter a valid email address.\nE.g. : jean.dupont@mail.fr', - ); - - await inputFirstName.fill(' '); - await inputFirstName.clear(); - await expect(page.getByText('Please enter your first name')).toBeVisible(); - - await inputLastName.fill(' '); - await inputLastName.clear(); - await expect(page.getByText('Please enter your last name')).toBeVisible(); - - await inputLocalPart.fill('wrong@'); - await expect(textInvalidLocalPart).toBeVisible(); - - await inputSecondaryEmailAddress.fill('uncomplete@mail'); - await expect(textInvalidSecondaryEmailAddress).toBeVisible(); - - await inputLocalPart.clear(); - await inputLocalPart.fill('wrong '); - await expect(textInvalidLocalPart).toBeVisible(); - - await inputLocalPart.clear(); - await expect( - page.getByText('You must have minimum 1 character'), - ).toBeVisible(); - - await page.getByRole('button', { name: 'Create the mailbox' }).click(); - - expect(isCreateMailboxRequestSent).toBeFalsy(); - }); - - test('checks field invalidation messages are displayed when sending already existing local_part data in mail domain to api', async ({ - page, - }) => { - const interceptRequests = (page: Page) => { - void interceptCommonApiRequests(page); - - void page.route( - '**/api/v1.0/mail-domains/domainfr/mailboxes/', - (route) => { - if (route.request().method() === 'POST') { - void route.fulfill({ - status: 400, - json: { - local_part: [ - 'Mailbox with this Local_part and Domain already exists.', - ], - }, - }); - } - }, - { times: 1 }, - ); - }; - - void interceptRequests(page); - - await navigateToMailboxCreationFormForMailDomainFr(page); - - const inputFirstName = page.getByLabel('First name'); - const inputLastName = page.getByLabel('Last name'); - const inputLocalPart = page.getByLabel('Email address prefix'); - const inputSecondaryEmailAddress = page.getByLabel( - 'Secondary email address', - ); - const submitButton = page.getByRole('button', { - name: 'Create the mailbox', - }); - - const textAlreadyUsedLocalPart = page.getByText( - 'This email prefix is already used.', - ); - - await inputFirstName.fill('John'); - await inputLastName.fill('Doe'); - await inputLocalPart.fill('john.already'); - await inputSecondaryEmailAddress.fill('john.already@mail.com'); - - await submitButton.click(); - - await expect(textAlreadyUsedLocalPart).toBeVisible(); - }); - - test('checks unknown api error causes are displayed above form when they are not related with invalid field', async ({ - page, - }) => { - const interceptRequests = async (page: Page) => { - void interceptCommonApiRequests(page); - - await page.route( - '**/api/v1.0/mail-domains/domainfr/mailboxes/', - async (route) => { - if (route.request().method() === 'POST') { - await route.fulfill({ - status: 500, - json: { - unknown_error: ['Unknown error from server'], - }, - }); - } - }, - { times: 1 }, - ); - }; - - void interceptRequests(page); - - await navigateToMailboxCreationFormForMailDomainFr(page); - - const inputFirstName = page.getByLabel('First name'); - const inputLastName = page.getByLabel('Last name'); - const inputLocalPart = page.getByLabel('Email address prefix'); - const inputSecondaryEmailAddress = page.getByLabel( - 'Secondary email address', - ); - - await inputFirstName.fill('John'); - await inputLastName.fill('Doe'); - await inputLocalPart.fill('john.doe'); - - await inputSecondaryEmailAddress.fill('john.do@mail.fr'); - - await page.getByRole('button', { name: 'Create the mailbox' }).click(); - - await expect(page.getByText('Unknown error from server')).toBeVisible(); - }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/mail-domains-add.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/mail-domains-add.spec.ts index 6ef300dd0..beb194e07 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/mail-domains-add.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/mail-domains-add.spec.ts @@ -132,142 +132,6 @@ test.describe('Add Mail Domains', () => { ).toBeVisible(); }); - test('checks form submits at "Enter" key press', async ({ page }) => { - void page.route('**/api/v1.0/mail-domains/', (route) => { - if (route.request().method() === 'POST') { - void route.fulfill({ - json: { - id: '2ebcfcfb-1dfa-4ed1-8e4a-554c63307b7c', - name: 'enter.fr', - slug: 'enterfr', - status: 'pending', - abilities: { - get: true, - patch: true, - put: true, - post: true, - delete: true, - manage_accesses: true, - }, - created_at: '2024-08-21T10:55:21.081994Z', - updated_at: '2024-08-21T10:55:21.082109Z', - }, - }); - } else { - void route.continue(); - } - }); - - await page.goto('/mail-domains/'); - - const { linkIndexPageAddDomain, inputName } = getElements(page); - - await linkIndexPageAddDomain.click(); - - await inputName.fill('enter.fr'); - await page.keyboard.press('Enter'); - - await expect(page).toHaveURL(`/mail-domains/enterfr/`); - }); - - test('checks error when duplicate mail domain name', async ({ - page, - browserName, - }) => { - await page.goto('/mail-domains/'); - - const { linkIndexPageAddDomain, inputName, buttonSubmit } = - getElements(page); - - const mailDomainName = randomName('duplicate.fr', browserName, 1)[0]; - const mailDomainSlug = mailDomainName.replace('.', ''); - - await linkIndexPageAddDomain.click(); - await inputName.fill(mailDomainName); - await buttonSubmit.click(); - - await expect(page).toHaveURL(`/mail-domains\/${mailDomainSlug}\/`); - - await linkIndexPageAddDomain.click(); - - await inputName.fill(mailDomainName); - await buttonSubmit.click(); - - await expect(page).toHaveURL(/mail-domains\//); - await expect( - page.getByText( - 'This mail domain is already used. Please, choose another one.', - ), - ).toBeVisible(); - await expect(inputName).toBeFocused(); - }); - - test('checks error when duplicate mail domain slug', async ({ - page, - browserName, - }) => { - await page.goto('/mail-domains/'); - - const { linkIndexPageAddDomain, inputName, buttonSubmit } = - getElements(page); - - const mailDomainSlug = randomName('duplicate', browserName, 1)[0]; - - await linkIndexPageAddDomain.click(); - await inputName.fill(mailDomainSlug); - await buttonSubmit.click(); - - await expect(page).toHaveURL(`/mail-domains\/${mailDomainSlug}\/`); - - await linkIndexPageAddDomain.click(); - - await inputName.fill(mailDomainSlug); - await buttonSubmit.click(); - - await expect(page).toHaveURL(/mail-domains\//); - await expect( - page.getByText( - 'This mail domain is already used. Please, choose another one.', - ), - ).toBeVisible(); - await expect(inputName).toBeFocused(); - }); - - test('checks unknown api error causes are displayed', async ({ page }) => { - await page.route( - '**/api/v1.0/mail-domains/', - async (route) => { - if (route.request().method() === 'POST') { - await route.fulfill({ - status: 500, - json: { - unknown_error: ['Unknown error from server'], - }, - }); - } - }, - { times: 1 }, - ); - - await page.goto('/mail-domains/'); - - const { linkIndexPageAddDomain, inputName, buttonSubmit } = - getElements(page); - - await linkIndexPageAddDomain.click(); - await inputName.fill('server-error.fr'); - await buttonSubmit.click(); - - await expect(page).toHaveURL(/mail-domains\//); - await expect( - page.getByText( - 'Your request cannot be processed because the server is experiencing an error. If the problem ' + - 'persists, please contact our support to resolve the issue: suiteterritoriale@anct.gouv.fr.', - ), - ).toBeVisible(); - await expect(inputName).toBeFocused(); - }); - test('checks 404 on mail-domains/[slug] page', async ({ page }) => { await page.goto('/mail-domains/unknown-domain');