From 4ae2f772ffbe4fb4018ab68737e1967e5403d670 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Fri, 22 Sep 2023 18:24:26 -0300 Subject: [PATCH] chore: resetPassword --- apps/meteor/tests/e2e/forgot-password.spec.ts | 71 +++++----- apps/meteor/tests/e2e/page-objects/auth.ts | 5 + apps/meteor/tests/e2e/reset-password.spec.ts | 35 +++++ .../web-ui-registration/src/RegisterForm.tsx | 16 ++- .../src/ResetPassword/ResetPasswordPage.tsx | 129 +++++++++++------- 5 files changed, 171 insertions(+), 85 deletions(-) create mode 100644 apps/meteor/tests/e2e/reset-password.spec.ts diff --git a/apps/meteor/tests/e2e/forgot-password.spec.ts b/apps/meteor/tests/e2e/forgot-password.spec.ts index ee531d59cdfa9..75d7a9eef786d 100644 --- a/apps/meteor/tests/e2e/forgot-password.spec.ts +++ b/apps/meteor/tests/e2e/forgot-password.spec.ts @@ -2,40 +2,47 @@ import { Registration } from './page-objects'; import { test, expect } from './utils/test'; test.describe.parallel('Forgot Password', () => { - let poRegistration: Registration; - - test.beforeEach(async ({ page }) => { - poRegistration = new Registration(page); - - await page.goto('/home'); - await poRegistration.btnForgotPassword.click(); - }); - - test('Email validation', async () => { - await test.step('expect trigger a validation error if no email is provided', async () => { - await poRegistration.btnSendInstructions.click(); - await expect(poRegistration.inputEmail).toBeInvalid(); + let poRegistration: Registration; + + test.describe('Email validation', () => { + test.beforeEach(async ({ page }) => { + poRegistration = new Registration(page); + + await page.goto('/home'); + await poRegistration.btnForgotPassword.click(); }); - await test.step('expect trigger a validation if a invalid email is provided (1)', async () => { - await poRegistration.inputEmail.fill('mail@mail'); - await poRegistration.btnSendInstructions.click(); - - await expect(poRegistration.inputEmail).toBeInvalid(); + test('Send email to recover account', async () => { + await test.step('expect trigger a validation error if no email is provided', async () => { + await poRegistration.btnSendInstructions.click(); + await expect(poRegistration.inputEmail).toBeInvalid(); + }); + + await test.step('expect trigger a validation if a invalid email is provided (1)', async () => { + await poRegistration.inputEmail.fill('mail@mail'); + await poRegistration.btnSendInstructions.click(); + + await expect(poRegistration.inputEmail).toBeInvalid(); + }); + + await test.step('expect trigger a validation if a invalid email is provided (2)', async () => { + await poRegistration.inputEmail.fill('mail'); + await poRegistration.btnSendInstructions.click(); + + await expect(poRegistration.inputEmail).toBeInvalid(); + }); + + await test.step('expect to show a success callout if a valid email is provided', async () => { + await poRegistration.inputEmail.fill('mail@mail.com'); + await poRegistration.btnSendInstructions.click(); + + await expect(poRegistration.forgotPasswordEmailCallout).toBeVisible(); + }); }); - await test.step('expect trigger a validation if a invalid email is provided (2)', async () => { - await poRegistration.inputEmail.fill('mail'); - await poRegistration.btnSendInstructions.click(); - - await expect(poRegistration.inputEmail).toBeInvalid(); - }); - - await test.step('expect to show a success toast if a valid email is provided', async () => { - await poRegistration.inputEmail.fill('mail@mail.com'); - await poRegistration.btnSendInstructions.click(); - - await expect(poRegistration.forgotPasswordEmailCallout).toBeVisible(); - }); - }); + test('should not have any accessibility violations', async ({ makeAxeBuilder }) => { + const results = await makeAxeBuilder().analyze(); + expect(results.violations).toEqual([]); + }) + }) }); diff --git a/apps/meteor/tests/e2e/page-objects/auth.ts b/apps/meteor/tests/e2e/page-objects/auth.ts index b03e45ee22b7c..9b47d2e44adca 100644 --- a/apps/meteor/tests/e2e/page-objects/auth.ts +++ b/apps/meteor/tests/e2e/page-objects/auth.ts @@ -11,6 +11,11 @@ export class Registration { return this.page.locator('role=button[name="Send instructions"]'); } + get btnReset(): Locator { + return this.page.locator('role=button[name="Reset"]'); + } + + get btnLogin(): Locator { return this.page.locator('role=button[name="Login"]'); } diff --git a/apps/meteor/tests/e2e/reset-password.spec.ts b/apps/meteor/tests/e2e/reset-password.spec.ts new file mode 100644 index 0000000000000..fc5e0b7037847 --- /dev/null +++ b/apps/meteor/tests/e2e/reset-password.spec.ts @@ -0,0 +1,35 @@ +import { Registration } from './page-objects'; +import { setSettingValueById } from './utils/setSettingValueById'; +import { test, expect } from './utils/test'; + +test.describe.parallel('Reset Password', () => { + let poRegistration: Registration; + + test.beforeEach(async ({ api, page }) => { + poRegistration = new Registration(page); + await setSettingValueById(api, 'Accounts_RequirePasswordConfirmation', true); + + await page.goto('/reset-password/someToken'); + }); + + test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'Accounts_RequirePasswordConfirmation', true); + }) + + test('should confirm password be invalid', async () => { + await poRegistration.inputPassword.fill('123456'); + await poRegistration.inputPasswordConfirm.fill('123455'); + await poRegistration.btnReset.click(); + await expect(poRegistration.inputPasswordConfirm).toBeInvalid(); + }); + + test('should confirm password not be visible', async ({ api }) => { + await setSettingValueById(api, 'Accounts_RequirePasswordConfirmation', false); + await expect(poRegistration.inputPasswordConfirm).not.toBeVisible(); + }) + + test('should not have any accessibility violations', async ({ makeAxeBuilder }) => { + const results = await makeAxeBuilder().analyze(); + expect(results.violations).toEqual([]); + }) +}); diff --git a/packages/web-ui-registration/src/RegisterForm.tsx b/packages/web-ui-registration/src/RegisterForm.tsx index eb0aa7229f6d8..265a414510c0b 100644 --- a/packages/web-ui-registration/src/RegisterForm.tsx +++ b/packages/web-ui-registration/src/RegisterForm.tsx @@ -129,9 +129,10 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo {errors.name && ( - {t('registration.component.form.requiredField')} + {errors.name.message} )} @@ -159,6 +160,7 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo })} placeholder={usernameOrEmailPlaceholder || t('registration.component.form.emailPlaceholder')} error={errors?.email?.message} + aria-required='true' aria-invalid={errors.email ? 'true' : 'false'} aria-describedby={`${emailId}-error`} id={emailId} @@ -180,6 +182,7 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo required: t('registration.component.form.requiredField'), })} error={errors?.username?.message} + aria-required='true' aria-invalid={errors.username ? 'true' : 'false'} aria-describedby={`${usernameId}-error`} id={usernameId} @@ -202,7 +205,8 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo required: t('registration.component.form.requiredField'), validate: () => (!passwordIsValid ? t('Password_must_meet_the_complexity_requirements') : true), })} - error={errors.password && (errors.password?.message || t('registration.component.form.requiredField'))} + error={errors.password?.message} + aria-required='true' aria-invalid={errors.password ? 'true' : undefined} id={passwordId} placeholder={passwordPlaceholder || t('Create_a_password')} @@ -227,6 +231,7 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo validate: (val: string) => (watch('password') === val ? true : t('registration.component.form.invalidConfirmPass')), })} error={errors.passwordConfirmation?.message} + aria-required='true' aria-invalid={errors.passwordConfirmation ? 'true' : 'false'} id={passwordConfirmationId} aria-describedby={`${passwordConfirmationId}-error`} @@ -252,6 +257,7 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo required: t('registration.component.form.requiredField'), })} error={errors?.reason?.message} + aria-required='true' aria-invalid={errors.reason ? 'true' : 'false'} aria-describedby={`${reasonId}-error`} id={reasonId} @@ -259,7 +265,7 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo {errors.reason && ( - {t('registration.component.form.requiredField')} + {errors.reason.message} )} diff --git a/packages/web-ui-registration/src/ResetPassword/ResetPasswordPage.tsx b/packages/web-ui-registration/src/ResetPassword/ResetPasswordPage.tsx index d3a3e6fa74138..19b0a13983bb7 100644 --- a/packages/web-ui-registration/src/ResetPassword/ResetPasswordPage.tsx +++ b/packages/web-ui-registration/src/ResetPassword/ResetPasswordPage.tsx @@ -1,10 +1,11 @@ -import { Button, Field, Modal, PasswordInput } from '@rocket.chat/fuselage'; +import { Button, FieldGroup, Field, FieldLabel, ButtonGroup, PasswordInput, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { Form } from '@rocket.chat/layout'; import { PasswordVerifier, useValidatePassword } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useRouter, useRouteParameter, useUser, useMethod, useTranslation, useLoginWithToken } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; +import { useEffect, useRef } from 'react'; import { useForm } from 'react-hook-form'; import HorizontalTemplate from '../template/HorizontalTemplate'; @@ -21,10 +22,15 @@ const ResetPasswordPage = (): ReactElement => { const resetPassword = useMethod('resetPassword'); const token = useRouteParameter('token'); + const resetPasswordFormRef = useRef(null); const passwordId = useUniqueId(); + const passwordConfirmationId = useUniqueId(); const passwordVerifierId = useUniqueId(); + const formLabelId = useUniqueId(); const requiresPasswordConfirmation = useSetting('Accounts_RequirePasswordConfirmation'); + const passwordPlaceholder = String(useSetting('Accounts_PasswordPlaceholder')); + const passwordConfirmationPlaceholder = String(useSetting('Accounts_ConfirmPasswordPlaceholder')); const router = useRouter(); @@ -36,7 +42,7 @@ const ResetPasswordPage = (): ReactElement => { register, handleSubmit, setError, - formState: { errors, isValid }, + formState: { errors, isSubmitting }, watch, } = useForm<{ password: string; @@ -48,76 +54,103 @@ const ResetPasswordPage = (): ReactElement => { const password = watch('password'); const passwordIsValid = useValidatePassword(password); - const submit = handleSubmit(async (data) => { + useEffect(() => { + if (resetPasswordFormRef.current) { + resetPasswordFormRef.current.focus(); + } + }, []); + + const handleResetPassword = async ({ password }: { password: string }) => { try { if (token) { - const result = await resetPassword(token, data.password); + const result = await resetPassword(token, password); await loginWithToken(result.token); router.navigate('/home'); } else { - await setUserPassword(data.password); + await setUserPassword(password); } } catch ({ error, reason }: any) { const _error = reason ?? error; setError('password', { message: String(_error) }); } - }); + }; return ( -
+ - {t('Reset_password')} + {t('Reset_password')} + {t(changePasswordReason)} - - {t(changePasswordReason)} - - (!passwordIsValid ? t('Password_must_meet_the_complexity_requirements') : true), - })} - error={errors.password?.message} - aria-invalid={errors.password ? 'true' : 'false'} - id={passwordId} - placeholder={t('Create_a_password')} - name='password' - autoComplete='off' - aria-describedby={passwordVerifierId} - /> - - {errors?.password && ( - - {errors.password.message} - - )} - - {requiresPasswordConfirmation && ( - + + + + {t('registration.component.form.password')} + + password === val, + {...register('password', { + required: t('registration.component.form.requiredField'), + validate: () => (!passwordIsValid ? t('Password_must_meet_the_complexity_requirements') : true), })} - error={errors.passwordConfirmation?.type === 'validate' ? t('registration.component.form.invalidConfirmPass') : undefined} - aria-invalid={errors.passwordConfirmation ? 'true' : false} - id='passwordConfirmation' - placeholder={t('Confirm_password')} - disabled={!passwordIsValid} + error={errors?.password?.message} + aria-invalid={errors.password ? 'true' : 'false'} + aria-required='true' + id={passwordId} + placeholder={passwordPlaceholder || t('Create_a_password')} + aria-describedby={`${passwordVerifierId} ${passwordId}-error`} /> - + + {errors?.password && ( + + {errors.password.message} + + )} + + + {requiresPasswordConfirmation && ( + + + {t('registration.component.form.confirmPassword')} + + + (password === val ? true : t('registration.component.form.invalidConfirmPass')), + })} + error={errors?.passwordConfirmation?.message} + aria-required='true' + aria-invalid={errors.passwordConfirmation ? 'true' : 'false'} + aria-describedby={`${passwordConfirmationId}-error`} + id={passwordConfirmationId} + placeholder={passwordConfirmationPlaceholder || t('Confirm_password')} + disabled={!passwordIsValid} + /> + + {errors.passwordConfirmation && ( + + {errors.passwordConfirmation?.message} + + )} + )} - {errors && {errors.password?.message}} - + - - - +