Skip to content

Commit

Permalink
feat: allow to change username (#180)
Browse files Browse the repository at this point in the history
fix: test allow to change username

fix: Cypress test related to username validation
  • Loading branch information
mariembencheikh authored Apr 16, 2024
1 parent 64011c2 commit 16012f4
Show file tree
Hide file tree
Showing 18 changed files with 310 additions and 48 deletions.
68 changes: 65 additions & 3 deletions cypress/e2e/profile.cy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,68 @@
describe('template spec', () => {
it('passes', () => {
cy.setUpApi();
import {
USERNAME_CANCEL_BUTTON_ID,
USERNAME_DISPLAY_ID,
USERNAME_EDIT_BUTTON_ID,
USERNAME_SAVE_BUTTON_ID,
} from '@/config/selectors';

import { BOB, MEMBERS } from '../fixtures/members';

const changeUsername = (newUserName: string) => {
cy.get(`#${USERNAME_EDIT_BUTTON_ID}`).click();
cy.get('input[name=username]').clear();
// Find the input field and type the new username
cy.get('input[name=username]').type(newUserName);
};

describe('Change username', () => {
beforeEach(() => {
cy.setUpApi({ currentMember: BOB });
cy.visit('/');
});

it('Username field connot be empty', () => {
changeUsername('validUsername');
cy.get('input[name=username]').clear();
cy.get(`#${USERNAME_SAVE_BUTTON_ID}`).should('be.disabled');
});

it('Username too long', () => {
const longUsername = MEMBERS.WRONG_NAME_TOO_LONG.name;
changeUsername(longUsername);

cy.get(`#${USERNAME_SAVE_BUTTON_ID}`).should('be.disabled');
});

it('Username too short', () => {
const shortUsername = MEMBERS.WRONG_NAME_TOO_SHORT.name;
changeUsername(shortUsername);
cy.get(`#${USERNAME_SAVE_BUTTON_ID}`).should('be.disabled');
});

it('Valid username can be saved', () => {
const validUsername = 'validUsername';
changeUsername(validUsername);
cy.get(`#${USERNAME_SAVE_BUTTON_ID}`).should('not.be.disabled');

cy.get(`#${USERNAME_SAVE_BUTTON_ID}`).click();

cy.wait('@editMember').then(({ request: { body } }) => {
expect(body.name).to.equal(validUsername);
});
});

it('Should not update the user name if canceling edit', () => {
changeUsername('validUsername');
cy.get(`#${USERNAME_CANCEL_BUTTON_ID}`).click();
cy.get(`#${USERNAME_DISPLAY_ID}`).contains(BOB.name);
});

it('Saves username after trimming trailing space', () => {
const usernameWithTrailingSpace = 'test '; // Nom d'utilisateur avec espace à la fin
changeUsername(usernameWithTrailingSpace);
cy.get(`#${USERNAME_SAVE_BUTTON_ID}`).click();
cy.wait('@editMember').then(({ request }) => {
expect(request.body.name).to.equal(usernameWithTrailingSpace.trim());
});
});
});
28 changes: 25 additions & 3 deletions cypress/fixtures/members.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
import { MemberFactory } from '@graasp/sdk';
import { Member, MemberFactory } from '@graasp/sdk';

// eslint-disable-next-line import/prefer-default-export
export const CURRENT_MEMBER = MemberFactory();

export const MEMBERS = [];
export const BOB = MemberFactory({
id: 'e1a0a49d-dfc4-466e-8379-f3846cda91e2',
name: 'BOB',
email: '[email protected]',
});
export const MEMBERS: {
[name: string]: Member & {
nameValid?: boolean;
};
} = {
WRONG_NAME_TOO_SHORT: {
id: '201621f0-848b-413f-80f9-25937a56c008',
name: 'w',
email: '[email protected]',
nameValid: false,
},
WRONG_NAME_TOO_LONG: {
id: 'a7e428e9-86d3-434a-b611-d930cf8380ec',
name: 'qwertyuiopasdfghjklzxcvbnmqwert',
email: '[email protected]',
nameValid: false,
},
VALID_NAME: BOB,
};
2 changes: 1 addition & 1 deletion cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Cypress.Commands.add(

mockSignOut();

mockEditMember(members, editMemberError);
mockEditMember(cachedMembers, editMemberError);

mockGetAvatarUrl(members, getAvatarUrlError);

Expand Down
4 changes: 2 additions & 2 deletions cypress/support/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,12 @@ export const mockEditMember = (
method: HttpMethod.Patch,
url: new RegExp(`${API_HOST}/${buildPatchMember(ID_FORMAT)}`),
},
({ reply }) => {
({ reply, body }) => {
if (shouldThrowError) {
return reply({ statusCode: StatusCodes.BAD_REQUEST });
}

return reply('edit member');
return reply(body);
},
).as('editMember');
};
Expand Down
13 changes: 13 additions & 0 deletions cypress/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom", "ES2021.String"],
"types": ["cypress", "node", "vite/client"],
"strictNullChecks": false,
"strict": true,
"sourceMap": false
},
"include": ["**/*.ts", "cypress.d.ts"],
"exclude": ["coverage", ".nyc_output"]
}
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"version": "2.4.0",
"private": true,
"contributors": [
"Kim Lan Phan Hoang"
"Kim Lan Phan Hoang",
"Mariem Bencheikh"
],
"engines": {
"node": ">=20"
Expand All @@ -13,8 +14,8 @@
"@emotion/cache": "11.11.0",
"@emotion/react": "11.11.4",
"@emotion/styled": "11.11.0",
"@graasp/query-client": "3.0.0",
"@graasp/sdk": "4.2.1",
"@graasp/query-client": "3.2.1",
"@graasp/sdk": "4.5.0",
"@graasp/translations": "1.25.3",
"@graasp/ui": "4.15.0",
"@mui/icons-material": "5.15.14",
Expand Down
99 changes: 99 additions & 0 deletions src/components/main/EditingNameField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { ChangeEvent, useState } from 'react';

import CloseIcon from '@mui/icons-material/Close';
import DoneIcon from '@mui/icons-material/Done';
import { IconButton, Stack, TextField } from '@mui/material';

import { useAccountTranslation } from '@/config/i18n';
import {
USERNAME_CANCEL_BUTTON_ID,
USERNAME_INPUT_FIELD_ID,
USERNAME_SAVE_BUTTON_ID,
} from '@/config/selectors';

const MIN_USERNAME_LENGTH = 3;
const MAX_USERNAME_LENGTH = 30;

type EditingUserNameFieldProps = {
name: string;
onSave: (newValue: string) => void;
onCancel: () => void;
};

const verifyUsername = (username: string) => {
const trimmedUsername = username.trim();
if (trimmedUsername === '') {
return 'USERNAME_EMPTY_ERROR';
}

if (
trimmedUsername.length < MIN_USERNAME_LENGTH ||
trimmedUsername.length > MAX_USERNAME_LENGTH
) {
return 'USERNAME_LENGTH_ERROR';
}
return null;
};

const EditingUserNameField = ({
name,
onSave,
onCancel,
}: EditingUserNameFieldProps): JSX.Element => {
const [newUserName, setNewUserName] = useState(name);
const { t } = useAccountTranslation();
const [error, setError] = useState<string | null>();

const handleChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
const { value } = target;
setNewUserName(value);
const errorMessage = verifyUsername(value);
setError(
errorMessage
? t(errorMessage, {
min: MIN_USERNAME_LENGTH,
max: MAX_USERNAME_LENGTH,
})
: null,
);
};
const handleSave = () => {
const errorMessage = verifyUsername(newUserName);

if (!errorMessage) {
onSave(newUserName.trim());
}
};

return (
<Stack direction="row" alignItems="center" spacing={1}>
<TextField
id={USERNAME_INPUT_FIELD_ID}
variant="standard"
type="text"
name="username"
value={newUserName}
error={Boolean(error)}
helperText={error}
onChange={handleChange}
autoFocus
/>
<IconButton
type="reset"
onClick={onCancel}
id={USERNAME_CANCEL_BUTTON_ID}
>
<CloseIcon />
</IconButton>
<IconButton
id={USERNAME_SAVE_BUTTON_ID}
onClick={handleSave}
disabled={Boolean(error)}
>
<DoneIcon />
</IconButton>
</Stack>
);
};

export default EditingUserNameField;
58 changes: 58 additions & 0 deletions src/components/main/UsernameForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useState } from 'react';

import { ModeEdit } from '@mui/icons-material';
import { Box, IconButton, Stack } from '@mui/material';

import {
USERNAME_DISPLAY_ID,
USERNAME_EDIT_BUTTON_ID,
} from '@/config/selectors';

import { mutations } from '../../config/queryClient';
import EditingUserNameField from './EditingNameField';

type UsernameProps = {
member: {
id: string;
name: string;
};
};
const UsernameForm = ({ member }: UsernameProps): JSX.Element => {
const { mutate: editMember } = mutations.useEditMember();
const [isEditing, setIsEditing] = useState(false);

const handleSave = (newUserName: string) => {
editMember({
id: member.id,
name: newUserName,
});

setIsEditing(false);
};

const cancelEditing = () => {
setIsEditing(false);
};

if (isEditing) {
return (
<EditingUserNameField
name={member.name}
onSave={handleSave}
onCancel={cancelEditing}
/>
);
}
return (
<Stack direction="row" alignItems="center" spacing={1}>
<Box id={USERNAME_DISPLAY_ID}>{member.name}</Box>
<IconButton
onClick={() => setIsEditing(true)}
id={USERNAME_EDIT_BUTTON_ID}
>
<ModeEdit />
</IconButton>
</Stack>
);
};
export default UsernameForm;
6 changes: 6 additions & 0 deletions src/config/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ export const DELETE_MEMBER_DIALOG_DESCRIPTION_ID = 'alert-dialog-description';
export const APP_NAVIGATION_PLATFORM_SWITCH_ID = 'appNavigationPlatformSwitch';

export const STORAGE_PROGRESS_BAR_ID = 'storageProgressBar';

export const USERNAME_EDIT_BUTTON_ID = 'username-edit-button';
export const USERNAME_SAVE_BUTTON_ID = 'username-save-button';
export const USERNAME_CANCEL_BUTTON_ID = 'username-cancel-button';
export const USERNAME_INPUT_FIELD_ID = 'username-input-field';
export const USERNAME_DISPLAY_ID = 'username-display';
1 change: 1 addition & 0 deletions src/langs/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"MAIN_MENU_STORAGE": "تخزين",
"SAVE_ACTIONS_TOGGLE_TOOLTIP": "قريباً!",
"PROFILE_TITLE": "الملف الشخصي",
"PROFILE_MEMBER_NAME": "اسم المستخدم",
"PROFILE_MEMBER_ID_TITLE": "هوية المستخدم",
"PROFILE_LANGUAGE_TITLE": "لغة",
"PROFILE_CREATED_AT_TITLE": "عضوّ منذ",
Expand Down
1 change: 1 addition & 0 deletions src/langs/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"MAIN_MENU_STORAGE": "Speicher",
"SAVE_ACTIONS_TOGGLE_TOOLTIP": "Demnächst verfügbar!",
"PROFILE_TITLE": "Profil",
"PROFILE_MEMBER_NAME": "Nutzername",
"PROFILE_MEMBER_ID_TITLE": "Benützer ID",
"PROFILE_LANGUAGE_TITLE": "Sprache",
"PROFILE_CREATED_AT_TITLE": "Mitglied seit",
Expand Down
5 changes: 4 additions & 1 deletion src/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"MAIN_MENU_STORAGE": "Storage",
"SAVE_ACTIONS_TOGGLE_TOOLTIP": "Coming soon!",
"PROFILE_TITLE": "Profile",
"PROFILE_MEMBER_NAME": "Username",
"PROFILE_MEMBER_ID_TITLE": "Member ID",
"PROFILE_LANGUAGE_TITLE": "Language",
"PROFILE_CREATED_AT_TITLE": "Member Since",
Expand Down Expand Up @@ -62,5 +63,7 @@
"GO_TO_MANAGE_ACCOUNT_PAGE": "Continue",
"PROFILE_DELETE_TYPE_CONFIRMATION_TEXT": "To confirm the deletion, type \"{{text}}\" below:",
"DELETE_CONFIRMATION_VALUE": "delete",
"DESTRUCTIVE_SETTINGS_DETAILS": "Please be aware that the actions outlined here have potentially irreversible consequences. Ensure you thoroughly review any restrictions before proceeding, and only take action once you are certain."
"DESTRUCTIVE_SETTINGS_DETAILS": "Please be aware that the actions outlined here have potentially irreversible consequences. Ensure you thoroughly review any restrictions before proceeding, and only take action once you are certain.",
"USERNAME_EMPTY_ERROR": "The field cannot be empty",
"USERNAME_LENGTH_ERROR": "Username must be between {{min}} and {{max}} characters long"
}
1 change: 1 addition & 0 deletions src/langs/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"MAIN_MENU_STORAGE": "Almacenamiento",
"SAVE_ACTIONS_TOGGLE_TOOLTIP": "¡Muy pronto!",
"PROFILE_TITLE": "Perfil",
"PROFILE_MEMBER_NAME": "Nombre de usuario",
"PROFILE_MEMBER_ID_TITLE": "Identificación de miembro",
"PROFILE_LANGUAGE_TITLE": "Idioma",
"PROFILE_CREATED_AT_TITLE": "Miembro desde",
Expand Down
5 changes: 4 additions & 1 deletion src/langs/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"MAIN_MENU_STORAGE": "Stockage",
"SAVE_ACTIONS_TOGGLE_TOOLTIP": "Prochainement !",
"PROFILE_TITLE": "Profil",
"PROFILE_MEMBER_NAME": "Nom de l'utilisateur",
"PROFILE_MEMBER_ID_TITLE": "Identifiant de l'utilisateur",
"PROFILE_LANGUAGE_TITLE": "Langue",
"PROFILE_CREATED_AT_TITLE": "Membre depuis",
Expand Down Expand Up @@ -62,5 +63,7 @@
"GO_TO_MANAGE_ACCOUNT_PAGE": "Poursuivre",
"PROFILE_DELETE_TYPE_CONFIRMATION_TEXT": "Pour confirmer la suppression, tapez \"{{text}}\" ci-dessous :",
"DELETE_CONFIRMATION_VALUE": "supprimer",
"DESTRUCTIVE_SETTINGS_DETAILS": "Veuillez noter que les opérations décrites ici ont des conséquences potentiellement irréversibles. Assurez-vous d’examiner attentivement toutes les restrictions avant de continuer avant de continuer."
"DESTRUCTIVE_SETTINGS_DETAILS": "Veuillez noter que les opérations décrites ici ont des conséquences potentiellement irréversibles. Assurez-vous d’examiner attentivement toutes les restrictions avant de continuer avant de continuer.",
"USERNAME_EMPTY_ERROR": "Le champ ne peut pas être vide",
"USERNAME_LENGTH_ERROR": "Le nom d'utilisateur doit contenir entre {{min}} et {{max}} caractères"
}
1 change: 1 addition & 0 deletions src/langs/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"MAIN_MENU_STORAGE": "Archiviazione",
"SAVE_ACTIONS_TOGGLE_TOOLTIP": "In arrivo prossimamente!",
"PROFILE_TITLE": "Profilo",
"PROFILE_MEMBER_NAME": "Nome utente",
"PROFILE_MEMBER_ID_TITLE": "ID Membro",
"PROFILE_LANGUAGE_TITLE": "Lingua",
"PROFILE_CREATED_AT_TITLE": "Membro da",
Expand Down
Loading

0 comments on commit 16012f4

Please sign in to comment.