🚸(frontend) improve mailbox creation validations and error handling

- replace known error causes returned by the API on unsuccessful mailbox
creation requests by meaningful interpolated message shown above the
form
- strengthen form validation rules to be identical as those of
the api endpoint to prevent emitting invalid requests
- designate which form fields are mandatory for accessiblity
- update texts for better ux writting, and their translations
- fix css style input errors
- update related e2e tests.
This commit is contained in:
daproclaima
2024-07-04 12:09:18 +02:00
committed by Sebastien Nobour
parent cda4373544
commit 32e6602dda
5 changed files with 348 additions and 201 deletions

View File

@@ -93,6 +93,16 @@
0 0 2px; 0 0 2px;
} }
.c__input__wrapper--error.c__input__wrapper:focus-within {
border-color: var(
--c--components--forms-input--border--color-error-hover
) !important;
}
.c__input__wrapper--error.c__input__wrapper:focus-within label {
color: var(--c--theme--colors--danger-600);
}
.c__input__wrapper--error:not(.c__input__wrapper--disabled):hover label { .c__input__wrapper--error:not(.c__input__wrapper--disabled):hover label {
color: var(--c--components--forms-input--border--color-error-hover); color: var(--c--components--forms-input--border--color-error-hover);
} }

View File

@@ -85,7 +85,7 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
{isCreateMailboxFormVisible && mailDomain ? ( {isCreateMailboxFormVisible && mailDomain ? (
<CreateMailboxForm <CreateMailboxForm
mailDomain={mailDomain} mailDomain={mailDomain}
setIsFormVisible={setIsCreateMailboxFormVisible} closeModal={() => setIsCreateMailboxFormVisible(false)}
/> />
) : null} ) : null}
<TopBanner <TopBanner

View File

@@ -15,6 +15,7 @@ import {
useForm, useForm,
} from 'react-hook-form'; } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { createGlobalStyle } from 'styled-components';
import { z } from 'zod'; import { z } from 'zod';
import { Box, Text, TextErrors } from '@/components'; import { Box, Text, TextErrors } from '@/components';
@@ -26,24 +27,53 @@ import { MailDomain } from '../../types';
const FORM_ID: string = 'form-create-mailbox'; const FORM_ID: string = 'form-create-mailbox';
const createMailboxValidationSchema = z.object({ const GlobalStyle = createGlobalStyle`
first_name: z.string().min(1), .c__field__footer__top > .c__field__text {
last_name: z.string().min(1), white-space: pre-line;
local_part: z.string().min(1), }
secondary_email: z.string().min(1), `;
});
export const CreateMailboxForm = ({ export const CreateMailboxForm = ({
mailDomain, mailDomain,
setIsFormVisible, closeModal,
}: { }: {
mailDomain: MailDomain; mailDomain: MailDomain;
setIsFormVisible: (value: boolean) => void; closeModal: () => void;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToastProvider(); const { toast } = useToastProvider();
const { colorsTokens } = useCunninghamTheme(); const { colorsTokens } = useCunninghamTheme();
const messageInvalidMinChar = t('You must have minimum 1 character');
const createMailboxValidationSchema = z.object({
first_name: z
.string()
.min(
1,
t('Please enter {{fieldName}}', { fieldName: 'your first name' }),
),
last_name: z
.string()
.min(1, t('Please enter {{fieldName}}', { fieldName: 'your last name' })),
local_part: z
.string()
.regex(
/^((?!@|\s)([a-zA-Z0-9.\-]))*$/,
t(
'It must not contain spaces, accents or special characters (except "." or "-"). E.g.: jean.dupont',
),
)
.min(1, messageInvalidMinChar),
secondary_email: z
.string()
.regex(
/[^@\s]+@[^@\s]+\.[^@\s]+/,
t('Please enter a valid email address.\nE.g. : jean.dupont@mail.fr'),
)
.min(1, messageInvalidMinChar),
});
const methods = useForm<CreateMailboxParams>({ const methods = useForm<CreateMailboxParams>({
delayError: 0, delayError: 0,
defaultValues: { defaultValues: {
@@ -57,19 +87,17 @@ export const CreateMailboxForm = ({
resolver: zodResolver(createMailboxValidationSchema), resolver: zodResolver(createMailboxValidationSchema),
}); });
const { mutate: createMailbox, ...queryState } = useCreateMailbox({ const { mutate: createMailbox, error } = useCreateMailbox({
mailDomainSlug: mailDomain.slug, mailDomainSlug: mailDomain.slug,
onSuccess: () => { onSuccess: () => {
toast(t('Mailbox created!'), VariantType.SUCCESS, { toast(t('Mailbox created!'), VariantType.SUCCESS, {
duration: 4000, duration: 4000,
}); });
setIsFormVisible(false); closeModal();
}, },
}); });
const closeModal = () => setIsFormVisible(false);
const onSubmitCallback = (event: React.FormEvent) => { const onSubmitCallback = (event: React.FormEvent) => {
event.preventDefault(); event.preventDefault();
void methods.handleSubmit((data) => void methods.handleSubmit((data) =>
@@ -77,6 +105,20 @@ 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 ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<Modal <Modal
@@ -102,7 +144,7 @@ export const CreateMailboxForm = ({
form={FORM_ID} form={FORM_ID}
disabled={methods.formState.isSubmitting} disabled={methods.formState.isSubmitting}
> >
{t('Submit')} {t('Create the mailbox')}
</Button> </Button>
} }
size={ModalSize.MEDIUM} size={ModalSize.MEDIUM}
@@ -113,15 +155,22 @@ export const CreateMailboxForm = ({
color={colorsTokens()['primary-text']} color={colorsTokens()['primary-text']}
title={t('Mailbox creation form')} title={t('Mailbox creation form')}
/> />
<Text $size="h3" $margin="none" role="heading" aria-level={3}> <Text
$size="h3"
$margin="none"
role="heading"
aria-level={3}
title={t('Create a mailbox')}
>
{t('Create a mailbox')} {t('Create a mailbox')}
</Text> </Text>
</Box> </Box>
} }
> >
<Box $width="100%" $margin={{ top: 'large', bottom: 'xl' }}> <GlobalStyle />
{queryState.isError && ( <Box $width="100%" $margin={{ top: 'none', bottom: 'xl' }}>
<TextErrors className="mb-s" causes={queryState.error.cause} /> {!!causes?.length && (
<TextErrors $margin={{ bottom: 'small' }} causes={causes} />
)} )}
{methods ? ( {methods ? (
<Form <Form
@@ -151,76 +200,39 @@ const Form = ({
<form onSubmit={onSubmitCallback} id={FORM_ID}> <form onSubmit={onSubmitCallback} id={FORM_ID}>
<Box $direction="column" $width="100%" $gap="2rem" $margin="auto"> <Box $direction="column" $width="100%" $gap="2rem" $margin="auto">
<Box $margin={{ horizontal: 'none' }}> <Box $margin={{ horizontal: 'none' }}>
<Controller <FieldMailBox
control={methods.control}
name="first_name" name="first_name"
render={({ fieldState }) => ( label={t('First name')}
<Input methods={methods}
aria-invalid={!!fieldState.error}
label={t('First name')}
state={fieldState.error ? 'error' : 'default'}
text={
fieldState.error
? t('Please enter your first name')
: undefined
}
{...methods.register('first_name')}
/>
)}
/> />
</Box> </Box>
<Box $margin={{ horizontal: 'none' }}> <Box $margin={{ horizontal: 'none' }}>
<Controller <FieldMailBox
control={methods.control}
name="last_name" name="last_name"
render={({ fieldState }) => ( label={t('Last name')}
<Input methods={methods}
aria-invalid={!!fieldState.error}
label={t('Last name')}
state={fieldState.error ? 'error' : 'default'}
text={
fieldState.error
? t('Please enter your last name')
: undefined
}
{...methods.register('last_name')}
/>
)}
/> />
</Box> </Box>
<Box $margin={{ horizontal: 'none' }} $direction="row"> <Box $margin={{ horizontal: 'none' }} $direction="row">
<Box $width="65%"> <Box $width="65%">
<Controller <FieldMailBox
control={methods.control}
name="local_part" name="local_part"
render={({ fieldState }) => ( label={t('Email address prefix')}
<Input methods={methods}
aria-invalid={!!fieldState.error} text={t(
label={t('Main email address')} 'It must not contain spaces, accents or special characters (except "." or "-"). E.g.: jean.dupont',
state={fieldState.error ? 'error' : 'default'}
text={
fieldState.error
? t(
'Please enter the first part of the email address, without including "@" in it',
)
: undefined
}
{...methods.register('local_part')}
/>
)} )}
/> />
</Box> </Box>
<Text <Text
as="span"
$theme="primary" $theme="primary"
$size="1rem" $size="1rem"
$display="inline-block" $display="inline-block"
$css={` $css={`
position: relative; position: relative;
display: inline-block;
left: 1rem; left: 1rem;
top: 1rem; top: 1rem;
`} `}
@@ -230,25 +242,41 @@ const Form = ({
</Box> </Box>
<Box $margin={{ horizontal: 'none' }}> <Box $margin={{ horizontal: 'none' }}>
<Controller <FieldMailBox
control={methods.control}
name="secondary_email" name="secondary_email"
render={({ fieldState }) => ( label={t('Secondary email address')}
<Input methods={methods}
aria-invalid={!!fieldState.error} text={t('E.g. : jean.dupont@mail.fr')}
label={t('Secondary email address')}
state={fieldState.error ? 'error' : 'default'}
text={
fieldState.error
? t('Please enter your secondary email address')
: undefined
}
{...methods.register('secondary_email')}
/>
)}
/> />
</Box> </Box>
</Box> </Box>
</form> </form>
); );
}; };
interface FieldMailBoxProps {
name: 'first_name' | 'last_name' | 'local_part' | 'secondary_email';
label: string;
methods: UseFormReturn<CreateMailboxParams>;
text?: string;
}
const FieldMailBox = ({ name, label, methods, text }: FieldMailBoxProps) => {
return (
<Controller
control={methods.control}
name={name}
render={({ fieldState }) => (
<Input
aria-invalid={!!fieldState.error}
aria-required
required
label={label}
state={fieldState.error ? 'error' : 'default'}
text={fieldState?.error?.message ? fieldState.error.message : text}
{...methods.register(name)}
/>
)}
/>
);
};

View File

@@ -18,6 +18,7 @@
"Add team icon": "Icône ajout de groupe", "Add team icon": "Icône ajout de groupe",
"Address: National Agency for Territorial Cohesion - 20, avenue de Ségur TSA 10717 75 334 Paris Cedex 07 Paris": "Adresse : Agence Nationale de la Cohésion des Territoires - 20, avenue de Ségur TSA 10717 75 334 Paris Cedex 07", "Address: National Agency for Territorial Cohesion - 20, avenue de Ségur TSA 10717 75 334 Paris Cedex 07 Paris": "Adresse : Agence Nationale de la Cohésion des Territoires - 20, avenue de Ségur TSA 10717 75 334 Paris Cedex 07",
"Administration": "Administration", "Administration": "Administration",
"All fields are mandatory.": "Tous les champs sont obligatoires.",
"Are you sure you want to delete {{teamName}} team?": "Êtes-vous sûr de vouloir supprimer le groupe {{teamName}}?", "Are you sure you want to delete {{teamName}} team?": "Êtes-vous sûr de vouloir supprimer le groupe {{teamName}}?",
"Are you sure you want to remove this member from the {{team}} group?": "Êtes-vous sûr de vouloir supprimer ce membre du groupe {{team}}?", "Are you sure you want to remove this member from the {{team}} group?": "Êtes-vous sûr de vouloir supprimer ce membre du groupe {{team}}?",
"Back to home page": "Retour à l'accueil", "Back to home page": "Retour à l'accueil",
@@ -37,6 +38,7 @@
"Create a new group": "Créer un nouveau groupe", "Create a new group": "Créer un nouveau groupe",
"Create a new team": "Créer un nouveau groupe", "Create a new team": "Créer un nouveau groupe",
"Create new team card": "Carte créer un nouveau groupe", "Create new team card": "Carte créer un nouveau groupe",
"Create the mailbox": "Créer la boîte mail",
"Create the team": "Créer le groupe", "Create the team": "Créer le groupe",
"Create your first team by clicking on the \"Create a new team\" button.": "Créez votre premier groupe en cliquant sur le bouton \"Créer un nouveau groupe\".", "Create your first team by clicking on the \"Create a new team\" button.": "Créez votre premier groupe en cliquant sur le bouton \"Créer un nouveau groupe\".",
"Created at": "Créé le", "Created at": "Créé le",
@@ -45,6 +47,8 @@
"Delete the team": "Supprimer le groupe", "Delete the team": "Supprimer le groupe",
"Deleting the {{teamName}} team": "Suppression du groupe {{teamName}}", "Deleting the {{teamName}} team": "Suppression du groupe {{teamName}}",
"E-mail:": "E-mail:", "E-mail:": "E-mail:",
"E.g. : jean.dupont@mail.fr": "Ex. : jean.dupont@mail.fr",
"Email address prefix": "Préfixe de l'adresse mail",
"Emails": "Emails", "Emails": "Emails",
"Empty teams icon": "Icône de groupe vide", "Empty teams icon": "Icône de groupe vide",
"Enter the new name of the selected team": "Entrez le nouveau nom du groupe sélectionné", "Enter the new name of the selected team": "Entrez le nouveau nom du groupe sélectionné",
@@ -61,6 +65,7 @@
"Improvement and contact": "Amélioration et contact", "Improvement and contact": "Amélioration et contact",
"Invitation sent to {{email}}": "Invitation envoyée à {{email}}", "Invitation sent to {{email}}": "Invitation envoyée à {{email}}",
"Invite new members to {{teamName}}": "Invitez de nouveaux membres à rejoindre {{teamName}}", "Invite new members to {{teamName}}": "Invitez de nouveaux membres à rejoindre {{teamName}}",
"It must not contain spaces, accents or special characters (except \".\" or \"-\"). E.g.: jean.dupont": "Il ne doit pas contenir d'espaces, d'accents ou de caractères spéciaux (excepté \".\" ou \"-\"). Ex. : jean.dupont",
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Il semble que la page que vous cherchez n'existe pas ou ne puisse pas être affichée correctement.", "It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Il semble que la page que vous cherchez n'existe pas ou ne puisse pas être affichée correctement.",
"It's true, you didn't have to click on a block that covers half the page to say you agree to the placement of cookies — even if you don't know what it means!": "C'est vrai, vous n'avez pas à cliquer sur un bloc qui couvre la moitié de la page pour dire que vous acceptez le placement de cookies — même si vous ne savez pas ce que cela signifie !", "It's true, you didn't have to click on a block that covers half the page to say you agree to the placement of cookies — even if you don't know what it means!": "C'est vrai, vous n'avez pas à cliquer sur un bloc qui couvre la moitié de la page pour dire que vous acceptez le placement de cookies — même si vous ne savez pas ce que cela signifie !",
"La Régie (Suite Territoriale) is non-compliant with the RGAA. The site has not yet been audited.": "La Régie (Suite Territoriale) est non conforme avec le RGAA. Le site na encore pas été audité.", "La Régie (Suite Territoriale) is non-compliant with the RGAA. The site has not yet been audited.": "La Régie (Suite Territoriale) est non conforme avec le RGAA. Le site na encore pas été audité.",
@@ -76,9 +81,7 @@
"Mail Domains": "Domaines de messagerie", "Mail Domains": "Domaines de messagerie",
"Mail Domains icon": "Icône des domaines mail", "Mail Domains icon": "Icône des domaines mail",
"Mailbox created!": "Boîte mail créée !", "Mailbox created!": "Boîte mail créée !",
"Mailbox creation form": "Formulaire de création de boîte mail",
"Mailboxes list": "Liste des boîtes mail", "Mailboxes list": "Liste des boîtes mail",
"Main email address": "Adresse e-mail principale",
"Marianne Logo": "Logo Marianne", "Marianne Logo": "Logo Marianne",
"Member": "Membre", "Member": "Membre",
"Member icon": "Icône de membre", "Member icon": "Icône de membre",
@@ -97,10 +100,8 @@
"Ouch !": "Ouch !", "Ouch !": "Ouch !",
"Owner": "Propriétaire", "Owner": "Propriétaire",
"Personal data and cookies": "Données personnelles et cookies", "Personal data and cookies": "Données personnelles et cookies",
"Please enter the first part of the email address, without including \"@\" in it": "Veuillez entrer la première partie de l'adresse e-mail, sans y inclure \"@\"", "Please enter a valid email address.\nE.g. : jean.dupont@mail.fr": "Veuillez entrer une adresse e-mail valide.\nEx. : jean.dupont@mail.fr",
"Please enter your first name": "Veuillez saisir votre prénom", "Please enter {{fieldName}}": "Veuillez entrer {{fieldName}}",
"Please enter your last name": "Veuillez saisir votre nom",
"Please enter your secondary email address": "Veuillez saisir votre adresse e-mail secondaire",
"Publication Director": "Directeur de la publication", "Publication Director": "Directeur de la publication",
"Publisher": "Éditeur", "Publisher": "Éditeur",
"Radio buttons to update the roles": "Boutons radio pour mettre à jour les rôles", "Radio buttons to update the roles": "Boutons radio pour mettre à jour les rôles",
@@ -123,7 +124,6 @@
"Sort the teams by creation date ascendent": "Trier les groupes par date de création ascendante", "Sort the teams by creation date ascendent": "Trier les groupes par date de création ascendante",
"Sort the teams by creation date descendent": "Trier les groupes par date de création descendante", "Sort the teams by creation date descendent": "Trier les groupes par date de création descendante",
"Stéphanie Schaer: Interministerial Digital Director (DINUM).": "Stéphanie Schaer: Directrice numérique interministériel (DINUM).", "Stéphanie Schaer: Interministerial Digital Director (DINUM).": "Stéphanie Schaer: Directrice numérique interministériel (DINUM).",
"Submit": "Valider",
"Team name": "Nom du groupe", "Team name": "Nom du groupe",
"Teams": "Équipes", "Teams": "Équipes",
"Teams icon": "Icône de groupe", "Teams icon": "Icône de groupe",
@@ -135,6 +135,7 @@
"The team in charge of the digital workspace \"La Suite numérique\" can be contacted directly at": "L'équipe responsable de l'espace de travail numérique \"La Suite numérique\" peut être contactée directement à l'adresse", "The team in charge of the digital workspace \"La Suite numérique\" can be contacted directly at": "L'équipe responsable de l'espace de travail numérique \"La Suite numérique\" peut être contactée directement à l'adresse",
"This accessibility statement applies to La Régie (Suite Territoriale)": "Cette déclaration daccessibilité sapplique à La Régie (Suite Territoriale)", "This accessibility statement applies to La Régie (Suite Territoriale)": "Cette déclaration daccessibilité sapplique à La Régie (Suite Territoriale)",
"This allows us to measure the number of visits and understand which pages are the most viewed.": "Cela nous permet de mesurer le nombre de visites et de comprendre quelles pages sont les plus consultées.", "This allows us to measure the number of visits and understand which pages are the most viewed.": "Cela nous permet de mesurer le nombre de visites et de comprendre quelles pages sont les plus consultées.",
"This email prefix is already used.": "Ce préfixe d'email est déjà utilisé.",
"This procedure is to be used in the following case: you have reported to the website \n manager an accessibility defect which prevents you from accessing content or one of the \n portal's services and you have not obtained a satisfactory response.": "Cette procédure est à utiliser dans le cas suivant : vous avez signalé au responsable du site internet un défaut daccessibilité qui vous empêche daccéder à un contenu ou à un des services du portail et vous navez pas obtenu de réponse satisfaisante.", "This procedure is to be used in the following case: you have reported to the website \n manager an accessibility defect which prevents you from accessing content or one of the \n portal's services and you have not obtained a satisfactory response.": "Cette procédure est à utiliser dans le cas suivant : vous avez signalé au responsable du site internet un défaut daccessibilité qui vous empêche daccéder à un contenu ou à un des services du portail et vous navez pas obtenu de réponse satisfaisante.",
"This site does not display a cookie consent banner, why?": "Ce site n'affiche pas de bannière de consentement des cookies, pourquoi?", "This site does not display a cookie consent banner, why?": "Ce site n'affiche pas de bannière de consentement des cookies, pourquoi?",
"This site places a small text file (a \"cookie\") on your computer when you visit it.": "Ce site place un petit fichier texte (un « cookie ») sur votre ordinateur lorsque vous le visitez.", "This site places a small text file (a \"cookie\") on your computer when you visit it.": "Ce site place un petit fichier texte (un « cookie ») sur votre ordinateur lorsque vous le visitez.",
@@ -153,6 +154,7 @@
"You can:": "Vous pouvez :", "You can:": "Vous pouvez :",
"You cannot remove other owner.": "Vous ne pouvez pas supprimer un autre propriétaire.", "You cannot remove other owner.": "Vous ne pouvez pas supprimer un autre propriétaire.",
"You cannot update the role of other owner.": "Vous ne pouvez pas mettre à jour les rôles d'autre propriétaire.", "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",
"accessibility-contact-defenseurdesdroits": "Contacter le délégué du<1>Défenseur des droits dans votre région</1>", "accessibility-contact-defenseurdesdroits": "Contacter le délégué du<1>Défenseur des droits dans votre région</1>",
"accessibility-form-defenseurdesdroits": "Écrire un message au<1>Défenseur des droits</1>", "accessibility-form-defenseurdesdroits": "Écrire un message au<1>Défenseur des droits</1>",
"icon group": "icône groupe", "icon group": "icône groupe",

View File

@@ -41,8 +41,57 @@ const mailDomainsFixtures: MailDomain[] = [
const mailDomainDomainFrFixture = mailDomainsFixtures[0]; const mailDomainDomainFrFixture = mailDomainsFixtures[0];
const clickOnMailDomainsNavButton = async (page: Page): Promise<void> => const mailboxesFixtures = {
domainFr: {
page1: Array.from({ length: 1 }, (_, i) => ({
id: `456ac6ca-0402-4615-8005-69bc1efde${i}f`,
local_part: `local_part-${i}`,
secondary_email: `secondary_email-${i}`,
})),
},
};
const interceptCommonApiRequests = (page: Page) => {
void page.route('**/api/v1.0/mail-domains/?page=*', (route) => {
void route.fulfill({
json: {
count: mailDomainsFixtures.length,
next: null,
previous: null,
results: mailDomainsFixtures,
},
});
});
void page.route('**/api/v1.0/mail-domains/domainfr', (route) => {
void route.fulfill({
json: mailDomainDomainFrFixture,
});
});
void page.route(
'**/api/v1.0/mail-domains/domainfr/mailboxes/?page=1**',
(route) => {
void route.fulfill({
json: {
count: mailboxesFixtures.domainFr.page1.length,
next: null,
previous: null,
results: mailboxesFixtures.domainFr.page1,
},
});
},
{ times: 1 },
);
};
const navigateToMailboxCreationFormForMailDomainFr = async (
page: Page,
): Promise<void> => {
await page.locator('menu').first().getByLabel(`Mail Domains button`).click(); await page.locator('menu').first().getByLabel(`Mail Domains button`).click();
await page.getByRole('listbox').first().getByText('domain.fr').click();
await page.getByRole('button', { name: 'Create a mailbox' }).click();
};
test.describe('Mail domain create mailbox', () => { test.describe('Mail domain create mailbox', () => {
test.beforeEach(async ({ page, browserName }) => { test.beforeEach(async ({ page, browserName }) => {
@@ -54,57 +103,19 @@ test.describe('Mail domain create mailbox', () => {
test('checks user can create a mailbox for a mail domain', async ({ test('checks user can create a mailbox for a mail domain', async ({
page, page,
}) => { }) => {
const mailboxesFixtures = {
domainFr: {
page1: Array.from({ length: 1 }, (_, i) => ({
id: `456ac6ca-0402-4615-8005-69bc1efde${i}f`,
local_part: `local_part-${i}`,
secondary_email: `secondary_email-${i}`,
})),
},
};
const newMailbox = { const newMailbox = {
id: '04433733-c9b7-453a-8122-755ac115bb00', id: '04433733-c9b7-453a-8122-755ac115bb00',
local_part: 'john.doe', local_part: 'john.doe',
secondary_email: 'john.doe@mail.com', secondary_email: 'john.doe-complex2024@mail.com',
}; };
const interceptApiCalls = async () => { const interceptRequests = (page: Page) => {
await page.route('**/api/v1.0/mail-domains/?page=*', async (route) => { void interceptCommonApiRequests(page);
await route.fulfill({
json: {
count: mailDomainsFixtures.length,
next: null,
previous: null,
results: mailDomainsFixtures,
},
});
});
await page.route('**/api/v1.0/mail-domains/domainfr', async (route) => {
await route.fulfill({
json: mailDomainDomainFrFixture,
});
});
await page.route(
'**/api/v1.0/mail-domains/domainfr/mailboxes/?page=1**',
async (route) => {
await route.fulfill({
json: {
count: mailboxesFixtures.domainFr.page1.length,
next: null,
previous: null,
results: mailboxesFixtures.domainFr.page1,
},
});
},
{ times: 1 },
);
await page.route( void page.route(
'**/api/v1.0/mail-domains/domainfr/mailboxes/?page=1**', '**/api/v1.0/mail-domains/domainfr/mailboxes/?page=1**',
async (route) => { (route) => {
await route.fulfill({ void route.fulfill({
json: { json: {
count: [...mailboxesFixtures.domainFr.page1, newMailbox].length, count: [...mailboxesFixtures.domainFr.page1, newMailbox].length,
next: null, next: null,
@@ -115,7 +126,7 @@ test.describe('Mail domain create mailbox', () => {
}, },
); );
await page.route( void page.route(
'**/api/v1.0/mail-domains/domainfr/mailboxes/', '**/api/v1.0/mail-domains/domainfr/mailboxes/',
(route) => { (route) => {
if (route.request().method() === 'POST') { if (route.request().method() === 'POST') {
@@ -149,13 +160,10 @@ test.describe('Mail domain create mailbox', () => {
} }
}); });
await interceptApiCalls(); void interceptRequests(page);
await clickOnMailDomainsNavButton(page); await navigateToMailboxCreationFormForMailDomainFr(page);
await page.getByRole('listbox').first().getByText('domain.fr').click();
await page.getByRole('button', { name: 'Create a mailbox' }).click();
await page.getByRole('button', { name: 'Cancel' }).click(); await page.getByRole('button', { name: 'Cancel' }).click();
await expect(page.getByTitle('Mailbox creation form')).toBeHidden(); await expect(page.getByTitle('Mailbox creation form')).toBeHidden();
@@ -167,13 +175,37 @@ test.describe('Mail domain create mailbox', () => {
page.getByRole('heading', { name: 'Create a mailbox' }), page.getByRole('heading', { name: 'Create a mailbox' }),
).toBeVisible(); ).toBeVisible();
await page.getByLabel('First name').fill('John'); const inputFirstName = page.getByLabel('First name');
await page.getByLabel('Last name').fill('Doe'); const inputLastName = page.getByLabel('Last name');
await page.getByLabel('Main email address').fill('john.doe'); const inputLocalPart = page.getByLabel('Email address prefix');
await expect(page.locator('span').getByText('@domain.fr')).toBeVisible(); const instructionInputLocalPart = page.getByText(
await page.getByLabel('Secondary email address').fill('john.doe@mail.com'); 'It must not contain spaces, accents or special characters (except "." or "-"). E.g.: jean.dupont',
);
const inputSecondaryEmailAddress = page.getByLabel(
'Secondary email address',
);
await page.getByRole('button', { name: 'Submit' }).click(); await expect(inputFirstName).toHaveAttribute('aria-required', 'true');
await expect(inputFirstName).toHaveAttribute('required', '');
await expect(inputLastName).toHaveAttribute('aria-required', 'true');
await expect(inputLastName).toHaveAttribute('required', '');
await expect(inputLocalPart).toHaveAttribute('aria-required', 'true');
await expect(inputLocalPart).toHaveAttribute('required', '');
await expect(inputSecondaryEmailAddress).toHaveAttribute(
'aria-required',
'true',
);
await expect(inputSecondaryEmailAddress).toHaveAttribute('required', '');
await inputFirstName.fill('John');
await inputLastName.fill('Doe');
await inputLocalPart.fill('john.doe');
await expect(instructionInputLocalPart).toBeVisible();
await expect(page.locator('span').getByText('@domain.fr')).toBeVisible();
await inputSecondaryEmailAddress.fill('john.doe@mail.com');
await page.getByRole('button', { name: 'Create the mailbox' }).click();
expect(isCreateMailboxRequestSentWithExpectedPayload).toBeTruthy(); expect(isCreateMailboxRequestSentWithExpectedPayload).toBeTruthy();
await expect(page.getByAltText('Mailbox creation form')).toBeHidden(); await expect(page.getByAltText('Mailbox creation form')).toBeHidden();
@@ -192,51 +224,9 @@ test.describe('Mail domain create mailbox', () => {
); );
}); });
test('checks client invalidation messages are displayed when fields are not properly filled', async ({ test('checks client invalidation messages are displayed and no mailbox creation request is sent when fields are not properly filled', async ({
page, page,
}) => { }) => {
const mailboxesFixtures = {
domainFr: {
page1: Array.from({ length: 1 }, (_, i) => ({
id: `456ac6ca-0402-4615-8005-69bc1efde${i}f`,
local_part: `local_part-${i}`,
secondary_email: `secondary_email-${i}`,
})),
},
};
const interceptApiCalls = async () => {
await page.route('**/api/v1.0/mail-domains/?page=*', async (route) => {
await route.fulfill({
json: {
count: mailDomainsFixtures.length,
next: null,
previous: null,
results: mailDomainsFixtures,
},
});
});
await page.route('**/api/v1.0/mail-domains/domainfr', async (route) => {
await route.fulfill({
json: mailDomainDomainFrFixture,
});
});
await page.route(
'**/api/v1.0/mail-domains/domainfr/mailboxes/?page=1**',
async (route) => {
await route.fulfill({
json: {
count: mailboxesFixtures.domainFr.page1.length,
next: null,
previous: null,
results: mailboxesFixtures.domainFr.page1,
},
});
},
{ times: 1 },
);
};
let isCreateMailboxRequestSent = false; let isCreateMailboxRequestSent = false;
page.on( page.on(
'request', 'request',
@@ -246,27 +236,144 @@ test.describe('Mail domain create mailbox', () => {
request.method() === 'POST'), request.method() === 'POST'),
); );
await interceptApiCalls(); void interceptCommonApiRequests(page);
await clickOnMailDomainsNavButton(page); await navigateToMailboxCreationFormForMailDomainFr(page);
await page.getByRole('listbox').first().getByText('domain.fr').click(); const inputFirstName = page.getByLabel('First name');
const inputLastName = page.getByLabel('Last name');
await page.getByRole('button', { name: 'Create a mailbox' }).click(); const inputLocalPart = page.getByLabel('Email address prefix');
const inputSecondaryEmailAddress = page.getByLabel(
await page.getByRole('button', { name: 'Submit' }).click(); '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 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 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( await expect(
page.getByText( page.getByText('You must have minimum 1 character'),
'Please enter the first part of the email address, without including "@" in it',
),
).toBeVisible();
await expect(
page.getByText('Please enter your secondary email address'),
).toBeVisible(); ).toBeVisible();
await page.getByRole('button', { name: 'Create the mailbox' }).click();
expect(isCreateMailboxRequestSent).toBeFalsy(); 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();
});
}); });