From 05ac2bd3b2f0408c1dcb4a649caaedb9903a93da Mon Sep 17 00:00:00 2001 From: rishabhbizzle Date: Sun, 7 Jan 2024 21:45:48 +0530 Subject: [PATCH 1/6] feat: Use of same Login/Register Component on User and Admin --- src/App.tsx | 5 +- src/screens/LoginPage/LoginPage.test.tsx | 36 +- src/screens/LoginPage/LoginPage.tsx | 80 ++- .../UserLoginPage/UserLoginPage.module.css | 208 ------ .../UserLoginPage/UserLoginPage.test.tsx | 630 ------------------ .../UserLoginPage/UserLoginPage.tsx | 625 ----------------- 6 files changed, 73 insertions(+), 1511 deletions(-) delete mode 100644 src/screens/UserPortal/UserLoginPage/UserLoginPage.module.css delete mode 100644 src/screens/UserPortal/UserLoginPage/UserLoginPage.test.tsx delete mode 100644 src/screens/UserPortal/UserLoginPage/UserLoginPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 3daf57b09c..b114cd2cf9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,7 +24,6 @@ import MemberDetail from 'screens/MemberDetail/MemberDetail'; import Loader from 'components/Loader/Loader'; // User Portal Components -import UserLoginPage from 'screens/UserPortal/UserLoginPage/UserLoginPage'; import Organizations from 'screens/UserPortal/Organizations/Organizations'; import Home from 'screens/UserPortal/Home/Home'; import People from 'screens/UserPortal/People/People'; @@ -99,7 +98,7 @@ function app(): JSX.Element { return ( <> - + } /> @@ -118,7 +117,7 @@ function app(): JSX.Element { {/* User Portal Routes */} - + } /> { - + @@ -136,7 +136,7 @@ describe('Talawa-API server fetch check', () => { - + @@ -157,7 +157,7 @@ describe('Testing Login Page Screen', () => { - + @@ -184,7 +184,7 @@ describe('Testing Login Page Screen', () => { - + @@ -229,7 +229,7 @@ describe('Testing Login Page Screen', () => { - + @@ -273,7 +273,7 @@ describe('Testing Login Page Screen', () => { - + @@ -316,7 +316,7 @@ describe('Testing Login Page Screen', () => { - + @@ -359,7 +359,7 @@ describe('Testing Login Page Screen', () => { - + @@ -399,7 +399,7 @@ describe('Testing Login Page Screen', () => { - + @@ -426,7 +426,7 @@ describe('Testing Login Page Screen', () => { - + @@ -452,7 +452,7 @@ describe('Testing Login Page Screen', () => { - + @@ -481,7 +481,7 @@ describe('Testing Login Page Screen', () => { - + @@ -512,7 +512,7 @@ describe('Testing Login Page Screen', () => { - + @@ -543,7 +543,7 @@ describe('Testing Login Page Screen', () => { - + @@ -564,7 +564,7 @@ describe('Testing Login Page Screen', () => { - + @@ -593,7 +593,7 @@ describe('Testing Login Page Screen', () => { - + @@ -622,7 +622,7 @@ describe('Testing Login Page Screen', () => { - + @@ -651,7 +651,7 @@ describe('Testing Login Page Screen', () => { - + diff --git a/src/screens/LoginPage/LoginPage.tsx b/src/screens/LoginPage/LoginPage.tsx index 64bbd7b36a..d23b4f1684 100644 --- a/src/screens/LoginPage/LoginPage.tsx +++ b/src/screens/LoginPage/LoginPage.tsx @@ -35,8 +35,11 @@ import Loader from 'components/Loader/Loader'; import { errorHandler } from 'utils/errorHandler'; import styles from './LoginPage.module.css'; import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined'; +interface InterfaceComponentProps { + role: string; +} -function loginPage(): JSX.Element { +function loginPage({ role }: InterfaceComponentProps): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'loginPage' }); const history = useHistory(); @@ -109,7 +112,9 @@ function loginPage(): JSX.Element { useEffect(() => { const isLoggedIn = localStorage.getItem('IsLoggedIn'); if (isLoggedIn == 'TRUE') { - history.push('/orglist'); + role === 'admin' + ? history.push('/orglist') + : history.push('/user/organizations/'); } setComponentLoader(false); }, []); @@ -161,6 +166,17 @@ function loginPage(): JSX.Element { } }; + const validatePassword = (password: string): boolean => { + const lengthCheck = new RegExp('^.{6,}$'); + return ( + lengthCheck.test(password) && + passwordValidationRegExp.lowercaseCharRegExp.test(password) && + passwordValidationRegExp.uppercaseCharRegExp.test(password) && + passwordValidationRegExp.numericalValueRegExp.test(password) && + passwordValidationRegExp.specialCharRegExp.test(password) + ); + }; + const signupLink = async (e: ChangeEvent): Promise => { e.preventDefault(); @@ -179,17 +195,6 @@ function loginPage(): JSX.Element { const isValidatedString = (value: string): boolean => /^[a-zA-Z]+$/.test(value); - const validatePassword = (password: string): boolean => { - const lengthCheck = new RegExp('^.{6,}$'); - return ( - lengthCheck.test(password) && - passwordValidationRegExp.lowercaseCharRegExp.test(password) && - passwordValidationRegExp.uppercaseCharRegExp.test(password) && - passwordValidationRegExp.numericalValueRegExp.test(password) && - passwordValidationRegExp.specialCharRegExp.test(password) - ); - }; - if ( isValidatedString(signfirstName) && isValidatedString(signlastName) && @@ -213,9 +218,11 @@ function loginPage(): JSX.Element { /* istanbul ignore next */ if (signUpData) { toast.success( - 'Successfully Registered. Please wait until you will be approved.' + role === 'admin' + ? 'Successfully Registered. Please wait until you will be approved.' + : 'Successfully registered. Please wait for admin to approve your request.' ); - + setShowTab('LOGIN'); setSignFormState({ signfirstName: '', signlastName: '', @@ -223,6 +230,12 @@ function loginPage(): JSX.Element { signPassword: '', cPassword: '', }); + setShowAlert({ + lowercaseChar: true, + uppercaseChar: true, + numericValue: true, + specialChar: true, + }); } } catch (error: any) { /* istanbul ignore next */ @@ -260,6 +273,11 @@ function loginPage(): JSX.Element { return; } + if (!validatePassword(formState.password)) { + toast.warn(t('password_invalid')); + return; + } + try { const { data: loginData } = await login({ variables: { @@ -270,21 +288,29 @@ function loginPage(): JSX.Element { /* istanbul ignore next */ if (loginData) { - if ( - loginData.login.user.userType === 'SUPERADMIN' || - (loginData.login.user.userType === 'ADMIN' && - loginData.login.user.adminApproved === true) - ) { + if (role === 'admin') { + if ( + loginData.login.user.userType === 'SUPERADMIN' || + (loginData.login.user.userType === 'ADMIN' && + loginData.login.user.adminApproved === true) + ) { + localStorage.setItem('token', loginData.login.accessToken); + localStorage.setItem('refreshToken', loginData.login.refreshToken); + localStorage.setItem('id', loginData.login.user._id); + localStorage.setItem('UserType', loginData.login.user.userType); + localStorage.setItem('IsLoggedIn', 'TRUE'); + } else { + toast.warn(t('notAuthorised')); + } + } else { localStorage.setItem('token', loginData.login.accessToken); + localStorage.setItem('userId', loginData.login.user._id); localStorage.setItem('refreshToken', loginData.login.refreshToken); - localStorage.setItem('id', loginData.login.user._id); localStorage.setItem('IsLoggedIn', 'TRUE'); - localStorage.setItem('UserType', loginData.login.user.userType); - if (localStorage.getItem('IsLoggedIn') == 'TRUE') { - history.push('/orglist'); - } - } else { - toast.warn(t('notAuthorised')); + navigator.clipboard.writeText(''); + } + if (localStorage.getItem('IsLoggedIn') == 'TRUE') { + history.push(role === 'admin' ? '/orglist' : '/user/organizations/'); } } else { toast.warn(t('notFound')); diff --git a/src/screens/UserPortal/UserLoginPage/UserLoginPage.module.css b/src/screens/UserPortal/UserLoginPage/UserLoginPage.module.css deleted file mode 100644 index 413df8cecd..0000000000 --- a/src/screens/UserPortal/UserLoginPage/UserLoginPage.module.css +++ /dev/null @@ -1,208 +0,0 @@ -.login_background { - min-height: 100vh; -} - -.row .left_portion { - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - height: 100vh; -} - -.row .left_portion .inner .palisadoes_logo { - width: 600px; - height: auto; -} - -.row .right_portion { - min-height: 100vh; - position: relative; - overflow-y: scroll; - display: flex; - flex-direction: column; - justify-content: center; - padding: 1rem 2.5rem; - background: var(--bs-white); -} - -.row .right_portion::-webkit-scrollbar { - display: none; -} - -.row .right_portion .langChangeBtn { - margin: 0; - position: absolute; - top: 1rem; - left: 1rem; -} - -.langChangeBtnStyle { - width: 7.5rem; - height: 2.2rem; - padding: 0; -} - -.row .right_portion .talawa_logo { - height: 5rem; - width: 5rem; - display: block; - margin: 1.5rem auto 1rem; - -webkit-animation: zoomIn 0.3s ease-in-out; - animation: zoomIn 0.3s ease-in-out; -} - -.row .orText { - display: block; - position: absolute; - top: calc(-0.7rem - 0.5rem); - left: calc(50% - 2.6rem); - margin: 0 auto; - padding: 0.5rem 2rem; - z-index: 100; - background: var(--bs-white); - color: var(--bs-secondary); -} - -@media (max-width: 992px) { - .row .left_portion { - padding: 0 2rem; - } - - .row .left_portion .inner .palisadoes_logo { - width: 100%; - } -} - -@media (max-width: 769px) { - .row { - flex-direction: column-reverse; - } - - .row .right_portion, - .row .left_portion { - height: unset; - } - - .row .right_portion { - min-height: 100vh; - overflow-y: unset; - } - - .row .left_portion .inner { - display: flex; - justify-content: center; - } - - .row .left_portion .inner .palisadoes_logo { - height: 70px; - width: unset; - position: absolute; - margin: 0.5rem; - top: 0; - right: 0; - z-index: 100; - } - - .row .left_portion .inner p { - margin-bottom: 0; - padding: 1rem; - } - - .socialIcons { - margin-bottom: 1rem; - } -} - -@media (max-width: 577px) { - .row .right_portion { - padding: 1rem 1rem 0 1rem; - } - - .row .right_portion .langChangeBtn { - position: absolute; - margin: 1rem; - left: 0; - top: 0; - } - - .marginTopForReg { - margin-top: 4rem !important; - } - - .row .right_portion .talawa_logo { - height: 120px; - margin: 0 auto 2rem auto; - } - - .socialIcons { - margin-bottom: 1rem; - } -} - -.active_tab { - -webkit-animation: fadeIn 0.3s ease-in-out; - animation: fadeIn 0.3s ease-in-out; -} - -@-webkit-keyframes zoomIn { - 0% { - opacity: 0; - -webkit-transform: scale(0.5); - transform: scale(0.5); - } - - 100% { - opacity: 1; - -webkit-transform: scale(1); - transform: scale(1); - } -} - -@keyframes zoomIn { - 0% { - opacity: 0; - -webkit-transform: scale(0.5); - transform: scale(0.5); - } - - 100% { - opacity: 1; - -webkit-transform: scale(1); - transform: scale(1); - } -} - -@-webkit-keyframes fadeIn { - 0% { - opacity: 0; - -webkit-transform: translateY(2rem); - transform: translateY(2rem); - } - - 100% { - opacity: 1; - -webkit-transform: translateY(0); - transform: translateY(0); - } -} - -@keyframes fadeIn { - 0% { - opacity: 0; - -webkit-transform: translateY(2rem); - transform: translateY(2rem); - } - - 100% { - opacity: 1; - -webkit-transform: translateY(0); - transform: translateY(0); - } -} - -.socialIcons { - display: flex; - gap: 16px; - justify-content: center; -} diff --git a/src/screens/UserPortal/UserLoginPage/UserLoginPage.test.tsx b/src/screens/UserPortal/UserLoginPage/UserLoginPage.test.tsx deleted file mode 100644 index 7b41dad875..0000000000 --- a/src/screens/UserPortal/UserLoginPage/UserLoginPage.test.tsx +++ /dev/null @@ -1,630 +0,0 @@ -import React from 'react'; -import { MockedProvider } from '@apollo/react-testing'; -import { act, render, screen } from '@testing-library/react'; -import { Provider } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; -import userEvent from '@testing-library/user-event'; -import { I18nextProvider } from 'react-i18next'; -import 'jest-localstorage-mock'; -import 'jest-location-mock'; -import { StaticMockLink } from 'utils/StaticMockLink'; -import LoginPage from './UserLoginPage'; -import { - LOGIN_MUTATION, - RECAPTCHA_MUTATION, - SIGNUP_MUTATION, -} from 'GraphQl/Mutations/mutations'; -import { store } from 'state/store'; -import i18nForTest from 'utils/i18nForTest'; - -const MOCKS = [ - { - request: { - query: LOGIN_MUTATION, - variables: { - email: 'johndoe@gmail.com', - password: 'johndoe', - }, - }, - result: { - data: { - login: { - user: { - _id: '1', - userType: 'ADMIN', - adminApproved: true, - }, - accessToken: 'accessToken', - refreshToken: 'refreshToken', - }, - }, - }, - }, - { - request: { - query: SIGNUP_MUTATION, - variables: { - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - password: 'johnDoe', - }, - }, - result: { - data: { - register: { - user: { - _id: '1', - }, - accessToken: 'accessToken', - refreshToken: 'refreshToken', - }, - }, - }, - }, - { - request: { - query: RECAPTCHA_MUTATION, - variables: { - recaptchaToken: null, - }, - }, - result: { - data: { - recaptcha: true, - }, - }, - }, -]; - -const link = new StaticMockLink(MOCKS, true); - -async function wait(ms = 100): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); -} - -jest.mock('react-toastify', () => ({ - toast: { - success: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }, -})); - -jest.mock('Constant/constant.ts', () => ({ - ...jest.requireActual('Constant/constant.ts'), - REACT_APP_USE_RECAPTCHA: 'yes', - RECAPTCHA_SITE_KEY: 'xxx', -})); - -describe('Talawa-API server fetch check', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('Checks if Talawa-API resource is loaded successfully', async () => { - global.fetch = jest.fn(() => Promise.resolve({} as unknown as Response)); - - await act(async () => { - render( - - - - - - - - - - ); - }); - - expect(fetch).toHaveBeenCalledWith('http://localhost:4000/graphql/'); - }); - - test('displays warning message when resource loading fails', async () => { - const mockError = new Error('Network error'); - global.fetch = jest.fn(() => Promise.reject(mockError)); - - await act(async () => { - render( - - - - - - - - - - ); - }); - - expect(fetch).toHaveBeenCalledWith('http://localhost:4000/graphql/'); - }); -}); - -describe('Testing Login Page Screen', () => { - test('Component Should be rendered properly', async () => { - window.location.assign('/user/organizations'); - - render( - - - - - - - - - - ); - - await wait(); - - expect(screen.getByText(/User Login/i)).toBeInTheDocument(); - expect(window.location).toBeAt('/user/organizations'); - }); - - test('Testing registration functionality', async () => { - const formData = { - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - password: 'johndoe', - confirmPassword: 'johndoe', - }; - - render( - - - - - - - - - - ); - - await wait(); - - userEvent.click(screen.getByTestId(/goToRegisterPortion/i)); - - await wait(); - - userEvent.type( - screen.getByPlaceholderText(/First Name/i), - formData.firstName - ); - userEvent.type( - screen.getByPlaceholderText(/Last name/i), - formData.lastName - ); - userEvent.type(screen.getByTestId(/signInEmail/i), formData.email); - userEvent.type(screen.getByPlaceholderText('Password'), formData.password); - userEvent.type( - screen.getByPlaceholderText('Confirm Password'), - formData.confirmPassword - ); - - userEvent.click(screen.getByTestId('registrationBtn')); - }); - - test('Testing registration functionality, when password and confirm password is not same', async () => { - const formData = { - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - password: 'johndoe', - confirmPassword: 'doeJohn', - }; - - render( - - - - - - - - - - ); - - await wait(); - - userEvent.click(screen.getByTestId(/goToRegisterPortion/i)); - - userEvent.type( - screen.getByPlaceholderText(/First Name/i), - formData.firstName - ); - userEvent.type( - screen.getByPlaceholderText(/Last Name/i), - formData.lastName - ); - userEvent.type(screen.getByTestId(/signInEmail/i), formData.email); - userEvent.type(screen.getByPlaceholderText('Password'), formData.password); - userEvent.type( - screen.getByPlaceholderText('Confirm Password'), - formData.confirmPassword - ); - - userEvent.click(screen.getByTestId('registrationBtn')); - }); - - test('Testing registration functionality, when input is not filled correctly', async () => { - const formData = { - firstName: 'J', - lastName: 'D', - email: 'johndoe@gmail.com', - password: 'joe', - confirmPassword: 'joe', - }; - - render( - - - - - - - - - - ); - - await wait(); - - userEvent.click(screen.getByTestId(/goToRegisterPortion/i)); - - userEvent.type( - screen.getByPlaceholderText(/First Name/i), - formData.firstName - ); - userEvent.type( - screen.getByPlaceholderText(/Last Name/i), - formData.lastName - ); - userEvent.type(screen.getByTestId(/signInEmail/i), formData.email); - userEvent.type(screen.getByPlaceholderText('Password'), formData.password); - userEvent.type( - screen.getByPlaceholderText('Confirm Password'), - formData.confirmPassword - ); - - userEvent.click(screen.getByTestId('registrationBtn')); - }); - - test('switches to login tab on successful registration', async () => { - const formData = { - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - password: 'johndoe', - confirmPassword: 'johndoe', - }; - - render( - - - - - - - - - - ); - - await wait(); - - userEvent.click(screen.getByTestId(/goToRegisterPortion/i)); - userEvent.type( - screen.getByPlaceholderText(/First Name/i), - formData.firstName - ); - userEvent.type( - screen.getByPlaceholderText(/Last name/i), - formData.lastName - ); - userEvent.type(screen.getByTestId(/signInEmail/i), formData.email); - userEvent.type(screen.getByPlaceholderText('Password'), formData.password); - userEvent.type( - screen.getByPlaceholderText('Confirm Password'), - formData.confirmPassword - ); - - userEvent.click(screen.getByTestId('registrationBtn')); - - await wait(); - - // Check if the login tab is now active by checking for elements that only appear in the login tab - expect(screen.getByTestId('loginBtn')).toBeInTheDocument(); - expect(screen.getByTestId('goToRegisterPortion')).toBeInTheDocument(); - }); - - test('Testing toggle login register portion', async () => { - render( - - - - - - - - - - ); - - await wait(); - - userEvent.click(screen.getByTestId('goToRegisterPortion')); - - userEvent.click(screen.getByTestId('goToLoginPortion')); - - await wait(); - }); - - test('Testing login functionality', async () => { - const formData = { - email: 'johndoe@gmail.com', - password: 'johndoe', - }; - - render( - - - - - - - - - - ); - - await wait(); - - userEvent.type(screen.getByTestId(/loginEmail/i), formData.email); - userEvent.type( - screen.getByPlaceholderText(/Enter Password/i), - formData.password - ); - - userEvent.click(screen.getByTestId('loginBtn')); - - await wait(); - }); - - test('Testing password preview feature for login', async () => { - render( - - - - - - - - - - ); - - await wait(); - - const input = screen.getByTestId('password') as HTMLInputElement; - const toggleText = screen.getByTestId('showLoginPassword'); - // password should be hidden - expect(input.type).toBe('password'); - // click the toggle button to show password - userEvent.click(toggleText); - expect(input.type).toBe('text'); - // click the toggle button to hide password - userEvent.click(toggleText); - expect(input.type).toBe('password'); - - await wait(); - }); - - test('Testing password preview feature for register', async () => { - render( - - - - - - - - - - ); - - await wait(); - - userEvent.click(screen.getByTestId('goToRegisterPortion')); - - const input = screen.getByTestId('passwordField') as HTMLInputElement; - const toggleText = screen.getByTestId('showPassword'); - // password should be hidden - expect(input.type).toBe('password'); - // click the toggle button to show password - userEvent.click(toggleText); - expect(input.type).toBe('text'); - // click the toggle button to hide password - userEvent.click(toggleText); - expect(input.type).toBe('password'); - - await wait(); - }); - - test('Testing confirm password preview feature', async () => { - render( - - - - - - - - - - ); - - await wait(); - - userEvent.click(screen.getByTestId('goToRegisterPortion')); - - const input = screen.getByTestId('cpassword') as HTMLInputElement; - const toggleText = screen.getByTestId('showPasswordCon'); - // password should be hidden - expect(input.type).toBe('password'); - // click the toggle button to show password - userEvent.click(toggleText); - expect(input.type).toBe('text'); - // click the toggle button to hide password - userEvent.click(toggleText); - expect(input.type).toBe('password'); - - await wait(); - }); - - test('Testing for the password error warning when user firsts lands on a page', async () => { - render( - - - - - - - - - - ); - await wait(); - - expect(screen.queryByTestId('passwordCheck')).toBeNull(); - }); - - test('Testing for the password error warning when user clicks on password field and password is less than 8 character', async () => { - const password = { - password: '7', - }; - - render( - - - - - - - - - - ); - await wait(); - - userEvent.click(screen.getByTestId('goToRegisterPortion')); - - userEvent.type(screen.getByPlaceholderText('Password'), password.password); - - expect(screen.getByTestId('passwordField')).toHaveFocus(); - - expect(password.password.length).toBeLessThan(8); - - expect(screen.queryByTestId('passwordCheck')).toBeInTheDocument(); - }); - - test('Testing for the password error warning when user clicks on password field and password is greater than or equal to 8 character', async () => { - const password = { - password: '12345678', - }; - - render( - - - - - - - - - - ); - await wait(); - - userEvent.click(screen.getByTestId('goToRegisterPortion')); - - userEvent.type(screen.getByPlaceholderText('Password'), password.password); - - expect(screen.getByTestId('passwordField')).toHaveFocus(); - - expect(password.password.length).toBeGreaterThanOrEqual(8); - - expect(screen.queryByTestId('passwordCheck')).toBeNull(); - }); - - test('Testing for the password error warning when user clicks on fields except password field and password is less than 8 character', async () => { - const password = { - password: '7', - }; - - render( - - - - - - - - - - ); - await wait(); - - userEvent.click(screen.getByTestId('goToRegisterPortion')); - - expect(screen.getByPlaceholderText('Password')).not.toHaveFocus(); - - userEvent.type(screen.getByPlaceholderText('Password'), password.password); - - expect(password.password.length).toBeLessThan(8); - - expect(screen.queryByTestId('passwordCheck')).toBeInTheDocument(); - }); - - test('Testing for the password error warning when user clicks on fields except password field and password is greater than or equal to 8 character', async () => { - const password = { - password: '12345678', - }; - - render( - - - - - - - - - - ); - await wait(); - - userEvent.click(screen.getByTestId('goToRegisterPortion')); - - await wait(); - - expect(screen.getByPlaceholderText('Password')).not.toHaveFocus(); - - userEvent.type(screen.getByPlaceholderText('Password'), password.password); - - expect(password.password.length).toBeGreaterThanOrEqual(8); - - expect(screen.queryByTestId('passwordCheck')).toBeNull(); - }); -}); diff --git a/src/screens/UserPortal/UserLoginPage/UserLoginPage.tsx b/src/screens/UserPortal/UserLoginPage/UserLoginPage.tsx deleted file mode 100644 index 2e16f48bc1..0000000000 --- a/src/screens/UserPortal/UserLoginPage/UserLoginPage.tsx +++ /dev/null @@ -1,625 +0,0 @@ -import { useMutation } from '@apollo/client'; -import type { ChangeEvent } from 'react'; -import React, { useEffect, useRef, useState } from 'react'; -import { Form } from 'react-bootstrap'; -import Button from 'react-bootstrap/Button'; -import Col from 'react-bootstrap/Col'; -import Row from 'react-bootstrap/Row'; -import ReCAPTCHA from 'react-google-recaptcha'; -import { useTranslation } from 'react-i18next'; -import { Link, useHistory } from 'react-router-dom'; -import { toast } from 'react-toastify'; - -import { - FacebookLogo, - LinkedInLogo, - GithubLogo, - InstagramLogo, - SlackLogo, - TwitterLogo, - YoutubeLogo, -} from 'assets/svgs/social-icons'; - -import { REACT_APP_USE_RECAPTCHA, RECAPTCHA_SITE_KEY } from 'Constant/constant'; -import { - LOGIN_MUTATION, - RECAPTCHA_MUTATION, - SIGNUP_MUTATION, -} from 'GraphQl/Mutations/mutations'; -import { ReactComponent as TalawaLogo } from 'assets/svgs/talawa.svg'; -import { ReactComponent as PalisadoesLogo } from 'assets/svgs/palisadoes.svg'; -import ChangeLanguageDropDown from 'components/ChangeLanguageDropdown/ChangeLanguageDropDown'; -import LoginPortalToggle from 'components/LoginPortalToggle/LoginPortalToggle'; -import Loader from 'components/Loader/Loader'; -import { errorHandler } from 'utils/errorHandler'; -import styles from './UserLoginPage.module.css'; -import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined'; - -function loginPage(): JSX.Element { - const { t } = useTranslation('translation', { keyPrefix: 'userLoginPage' }); - const history = useHistory(); - - document.title = t('title'); - - const [showTab, setShowTab] = useState<'LOGIN' | 'REGISTER'>('LOGIN'); - const [componentLoader, setComponentLoader] = useState(true); - const [isInputFocused, setIsInputFocused] = useState(false); - const [signformState, setSignFormState] = useState({ - signfirstName: '', - signlastName: '', - signEmail: '', - signPassword: '', - cPassword: '', - }); - const [formState, setFormState] = useState({ - email: '', - password: '', - }); - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = - useState(false); - const recaptchaRef = useRef(null); - - useEffect(() => { - const isLoggedIn = localStorage.getItem('IsLoggedIn'); - if (isLoggedIn == 'TRUE') { - history.push('/user/organizations/'); - } - setComponentLoader(false); - }, []); - - const togglePassword = (): void => setShowPassword(!showPassword); - const toggleConfirmPassword = (): void => - setShowConfirmPassword(!showConfirmPassword); - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [login, { loading: loginLoading }] = useMutation(LOGIN_MUTATION); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [signup, { loading: signinLoading }] = useMutation(SIGNUP_MUTATION); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [recaptcha, { loading: recaptchaLoading }] = - useMutation(RECAPTCHA_MUTATION); - - useEffect(() => { - async function loadResource(): Promise { - try { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const response = await fetch('http://localhost:4000/graphql/'); - } catch (error: any) { - /* istanbul ignore next */ - errorHandler(t, error); - } - } - - loadResource(); - }, []); - - const verifyRecaptcha = async ( - recaptchaToken: any - ): Promise => { - try { - /* istanbul ignore next */ - if (REACT_APP_USE_RECAPTCHA !== 'yes') { - return true; - } - const { data } = await recaptcha({ - variables: { - recaptchaToken, - }, - }); - - return data.recaptcha; - } catch (error: any) { - /* istanbul ignore next */ - toast.error(t('captchaError')); - } - }; - - const signupLink = async (e: ChangeEvent): Promise => { - e.preventDefault(); - - const { signfirstName, signlastName, signEmail, signPassword, cPassword } = - signformState; - - const recaptchaToken = recaptchaRef.current?.getValue(); - recaptchaRef.current?.reset(); - - const isVerified = await verifyRecaptcha(recaptchaToken); - /* istanbul ignore next */ - if (!isVerified) { - toast.error(t('Please_check_the_captcha')); - return; - } - - if ( - signfirstName.length > 1 && - signlastName.length > 1 && - signEmail.length >= 8 && - signPassword.length > 1 - ) { - if (cPassword == signPassword) { - try { - const { data: signUpData } = await signup({ - variables: { - firstName: signfirstName, - lastName: signlastName, - email: signEmail, - password: signPassword, - }, - }); - - /* istanbul ignore next */ - if (signUpData) { - toast.success(t('afterRegister')); - - setShowTab('LOGIN'); - - setSignFormState({ - signfirstName: '', - signlastName: '', - signEmail: '', - signPassword: '', - cPassword: '', - }); - } - } catch (error: any) { - /* istanbul ignore next */ - errorHandler(t, error); - } - } else { - toast.warn(t('passwordMismatches')); - } - } else { - toast.warn(t('fillCorrectly')); - } - }; - - const loginLink = async (e: ChangeEvent): Promise => { - e.preventDefault(); - - const recaptchaToken = recaptchaRef.current?.getValue(); - recaptchaRef.current?.reset(); - - const isVerified = await verifyRecaptcha(recaptchaToken); - /* istanbul ignore next */ - if (!isVerified) { - toast.error(t('Please_check_the_captcha')); - return; - } - - try { - const { data: loginData } = await login({ - variables: { - email: formState.email, - password: formState.password, - }, - }); - - /* istanbul ignore next */ - if (loginData) { - localStorage.setItem('token', loginData.login.accessToken); - localStorage.setItem('userId', loginData.login.user._id); - localStorage.setItem('refreshToken', loginData.login.refreshToken); - localStorage.setItem('IsLoggedIn', 'TRUE'); - navigator.clipboard.writeText(''); - if (localStorage.getItem('IsLoggedIn') == 'TRUE') { - history.push('/user/organizations/'); - } - } else { - toast.warn(t('notAuthorised')); - } - } catch (error: any) { - /* istanbul ignore next */ - errorHandler(t, error); - } - }; - - if (componentLoader || loginLoading || signinLoading || recaptchaLoading) { - return ; - } - - return ( - <> -
- - -
- -

{t('fromPalisadoes')}

-
- - - - -
- - - - - - {/* LOGIN FORM */} -
-
-

- {t('userLogin')} -

- {t('email')} -
- { - setFormState({ - ...formState, - email: e.target.value, - }); - }} - autoComplete="username" - data-testid="loginEmail" - /> - -
- {t('password')} -
- { - setFormState({ - ...formState, - password: e.target.value, - }); - }} - autoComplete="current-password" - /> - -
-
- - {t('forgotPassword')} - -
- {REACT_APP_USE_RECAPTCHA === 'yes' ? ( -
- -
- ) : ( - /* istanbul ignore next */ - <> - )} - -
-
- {t('OR')} -
- -
-
- {/* REGISTER FORM */} -
-
-

- {t('register')} -

- - -
- {t('firstName')} - { - setSignFormState({ - ...signformState, - signfirstName: e.target.value, - }); - }} - /> -
- - -
- {t('lastName')} - { - setSignFormState({ - ...signformState, - signlastName: e.target.value, - }); - }} - /> -
- -
-
- {t('email')} -
- { - setSignFormState({ - ...signformState, - signEmail: e.target.value.toLowerCase(), - }); - }} - /> - -
-
- -
- {t('password')} -
- setIsInputFocused(true)} - onBlur={(): void => setIsInputFocused(false)} - required - value={signformState.signPassword} - onChange={(e): void => { - setSignFormState({ - ...signformState, - signPassword: e.target.value, - }); - }} - /> - -
- {isInputFocused && - signformState.signPassword.length < 8 && ( -
- {t('atleast_8_char_long')} -
- )} - {!isInputFocused && - signformState.signPassword.length > 0 && - signformState.signPassword.length < 8 && ( -
- {t('atleast_8_char_long')} -
- )} -
-
- {t('confirmPassword')} -
- { - setSignFormState({ - ...signformState, - cPassword: e.target.value, - }); - }} - data-testid="cpassword" - autoComplete="new-password" - /> - -
- {signformState.cPassword.length > 0 && - signformState.signPassword !== - signformState.cPassword && ( -
- {t('Password_and_Confirm_password_mismatches.')} -
- )} -
- {REACT_APP_USE_RECAPTCHA === 'yes' ? ( -
- -
- ) : ( - /* istanbul ignore next */ - <> - )} - -
-
- {t('OR')} -
- -
-
-
- -
-
- - ); -} - -export default loginPage; From dc41193e1592d00b23d52fe1eb99b515f55746c0 Mon Sep 17 00:00:00 2001 From: rishabhbizzle Date: Thu, 25 Jan 2024 01:21:57 +0530 Subject: [PATCH 2/6] feat: Add test cases for User Login Component --- src/App.tsx | 2 +- src/screens/LoginPage/LoginPage.tsx | 4 +- src/screens/LoginPage/LoginPageUser.test.tsx | 674 +++++++++++++++++++ 3 files changed, 677 insertions(+), 3 deletions(-) create mode 100644 src/screens/LoginPage/LoginPageUser.test.tsx diff --git a/src/App.tsx b/src/App.tsx index 1ff00e7fbc..bae62433fe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -114,7 +114,7 @@ function app(): JSX.Element { {/* User Portal Routes */} - } /> + { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); + +jest.mock('Constant/constant.ts', () => ({ + ...jest.requireActual('Constant/constant.ts'), + REACT_APP_USE_RECAPTCHA: 'yes', + RECAPTCHA_SITE_KEY: 'xxx', +})); + +describe('Talawa-API server fetch check', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('Checks if Talawa-API resource is loaded successfully', async () => { + global.fetch = jest.fn(() => Promise.resolve({} as unknown as Response)); + + await act(async () => { + render( + + + + + + + + + + ); + }); + + expect(fetch).toHaveBeenCalledWith('http://localhost:4000/graphql/'); + }); + + test('displays warning message when resource loading fails', async () => { + const mockError = new Error('Network error'); + global.fetch = jest.fn(() => Promise.reject(mockError)); + + await act(async () => { + render( + + + + + + + + + + ); + }); + + expect(fetch).toHaveBeenCalledWith('http://localhost:4000/graphql/'); + }); +}); + +describe('Testing Login Page Screen', () => { + test('Component Should be rendered properly', async () => { + window.location.assign('/user/organizations'); + + render( + + + + + + + + + + ); + + await wait(); + + expect(screen.getByText(/Admin/i)).toBeInTheDocument(); + expect(window.location).toBeAt('/user/organizations'); + }); + + test('Testing registration functionality', async () => { + const formData = { + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@gmail.com', + password: 'John@123', + confirmPassword: 'John@123', + }; + + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId(/goToRegisterPortion/i)); + + await wait(); + + userEvent.type( + screen.getByPlaceholderText(/First Name/i), + formData.firstName + ); + userEvent.type( + screen.getByPlaceholderText(/Last name/i), + formData.lastName + ); + userEvent.type(screen.getByTestId(/signInEmail/i), formData.email); + userEvent.type(screen.getByPlaceholderText('Password'), formData.password); + userEvent.type( + screen.getByPlaceholderText('Confirm Password'), + formData.confirmPassword + ); + + userEvent.click(screen.getByTestId('registrationBtn')); + }); + + test('Testing registration functionality when all inputs are invalid', async () => { + const formData = { + firstName: '1234', + lastName: '8890', + email: 'j@l.co', + password: 'john@123', + confirmPassword: 'john@123', + }; + + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId(/goToRegisterPortion/i)); + + await wait(); + + userEvent.type( + screen.getByPlaceholderText(/First Name/i), + formData.firstName + ); + userEvent.type( + screen.getByPlaceholderText(/Last name/i), + formData.lastName + ); + userEvent.type(screen.getByTestId(/signInEmail/i), formData.email); + userEvent.type(screen.getByPlaceholderText('Password'), formData.password); + userEvent.type( + screen.getByPlaceholderText('Confirm Password'), + formData.confirmPassword + ); + userEvent.click(screen.getByTestId('registrationBtn')); + }); + + test('Testing registration functionality, when password and confirm password is not same', async () => { + const formData = { + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@gmail.com', + password: 'johnDoe@1', + confirmPassword: 'doeJohn@2', + }; + + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId(/goToRegisterPortion/i)); + + userEvent.type( + screen.getByPlaceholderText(/First Name/i), + formData.firstName + ); + userEvent.type( + screen.getByPlaceholderText(/Last Name/i), + formData.lastName + ); + userEvent.type(screen.getByTestId(/signInEmail/i), formData.email); + userEvent.type(screen.getByPlaceholderText('Password'), formData.password); + userEvent.type( + screen.getByPlaceholderText('Confirm Password'), + formData.confirmPassword + ); + + userEvent.click(screen.getByTestId('registrationBtn')); + }); + + test('Testing registration functionality, when input is not filled correctly', async () => { + const formData = { + firstName: 'J', + lastName: 'D', + email: 'johndoe@gmail.com', + password: 'joe', + confirmPassword: 'joe', + }; + + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId(/goToRegisterPortion/i)); + + userEvent.type( + screen.getByPlaceholderText(/First Name/i), + formData.firstName + ); + userEvent.type( + screen.getByPlaceholderText(/Last Name/i), + formData.lastName + ); + userEvent.type(screen.getByTestId(/signInEmail/i), formData.email); + userEvent.type(screen.getByPlaceholderText('Password'), formData.password); + userEvent.type( + screen.getByPlaceholderText('Confirm Password'), + formData.confirmPassword + ); + + userEvent.click(screen.getByTestId('registrationBtn')); + }); + + test('switches to login tab on successful registration', async () => { + const formData = { + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@gmail.com', + password: 'johndoe', + confirmPassword: 'johndoe', + }; + + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId(/goToRegisterPortion/i)); + userEvent.type( + screen.getByPlaceholderText(/First Name/i), + formData.firstName + ); + userEvent.type( + screen.getByPlaceholderText(/Last name/i), + formData.lastName + ); + userEvent.type(screen.getByTestId(/signInEmail/i), formData.email); + userEvent.type(screen.getByPlaceholderText('Password'), formData.password); + userEvent.type( + screen.getByPlaceholderText('Confirm Password'), + formData.confirmPassword + ); + + userEvent.click(screen.getByTestId('registrationBtn')); + + await wait(); + + // Check if the login tab is now active by checking for elements that only appear in the login tab + expect(screen.getByTestId('loginBtn')).toBeInTheDocument(); + expect(screen.getByTestId('goToRegisterPortion')).toBeInTheDocument(); + }); + + test('Testing toggle login register portion', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('goToRegisterPortion')); + + userEvent.click(screen.getByTestId('goToLoginPortion')); + + await wait(); + }); + + test('Testing login functionality', async () => { + const formData = { + email: 'johndoe@gmail.com', + password: 'johndoe', + }; + + render( + + + + + + + + + + ); + + await wait(); + + userEvent.type(screen.getByTestId(/loginEmail/i), formData.email); + userEvent.type( + screen.getByPlaceholderText(/Enter Password/i), + formData.password + ); + + userEvent.click(screen.getByTestId('loginBtn')); + + await wait(); + }); + + test('Testing password preview feature for login', async () => { + render( + + + + + + + + + + ); + + await wait(); + + const input = screen.getByTestId('password') as HTMLInputElement; + const toggleText = screen.getByTestId('showLoginPassword'); + // password should be hidden + expect(input.type).toBe('password'); + // click the toggle button to show password + userEvent.click(toggleText); + expect(input.type).toBe('text'); + // click the toggle button to hide password + userEvent.click(toggleText); + expect(input.type).toBe('password'); + + await wait(); + }); + + test('Testing password preview feature for register', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('goToRegisterPortion')); + + const input = screen.getByTestId('passwordField') as HTMLInputElement; + const toggleText = screen.getByTestId('showPassword'); + // password should be hidden + expect(input.type).toBe('password'); + // click the toggle button to show password + userEvent.click(toggleText); + expect(input.type).toBe('text'); + // click the toggle button to hide password + userEvent.click(toggleText); + expect(input.type).toBe('password'); + + await wait(); + }); + + test('Testing confirm password preview feature', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('goToRegisterPortion')); + + const input = screen.getByTestId('cpassword') as HTMLInputElement; + const toggleText = screen.getByTestId('showPasswordCon'); + // password should be hidden + expect(input.type).toBe('password'); + // click the toggle button to show password + userEvent.click(toggleText); + expect(input.type).toBe('text'); + // click the toggle button to hide password + userEvent.click(toggleText); + expect(input.type).toBe('password'); + + await wait(); + }); + + test('Testing for the password error warning when user firsts lands on a page', async () => { + render( + + + + + + + + + + ); + await wait(); + + expect(screen.queryByTestId('passwordCheck')).toBeNull(); + }); + + test('Testing for the password error warning when user clicks on password field and password is less than 8 character', async () => { + const password = { + password: '7', + }; + + render( + + + + + + + + + + ); + await wait(); + + userEvent.click(screen.getByTestId('goToRegisterPortion')); + + userEvent.type(screen.getByPlaceholderText('Password'), password.password); + + expect(screen.getByTestId('passwordField')).toHaveFocus(); + + expect(password.password.length).toBeLessThan(8); + + expect(screen.queryByTestId('passwordCheck')).toBeInTheDocument(); + }); + + test('Testing for the password error warning when user clicks on password field and password is greater than or equal to 8 character', async () => { + const password = { + password: '12345678', + }; + + render( + + + + + + + + + + ); + await wait(); + + userEvent.click(screen.getByTestId('goToRegisterPortion')); + + userEvent.type(screen.getByPlaceholderText('Password'), password.password); + + expect(screen.getByTestId('passwordField')).toHaveFocus(); + + expect(password.password.length).toBeGreaterThanOrEqual(8); + + expect(screen.queryByTestId('passwordCheck')).toBeNull(); + }); + + test('Testing for the password error warning when user clicks on fields except password field and password is less than 8 character', async () => { + const password = { + password: '7', + }; + + render( + + + + + + + + + + ); + await wait(); + + userEvent.click(screen.getByTestId('goToRegisterPortion')); + + expect(screen.getByPlaceholderText('Password')).not.toHaveFocus(); + + userEvent.type(screen.getByPlaceholderText('Password'), password.password); + + expect(password.password.length).toBeLessThan(8); + + expect(screen.queryByTestId('passwordCheck')).toBeInTheDocument(); + }); + + test('Testing for the password error warning when user clicks on fields except password field and password is greater than or equal to 8 character', async () => { + const password = { + password: '12345678', + }; + + render( + + + + + + + + + + ); + await wait(); + + userEvent.click(screen.getByTestId('goToRegisterPortion')); + + await wait(); + + expect(screen.getByPlaceholderText('Password')).not.toHaveFocus(); + + userEvent.type(screen.getByPlaceholderText('Password'), password.password); + + expect(password.password.length).toBeGreaterThanOrEqual(8); + + expect(screen.queryByTestId('passwordCheck')).toBeNull(); + }); +}); From 333f39a1bc9f48f6b4bcb1dfb976232247b2345b Mon Sep 17 00:00:00 2001 From: rishabhbizzle Date: Thu, 25 Jan 2024 01:39:03 +0530 Subject: [PATCH 3/6] fix: typo in LoginPage --- src/screens/LoginPage/LoginPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/screens/LoginPage/LoginPage.tsx b/src/screens/LoginPage/LoginPage.tsx index 991b2b9369..d0542ddaa6 100644 --- a/src/screens/LoginPage/LoginPage.tsx +++ b/src/screens/LoginPage/LoginPage.tsx @@ -114,7 +114,7 @@ function loginPage({ role = 'user' }: InterfaceComponentProps): JSX.Element { if (isLoggedIn == 'TRUE') { role === 'admin' ? history.push('/orglist') - : history.push('/user/organizations/'); + : history.push('/user/organizations'); } setComponentLoader(false); }, []); From 28c23f7d5b1be80bca206fb12ff27780fc536e04 Mon Sep 17 00:00:00 2001 From: rishabhbizzle Date: Thu, 25 Jan 2024 19:34:17 +0530 Subject: [PATCH 4/6] refactor: history.push line --- src/screens/LoginPage/LoginPage.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/screens/LoginPage/LoginPage.tsx b/src/screens/LoginPage/LoginPage.tsx index d0542ddaa6..736099a37d 100644 --- a/src/screens/LoginPage/LoginPage.tsx +++ b/src/screens/LoginPage/LoginPage.tsx @@ -112,9 +112,7 @@ function loginPage({ role = 'user' }: InterfaceComponentProps): JSX.Element { useEffect(() => { const isLoggedIn = localStorage.getItem('IsLoggedIn'); if (isLoggedIn == 'TRUE') { - role === 'admin' - ? history.push('/orglist') - : history.push('/user/organizations'); + history.push(role === 'admin' ? '/orglist' : '/user/organizations'); } setComponentLoader(false); }, []); @@ -310,7 +308,7 @@ function loginPage({ role = 'user' }: InterfaceComponentProps): JSX.Element { navigator.clipboard.writeText(''); } if (localStorage.getItem('IsLoggedIn') == 'TRUE') { - history.push(role === 'admin' ? '/orglist' : '/user/organizations/'); + history.push(role === 'admin' ? '/orglist' : '/user/organizations'); } } else { toast.warn(t('notFound')); From 206b7f52919e66a941b224304e38329946598cfb Mon Sep 17 00:00:00 2001 From: rishabhbizzle Date: Sat, 3 Feb 2024 01:35:26 +0530 Subject: [PATCH 5/6] fix: Test coverage issue in LoginPage --- src/screens/LoginPage/LoginPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/screens/LoginPage/LoginPage.tsx b/src/screens/LoginPage/LoginPage.tsx index e83c9f4719..8c8e67a83b 100644 --- a/src/screens/LoginPage/LoginPage.tsx +++ b/src/screens/LoginPage/LoginPage.tsx @@ -280,6 +280,7 @@ function loginPage({ role = 'user' }: InterfaceComponentProps): JSX.Element { } try { + /* istanbul ignore next */ const { data: loginData } = await login({ variables: { email: formState.email, From 8bc9e743a87962409065745e6d71aa5b19f24784 Mon Sep 17 00:00:00 2001 From: rishabhbizzle Date: Sat, 3 Feb 2024 01:55:59 +0530 Subject: [PATCH 6/6] fix: remove comment --- src/screens/LoginPage/LoginPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/screens/LoginPage/LoginPage.tsx b/src/screens/LoginPage/LoginPage.tsx index 8c8e67a83b..e83c9f4719 100644 --- a/src/screens/LoginPage/LoginPage.tsx +++ b/src/screens/LoginPage/LoginPage.tsx @@ -280,7 +280,6 @@ function loginPage({ role = 'user' }: InterfaceComponentProps): JSX.Element { } try { - /* istanbul ignore next */ const { data: loginData } = await login({ variables: { email: formState.email,