diff --git a/CHANGELOG.md b/CHANGELOG.md
index d1bf349..056a7c7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,8 +13,9 @@ and this project adheres to
- ✨(domains) add endpoint to list and retrieve domain accesses #404
- 🍱(dev) embark dimail-api as container #366
-- ✨(dimail) allow la regie to request a token for another user #416
-- ⚗️(frontend) show username on AccountDropDown #412
+- ✨(dimail) allow la regie to request a token for another user #416
+- ✨(frontend) show username on AccountDropDown #412
+- 🥅(frontend) improve add & update group forms error handling #387
### Changed
diff --git a/src/frontend/apps/desk/src/api/__tests__/parseAPIError.test.ts b/src/frontend/apps/desk/src/api/__tests__/parseAPIError.test.ts
new file mode 100644
index 0000000..b2b3d78
--- /dev/null
+++ b/src/frontend/apps/desk/src/api/__tests__/parseAPIError.test.ts
@@ -0,0 +1,157 @@
+import { APIError } from '@/api';
+
+import {
+ parseAPIError,
+ parseAPIErrorCause,
+ parseServerAPIError,
+} from '../parseAPIError';
+
+describe('parseAPIError', () => {
+ const handleErrorMock = jest.fn();
+ const handleServerErrorMock = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should handle specific API error and return no unhandled causes', () => {
+ const error = new APIError('client error', {
+ cause: ['Mail domain with this name already exists.'],
+ status: 400,
+ });
+
+ const result = parseAPIError({
+ error,
+ errorParams: [
+ [
+ ['Mail domain with this name already exists.'],
+ 'This domain already exists.',
+ handleErrorMock,
+ ],
+ ],
+ serverErrorParams: ['Server error', handleServerErrorMock],
+ });
+
+ expect(handleErrorMock).toHaveBeenCalled();
+ expect(result).toEqual(['This domain already exists.']);
+ });
+
+ it('should return unhandled causes when no match is found', () => {
+ const error = new APIError('client error', {
+ cause: ['Unhandled error'],
+ status: 400,
+ });
+
+ const result = parseAPIError({
+ error,
+ errorParams: [
+ [
+ ['Mail domain with this name already exists.'],
+ 'This domain already exists.',
+ handleErrorMock,
+ ],
+ ],
+ serverErrorParams: ['Server error', handleServerErrorMock],
+ });
+
+ expect(handleErrorMock).not.toHaveBeenCalled();
+ expect(result).toEqual(['Unhandled error']);
+ });
+
+ it('should handle server errors correctly and prepend server error message', () => {
+ const error = new APIError('server error', { status: 500 });
+
+ const result = parseAPIError({
+ error,
+ errorParams: undefined,
+ serverErrorParams: ['Server error occurred', handleServerErrorMock],
+ });
+
+ expect(handleServerErrorMock).toHaveBeenCalled();
+ expect(result).toEqual(['Server error occurred']);
+ });
+
+ it('should handle absence of errors gracefully', () => {
+ const result = parseAPIError({
+ error: null,
+ errorParams: [
+ [
+ ['Mail domain with this name already exists.'],
+ 'This domain already exists.',
+ handleErrorMock,
+ ],
+ ],
+ serverErrorParams: ['Server error', handleServerErrorMock],
+ });
+
+ expect(result).toBeUndefined();
+ });
+});
+
+describe('parseAPIErrorCause', () => {
+ it('should handle specific errors and call handleError', () => {
+ const handleErrorMock = jest.fn();
+ const causes = ['Mail domain with this name already exists.'];
+
+ const errorParams: [string[], string, () => void][] = [
+ [
+ ['Mail domain with this name already exists.'],
+ 'This domain already exists.',
+ handleErrorMock,
+ ],
+ ];
+
+ const result = parseAPIErrorCause(
+ new APIError('client error', { cause: causes, status: 400 }),
+ errorParams,
+ );
+
+ expect(handleErrorMock).toHaveBeenCalled();
+ expect(result).toEqual(['This domain already exists.']);
+ });
+
+ it('should handle multiple causes and return unhandled causes', () => {
+ const handleErrorMock = jest.fn();
+ const causes = [
+ 'Mail domain with this name already exists.',
+ 'Unhandled error',
+ ];
+
+ const errorParams: [string[], string, () => void][] = [
+ [
+ ['Mail domain with this name already exists.'],
+ 'This domain already exists.',
+ handleErrorMock,
+ ],
+ ];
+
+ const result = parseAPIErrorCause(
+ new APIError('client error', { cause: causes, status: 400 }),
+ errorParams,
+ );
+
+ expect(handleErrorMock).toHaveBeenCalled();
+ expect(result).toEqual(['This domain already exists.', 'Unhandled error']);
+ });
+});
+
+describe('parseServerAPIError', () => {
+ const handleServerErrorMock = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return the server error message and handle callback', () => {
+ const result = parseServerAPIError(['Server error', handleServerErrorMock]);
+
+ expect(result).toEqual('Server error');
+ expect(handleServerErrorMock).toHaveBeenCalled();
+ });
+
+ it('should return only the server error message when no callback is provided', () => {
+ const result = parseServerAPIError(['Server error', undefined]);
+
+ expect(result).toEqual('Server error');
+ });
+});
diff --git a/src/frontend/apps/desk/src/api/__tests__/parseAPIError.test.tsx b/src/frontend/apps/desk/src/api/__tests__/parseAPIError.test.tsx
deleted file mode 100644
index 83bd991..0000000
--- a/src/frontend/apps/desk/src/api/__tests__/parseAPIError.test.tsx
+++ /dev/null
@@ -1,174 +0,0 @@
-import { APIError } from '@/api';
-
-import {
- parseAPIError,
- parseAPIErrorCause,
- parseServerAPIError,
-} from '../parseAPIError';
-
-describe('parseAPIError', () => {
- const handleErrorMock = jest.fn();
- const handleServerErrorMock = jest.fn();
-
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('should handle specific API error and return no unhandled causes', () => {
- const error = new APIError('client error', {
- cause: ['Mail domain with this name already exists.'],
- status: 400,
- });
-
- const result = parseAPIError({
- error,
- errorParams: {
- name: {
- causes: ['Mail domain with this name already exists.'],
- handleError: handleErrorMock,
- },
- },
- serverErrorParams: {
- defaultMessage: 'Server error',
- handleError: handleServerErrorMock,
- },
- });
-
- expect(handleErrorMock).toHaveBeenCalled();
- expect(result).toEqual([]);
- });
-
- it('should return unhandled causes when no match is found', () => {
- const error = new APIError('client error', {
- cause: ['Unhandled error'],
- status: 400,
- });
-
- const result = parseAPIError({
- error,
- errorParams: {
- name: {
- causes: ['Mail domain with this name already exists.'],
- handleError: handleErrorMock,
- },
- },
- serverErrorParams: {
- defaultMessage: 'Server error',
- handleError: handleServerErrorMock,
- },
- });
-
- expect(handleErrorMock).not.toHaveBeenCalled();
- expect(result).toEqual(['Unhandled error']);
- });
-
- it('should handle server errors correctly and prepend server error message', () => {
- const error = new APIError('server error', { status: 500 });
-
- const result = parseAPIError({
- error,
- errorParams: undefined,
- serverErrorParams: {
- defaultMessage: 'Server error occurred',
- handleError: handleServerErrorMock,
- },
- });
-
- expect(handleServerErrorMock).toHaveBeenCalled();
- expect(result).toEqual(['Server error occurred']);
- });
-
- it('should handle absence of errors gracefully', () => {
- const result = parseAPIError({
- error: null,
- errorParams: {
- name: {
- causes: ['Mail domain with this name already exists.'],
- handleError: handleErrorMock,
- },
- },
- serverErrorParams: {
- defaultMessage: 'Server error',
- handleError: handleServerErrorMock,
- },
- });
-
- expect(result).toBeUndefined();
- });
-});
-
-describe('parseAPIErrorCause', () => {
- it('should handle specific errors and call handleError', () => {
- const handleErrorMock = jest.fn();
- const causes = ['Mail domain with this name already exists.'];
-
- const errorParams = {
- name: {
- causes: ['Mail domain with this name already exists.'],
- handleError: handleErrorMock,
- },
- };
-
- const result = parseAPIErrorCause({ causes, errorParams });
-
- expect(handleErrorMock).toHaveBeenCalled();
- expect(result).toEqual([]);
- });
-
- it('should handle multiple causes and return unhandled causes', () => {
- const handleErrorMock = jest.fn();
- const causes = [
- 'Mail domain with this name already exists.',
- 'Unhandled error',
- ];
-
- const errorParams = {
- name: {
- causes: ['Mail domain with this name already exists.'],
- handleError: handleErrorMock,
- },
- };
-
- const result = parseAPIErrorCause({ causes, errorParams });
-
- expect(handleErrorMock).toHaveBeenCalled();
- expect(result).toEqual(['Unhandled error']);
- });
-});
-
-describe('parseServerAPIError', () => {
- it('should prepend the server error message when there are other causes', () => {
- const causes = ['Some other error'];
- const serverErrorParams = {
- defaultMessage: 'Server error',
- };
-
- const result = parseServerAPIError({ causes, serverErrorParams });
-
- expect(result).toEqual(['Server error', 'Some other error']);
- });
-
- it('should only return server error message when no other causes exist', () => {
- const causes: string[] = [];
- const serverErrorParams = {
- defaultMessage: 'Server error',
- };
-
- const result = parseServerAPIError({ causes, serverErrorParams });
-
- expect(result).toEqual(['Server error']);
- });
-
- it('should call handleError when provided as a param', () => {
- const handleErrorMock = jest.fn();
- const causes: string[] = [];
- const serverErrorParams = {
- defaultMessage: 'Server error',
- handleError: handleErrorMock,
- };
-
- parseServerAPIError({ causes, serverErrorParams });
-
- expect(handleErrorMock).toHaveBeenCalled();
- });
-});
diff --git a/src/frontend/apps/desk/src/api/parseAPIError.ts b/src/frontend/apps/desk/src/api/parseAPIError.ts
index 19b0b66..39a4320 100644
--- a/src/frontend/apps/desk/src/api/parseAPIError.ts
+++ b/src/frontend/apps/desk/src/api/parseAPIError.ts
@@ -1,85 +1,106 @@
import { APIError } from '@/api/index';
-type ErrorParams = {
- [fieldName: string]: {
- causes: string[];
- causeShown?: string;
- handleError: () => void;
- };
-};
+type ErrorCallback = () => void;
-type ServerErrorParams = {
- defaultMessage: string;
- handleError?: () => void;
-};
+// Type for the error tuple format [causes, message, handleError]
+type ErrorTuple = [string[], string, ErrorCallback | undefined];
-export type parseAPIErrorParams = {
- error: APIError | null;
- errorParams?: ErrorParams;
- serverErrorParams: ServerErrorParams;
-};
+// Server error tuple [defaultMessage, handleError]
+type ServerErrorTuple = [string, ErrorCallback | undefined];
+
+/**
+ * @function parseAPIError
+ * @description function to centralize APIError handling to treat discovered errors
+ * and error type 500 with default behavior using a simplified tuple structure.
+ * @param error - APIError object
+ * @param errorParams - Array of tuples: each contains an array of causes, a message, and an optional callback function.
+ * @param serverErrorParams - A tuple for server error handling: [defaultMessage, handleError]
+ * @returns Array of error messages or undefined
+ */
export const parseAPIError = ({
error,
errorParams,
serverErrorParams,
-}: parseAPIErrorParams) => {
- if (!error || !serverErrorParams?.defaultMessage) {
+}: {
+ error: APIError | null;
+ errorParams?: ErrorTuple[];
+ serverErrorParams?: ServerErrorTuple;
+}): string[] | undefined => {
+ if (!error) {
return;
}
- let causes: string[] =
+ // Parse known error causes using the tuple structure
+ const errorCauses =
error.cause?.length && errorParams
- ? parseAPIErrorCause({ causes: error.cause, errorParams })
- : [];
+ ? parseAPIErrorCause(error, errorParams)
+ : undefined;
- if (error?.status === 500 || !error?.status) {
- causes = parseServerAPIError({ causes, serverErrorParams });
+ // Check if it's a server error (500) and handle that case
+ const serverErrorCause =
+ (error?.status === 500 || !error?.status) && serverErrorParams
+ ? parseServerAPIError(serverErrorParams)
+ : undefined;
+
+ // Combine the causes and return
+ const causes: string[] = errorCauses ? [...errorCauses] : [];
+ if (serverErrorCause) {
+ causes.unshift(serverErrorCause);
}
- return causes;
+ return causes.length ? causes : undefined;
};
-export const parseAPIErrorCause = ({
- causes,
- errorParams,
-}: {
- causes: string[];
- errorParams: ErrorParams;
-}): string[] =>
- causes.reduce((arrayCauses, cause) => {
- const foundErrorParams = Object.values(errorParams).find((params) =>
- params.causes.find((knownCause) =>
- new RegExp(knownCause, 'i').test(cause),
- ),
+/**
+ * @function parseAPIErrorCause
+ * @description Processes known API error causes using the tuple structure.
+ * @param error - APIError object
+ * @param errorParams - Array of tuples: each contains an array of causes, a message, and an optional callback function.
+ * @returns Array of error messages
+ */
+export const parseAPIErrorCause = (
+ error: APIError,
+ errorParams: ErrorTuple[],
+): string[] | undefined => {
+ if (!error.cause) {
+ return;
+ }
+
+ return error.cause.reduce((causes: string[], cause: string) => {
+ // Find the matching error tuple
+ const matchedError = errorParams.find(([errorCauses]) =>
+ errorCauses.some((knownCause) => new RegExp(knownCause, 'i').test(cause)),
);
- if (!foundErrorParams) {
- arrayCauses.push(cause);
+ if (matchedError) {
+ const [, message, handleError] = matchedError;
+ causes.push(message);
+
+ if (handleError) {
+ handleError();
+ }
+ } else {
+ // If no match is found, add the original cause
+ causes.push(cause);
}
- if (foundErrorParams?.causeShown) {
- arrayCauses.push(foundErrorParams.causeShown);
- }
+ return causes;
+ }, []);
+};
- if (typeof foundErrorParams?.handleError === 'function') {
- foundErrorParams.handleError();
- }
-
- return arrayCauses;
- }, [] as string[]);
-
-export const parseServerAPIError = ({
- causes,
- serverErrorParams,
-}: {
- causes: string[];
- serverErrorParams: ServerErrorParams;
-}): string[] => {
- causes.unshift(serverErrorParams.defaultMessage);
-
- if (typeof serverErrorParams?.handleError === 'function') {
- serverErrorParams.handleError();
+/**
+ * @function parseServerAPIError
+ * @description Handles server errors (500) and adds the default message.
+ * @param serverErrorParams - Tuple [defaultMessage, handleError]
+ * @returns Server error message
+ */
+export const parseServerAPIError = ([
+ defaultMessage,
+ handleError,
+]: ServerErrorTuple): string => {
+ if (handleError) {
+ handleError();
}
- return causes;
+ return defaultMessage;
};
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 e0f2070..f896791 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
@@ -42,13 +42,14 @@ export const ModalAddMailDomain = () => {
onError: (error) => {
const unhandledCauses = parseAPIError({
error,
- errorParams: {
- name: {
- causes: [
+ errorParams: [
+ [
+ [
'Mail domain with this name already exists.',
'Mail domain with this Slug already exists.',
],
- handleError: () => {
+ '',
+ () => {
if (methods.formState.errors.name) {
return;
}
@@ -61,17 +62,17 @@ export const ModalAddMailDomain = () => {
});
methods.setFocus('name');
},
- },
- },
- serverErrorParams: {
- handleError: () => {
- methods.setFocus('name');
- },
- defaultMessage: t(
+ ],
+ ],
+ serverErrorParams: [
+ 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',
),
- },
+ () => {
+ methods.setFocus('name');
+ },
+ ],
});
setErrorCauses((prevState) =>
diff --git a/src/frontend/apps/desk/src/features/mail-domains/components/ModalCreateMailbox.tsx b/src/frontend/apps/desk/src/features/mail-domains/components/ModalCreateMailbox.tsx
index 96b750f..deafe7e 100644
--- a/src/frontend/apps/desk/src/features/mail-domains/components/ModalCreateMailbox.tsx
+++ b/src/frontend/apps/desk/src/features/mail-domains/components/ModalCreateMailbox.tsx
@@ -90,48 +90,44 @@ export const ModalCreateMailbox = ({
closeModal();
},
onError: (error) => {
- const unhandledCauses = parseAPIError({
+ const causes = parseAPIError({
error,
- errorParams: {
- local_part: {
- causes: ['Mailbox with this Local_part and Domain already exists.'],
- handleError: () => {
+ errorParams: [
+ [
+ ['Mailbox with this Local_part and Domain already exists.'],
+ '',
+ () => {
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`,
+ 'Secret not valid for this domain',
],
- causeShown: t(
+ 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(
+ () => methods.setFocus('first_name'),
+ ],
+ ],
+ serverErrorParams: [
+ 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',
),
- },
+ () => methods.setFocus('first_name'),
+ ],
});
setErrorCauses((prevState) =>
- unhandledCauses &&
- JSON.stringify(unhandledCauses) !== JSON.stringify(prevState)
- ? unhandledCauses
+ causes && JSON.stringify(causes) !== JSON.stringify(prevState)
+ ? causes
: prevState,
);
},
diff --git a/src/frontend/apps/desk/src/features/teams/team-management/components/CardCreateTeam.tsx b/src/frontend/apps/desk/src/features/teams/team-management/components/CardCreateTeam.tsx
index 46b914c..e8db9b5 100644
--- a/src/frontend/apps/desk/src/features/teams/team-management/components/CardCreateTeam.tsx
+++ b/src/frontend/apps/desk/src/features/teams/team-management/components/CardCreateTeam.tsx
@@ -14,6 +14,7 @@ import { InputTeamName } from './InputTeamName';
export const CardCreateTeam = () => {
const { t } = useTranslation();
const router = useRouter();
+
const {
mutate: createTeam,
isError,
@@ -24,6 +25,7 @@ export const CardCreateTeam = () => {
router.push(`/teams/${team.id}`);
},
});
+
const [teamName, setTeamName] = useState('');
const { colorsTokens } = useCunninghamTheme();
diff --git a/src/frontend/apps/desk/src/features/teams/team-management/components/InputTeamName.tsx b/src/frontend/apps/desk/src/features/teams/team-management/components/InputTeamName.tsx
index 3a58903..ecdca84 100644
--- a/src/frontend/apps/desk/src/features/teams/team-management/components/InputTeamName.tsx
+++ b/src/frontend/apps/desk/src/features/teams/team-management/components/InputTeamName.tsx
@@ -1,7 +1,9 @@
import { Input, Loader } from '@openfun/cunningham-react';
import { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
import { APIError } from '@/api';
+import { parseAPIError } from '@/api/parseAPIError';
import { Box, TextErrors } from '@/components';
interface InputTeamNameProps {
@@ -21,6 +23,7 @@ export const InputTeamName = ({
label,
setTeamName,
}: InputTeamNameProps) => {
+ const { t } = useTranslation();
const [isInputError, setIsInputError] = useState(isError);
useEffect(() => {
@@ -29,6 +32,28 @@ export const InputTeamName = ({
}
}, [isError]);
+ const causes = error
+ ? parseAPIError({
+ error,
+ errorParams: [
+ [
+ ['Team with this Slug already exists.'],
+ t(
+ 'This name is already used for another group. Please enter another one.',
+ ),
+ undefined,
+ ],
+ ],
+ serverErrorParams: [
+ 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',
+ ),
+ undefined,
+ ],
+ })
+ : undefined;
+
return (
<>
- {isError && error && }
+ {isError && causes && }
{isPending && (
diff --git a/src/frontend/apps/desk/src/features/teams/team-management/components/__tests__/CardCreateTeam.test.tsx b/src/frontend/apps/desk/src/features/teams/team-management/components/__tests__/CardCreateTeam.test.tsx
new file mode 100644
index 0000000..d223596
--- /dev/null
+++ b/src/frontend/apps/desk/src/features/teams/team-management/components/__tests__/CardCreateTeam.test.tsx
@@ -0,0 +1,149 @@
+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 { CardCreateTeam } from '../CardCreateTeam';
+
+const mockPush = jest.fn();
+jest.mock('next/navigation', () => ({
+ useRouter: () => ({
+ push: mockPush,
+ }),
+}));
+
+describe('CardCreateTeam', () => {
+ const renderCardCreateTeam = () =>
+ render(, { wrapper: AppWrapper });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ fetchMock.restore();
+ });
+
+ it('renders all the elements', () => {
+ renderCardCreateTeam();
+
+ expect(screen.getByLabelText('Create new team card')).toBeInTheDocument();
+ expect(screen.getByText('Create a new group')).toBeInTheDocument();
+ expect(screen.getByLabelText('Team name')).toBeInTheDocument();
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
+ expect(screen.getByText('Create the team')).toBeInTheDocument();
+ });
+
+ it('handles input for team name and enables submit button', async () => {
+ const user = userEvent.setup();
+ renderCardCreateTeam();
+
+ const teamNameInput = screen.getByLabelText('Team name');
+ const createButton = screen.getByText('Create the team');
+
+ expect(createButton).toBeDisabled();
+
+ await user.type(teamNameInput, 'New Team');
+ expect(createButton).toBeEnabled();
+ });
+
+ it('creates a team successfully and redirects', async () => {
+ fetchMock.post('end:teams/', {
+ id: '270328ea-c2c0-4f74-a449-5cdc976dcdb6',
+ name: 'New Team',
+ });
+
+ const user = userEvent.setup();
+ renderCardCreateTeam();
+
+ const teamNameInput = screen.getByLabelText('Team name');
+ const createButton = screen.getByText('Create the team');
+
+ await user.type(teamNameInput, 'New Team');
+ await user.click(createButton);
+
+ await waitFor(() => {
+ expect(mockPush).toHaveBeenCalledWith(
+ '/teams/270328ea-c2c0-4f74-a449-5cdc976dcdb6',
+ );
+ });
+
+ expect(fetchMock.calls()).toHaveLength(1);
+ expect(fetchMock.lastCall()?.[0]).toContain('/teams/');
+ expect(fetchMock.lastCall()?.[1]?.body).toEqual(
+ JSON.stringify({ name: 'New Team' }),
+ );
+ });
+
+ it('displays an error message when team name already exists', async () => {
+ fetchMock.post('end:teams/', {
+ body: {
+ cause: ['Team with this Slug already exists.'],
+ },
+ status: 400,
+ });
+
+ const user = userEvent.setup();
+ renderCardCreateTeam();
+
+ const teamNameInput = screen.getByLabelText('Team name');
+ const createButton = screen.getByText('Create the team');
+
+ await user.type(teamNameInput, 'Existing Team');
+ await user.click(createButton);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(/This name is already used for another group/i),
+ ).toBeInTheDocument();
+ });
+ });
+
+ it('handles server error gracefully', async () => {
+ fetchMock.post('end:/teams/', {
+ body: {},
+ status: 500,
+ });
+
+ const user = userEvent.setup();
+ renderCardCreateTeam();
+
+ const teamNameInput = screen.getByLabelText('Team name');
+ const createButton = screen.getByText('Create the team');
+
+ await user.type(teamNameInput, 'Server Error Team');
+ await user.click(createButton);
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ /Your request cannot be processed because the server is experiencing an error/i,
+ ),
+ ).toBeInTheDocument();
+ });
+
+ expect(fetchMock.calls()).toHaveLength(1);
+ expect(fetchMock.lastCall()?.[0]).toContain('/teams/');
+ expect(fetchMock.lastCall()?.[1]?.body).toEqual(
+ JSON.stringify({ name: 'Server Error Team' }),
+ );
+ });
+
+ it('disables create button when API request is pending', async () => {
+ // Never resolves
+ fetchMock.post('end:teams/', new Promise(() => {}));
+
+ const user = userEvent.setup();
+ renderCardCreateTeam();
+
+ const teamNameInput = screen.getByLabelText('Team name');
+ const createButton = screen.getByText('Create the team');
+
+ await user.type(teamNameInput, 'Pending Team');
+ await user.click(createButton);
+
+ expect(createButton).toBeDisabled();
+ });
+});
diff --git a/src/frontend/apps/desk/src/i18n/translations.json b/src/frontend/apps/desk/src/i18n/translations.json
index 2e17aad..d892dce 100644
--- a/src/frontend/apps/desk/src/i18n/translations.json
+++ b/src/frontend/apps/desk/src/i18n/translations.json
@@ -147,6 +147,7 @@
"This domain name is deactivated. No new mailboxes can be created.": "Ce nom de domaine est désactivé. Aucune nouvelle boîte mail ne peut être créée.",
"This email prefix is already used.": "Ce préfixe d'email est déjà utilisé.",
"This mail domain is already used. Please, choose another one.": "Ce domaine de messagerie est déjà utilisé. Veuillez en choisir un autre.",
+ "This name is already used for another group. Please enter another one.": "Un autre groupe utilise déjà ce nom. Veuillez en saisir un autre.",
"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 d’accessibilité qui vous empêche d’accéder à un contenu ou à un des services du portail et vous n’avez 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 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.",
diff --git a/src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts
index c800c79..5018d5c 100644
--- a/src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts
@@ -108,7 +108,9 @@ test.describe('Teams Create', () => {
await page.getByRole('button', { name: 'Create the team' }).click();
await expect(
- page.getByText('Team with this Slug already exists.'),
+ page.getByText(
+ 'This name is already used for another group. Please enter another one.',
+ ),
).toBeVisible();
});