🥅(frontend) improve error catching in forms

- 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)
This commit is contained in:
daproclaima
2024-09-04 23:23:13 +02:00
committed by Sebastien Nobour
parent 25898bbb64
commit e4aed82ff2
13 changed files with 737 additions and 411 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -5,7 +5,13 @@ import { MailDomain } from '@/features/mail-domains';
import { KEY_LIST_MAIL_DOMAIN } from './useMailDomains';
export const createMailDomain = async (name: string): Promise<MailDomain> => {
export interface AddMailDomainParams {
name: string;
}
export const addMailDomain = async (
name: AddMailDomainParams['name'],
): Promise<MailDomain> => {
const response = await fetchAPI(`mail-domains/`, {
method: 'POST',
body: JSON.stringify({
@@ -23,19 +29,24 @@ export const createMailDomain = async (name: string): Promise<MailDomain> => {
return response.json() as Promise<MailDomain>;
};
export function useCreateMailDomain({
export const useAddMailDomain = ({
onSuccess,
onError,
}: {
onSuccess: (data: MailDomain) => void;
}) {
onError: (error: APIError) => void;
}) => {
const queryClient = useQueryClient();
return useMutation<MailDomain, APIError, string>({
mutationFn: createMailDomain,
mutationFn: addMailDomain,
onSuccess: (data) => {
void queryClient.invalidateQueries({
queryKey: [KEY_LIST_MAIL_DOMAIN],
});
onSuccess(data);
},
onError: (error) => {
onError(error);
},
});
}
};

View File

@@ -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<void, APIError, CreateMailboxParams>({
mutationFn: createMailbox,
@@ -61,4 +60,4 @@ export function useCreateMailbox(options: UseCreateMailboxParams) {
}
},
});
}
};

View File

@@ -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 ? (
<CreateMailboxForm
<ModalCreateMailbox
mailDomain={mailDomain}
closeModal={() => setIsCreateMailboxFormVisible(false)}
/>

View File

@@ -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 | string[]>(
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<string[]>([]);
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;
}
const errorCauses = useAddMailDomainApiError({ error, methods });
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 onSubmitCallback = (event: React.FormEvent) => {
event.preventDefault();
void methods.handleSubmit(({ name }) => {
void createMailDomain(name);
void addMailDomain(name);
})();
};
@@ -139,7 +111,11 @@ export const ModalAddMailDomain = () => {
<Button
type="submit"
form={FORM_ID}
disabled={!methods.watch('name') || isPending}
disabled={
methods.formState.isSubmitting ||
!methods.formState.isValid ||
isPending
}
>
{t('Add the domain')}
</Button>
@@ -163,7 +139,11 @@ export const ModalAddMailDomain = () => {
) : null}
<FormProvider {...methods}>
<form id={FORM_ID} onSubmit={onSubmitCallback}>
<form
id={FORM_ID}
onSubmit={onSubmitCallback}
title={t('Mail domain addition form')}
>
<Controller
control={methods.control}
name="name"

View File

@@ -6,7 +6,7 @@ import {
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import React from 'react';
import React, { useState } from 'react';
import {
Controller,
FormProvider,
@@ -17,11 +17,12 @@ import { useTranslation } from 'react-i18next';
import { createGlobalStyle } from 'styled-components';
import { z } from 'zod';
import { parseAPIError } from '@/api/parseAPIError';
import { Box, Text, TextErrors } from '@/components';
import { Modal } from '@/components/Modal';
import { CreateMailboxParams, useCreateMailbox } from '../../api';
import { MailDomain } from '../../types';
import { CreateMailboxParams, useCreateMailbox } from '../api';
import { MailDomain } from '../types';
const FORM_ID: string = 'form-create-mailbox';
@@ -32,7 +33,7 @@ const GlobalStyle = createGlobalStyle`
}
`;
export const CreateMailboxForm = ({
export const ModalCreateMailbox = ({
mailDomain,
closeModal,
}: {
@@ -42,6 +43,8 @@ export const CreateMailboxForm = ({
const { t } = useTranslation();
const { toast } = useToastProvider();
const [errorCauses, setErrorCauses] = useState<string[]>([]);
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 (
<FormProvider {...methods}>
<Modal
@@ -132,7 +167,11 @@ export const CreateMailboxForm = ({
fullWidth
type="submit"
form={FORM_ID}
disabled={methods.formState.isSubmitting}
disabled={
methods.formState.isSubmitting ||
!methods.formState.isValid ||
isPending
}
>
{t('Create the mailbox')}
</Button>
@@ -152,8 +191,12 @@ export const CreateMailboxForm = ({
>
<GlobalStyle />
<Box $width="100%" $margin={{ top: 'none', bottom: 'xl' }}>
{!!causes?.length && (
<TextErrors $margin={{ bottom: 'small' }} causes={causes} />
{!!errorCauses?.length && (
<TextErrors
$margin={{ bottom: 'small' }}
causes={errorCauses}
$textAlign="left"
/>
)}
<Text
$margin={{ horizontal: 'none', vertical: 'big' }}
@@ -188,7 +231,11 @@ const Form = ({
const { t } = useTranslation();
return (
<form onSubmit={onSubmitCallback} id={FORM_ID}>
<form
onSubmit={onSubmitCallback}
id={FORM_ID}
title={t('Mailbox creation form')}
>
<Box $direction="column" $width="100%" $gap="2rem" $margin="auto">
<Box $margin={{ horizontal: 'none' }}>
<FieldMailBox

View File

@@ -0,0 +1,241 @@
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 { AppWrapper } from '@/tests/utils';
import { ModalAddMailDomain } from '../ModalAddMailDomain';
const mockPush = jest.fn();
jest.mock('next/navigation', () => ({
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(<ModalAddMailDomain />, { 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(<ModalAddMailDomain />, { 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(<ModalAddMailDomain />, { 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(<ModalAddMailDomain />, { 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(<ModalAddMailDomain />, { 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(<ModalAddMailDomain />, { 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(<ModalAddMailDomain />, { 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(<ModalAddMailDomain />, { 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();
});
});

View File

@@ -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<void, APIError, CreateMailboxParams>({
mutationFn: createMailbox,
onSuccess: mockOnSuccess,
onError: (error) => onError(error),
}),
),
};
});
describe('ModalCreateMailbox', () => {
const mockCloseModal = jest.fn();
const renderModalCreateMailbox = () => {
return render(
<ModalCreateMailbox
mailDomain={mockMailDomain}
closeModal={mockCloseModal}
/>,
{ 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();
});
});

View File

@@ -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 sengage à rendre son service accessible, conformément à larticle 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é]",

View File

@@ -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 (
<Box $padding="large" $height="inherit">
<ModalCreateMailDomain />
<ModalAddMailDomain />
</Box>
);
};

View File

@@ -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();
});
});

View File

@@ -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');