Skip to content

Commit

Permalink
chore: resetPassword
Browse files Browse the repository at this point in the history
  • Loading branch information
dougfabris committed Sep 22, 2023
1 parent c714962 commit 4ae2f77
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 85 deletions.
71 changes: 39 additions & 32 deletions apps/meteor/tests/e2e/forgot-password.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('[email protected]');
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('[email protected]');
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([]);
})
})
});
5 changes: 5 additions & 0 deletions apps/meteor/tests/e2e/page-objects/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]');
}
Expand Down
35 changes: 35 additions & 0 deletions apps/meteor/tests/e2e/reset-password.spec.ts
Original file line number Diff line number Diff line change
@@ -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([]);
})
});
16 changes: 11 additions & 5 deletions packages/web-ui-registration/src/RegisterForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,10 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo
<Field.Row>
<TextInput
{...register('name', {
required: requireNameForRegister,
required: requireNameForRegister ? t('registration.component.form.requiredField') : false,
})}
error={errors.name && t('registration.component.form.requiredField')}
error={errors?.name?.message}
aria-required={requireNameForRegister}
aria-invalid={errors.name ? 'true' : 'false'}
placeholder={t('onboarding.form.adminInfoForm.fields.fullName.placeholder')}
aria-describedby={`${nameId}-error`}
Expand All @@ -140,7 +141,7 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo
</Field.Row>
{errors.name && (
<Field.Error aria-live='assertive' id={`${nameId}-error`}>
{t('registration.component.form.requiredField')}
{errors.name.message}
</Field.Error>
)}
</Field>
Expand All @@ -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}
Expand All @@ -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}
Expand All @@ -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')}
Expand All @@ -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`}
Expand All @@ -252,14 +257,15 @@ 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}
/>
</Field.Row>
{errors.reason && (
<Field.Error aria-live='assertive' id={`${reasonId}-error`}>
{t('registration.component.form.requiredField')}
{errors.reason.message}
</Field.Error>
)}
</Field>
Expand Down
129 changes: 81 additions & 48 deletions packages/web-ui-registration/src/ResetPassword/ResetPasswordPage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -21,10 +22,15 @@ const ResetPasswordPage = (): ReactElement => {
const resetPassword = useMethod('resetPassword');
const token = useRouteParameter('token');

const resetPasswordFormRef = useRef<HTMLElement>(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();

Expand All @@ -36,7 +42,7 @@ const ResetPasswordPage = (): ReactElement => {
register,
handleSubmit,
setError,
formState: { errors, isValid },
formState: { errors, isSubmitting },
watch,
} = useForm<{
password: string;
Expand All @@ -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 (
<HorizontalTemplate>
<Form onSubmit={submit}>
<Form
tabIndex={-1}
ref={resetPasswordFormRef}
aria-labelledby={formLabelId}
aria-describedby='welcomeTitle'
onSubmit={handleSubmit(handleResetPassword)}
>
<Form.Header>
<Modal.Title textAlign='start'>{t('Reset_password')}</Modal.Title>
<Form.Title id={formLabelId}>{t('Reset_password')}</Form.Title>
<Form.Subtitle>{t(changePasswordReason)}</Form.Subtitle>
</Form.Header>
<Form.Container>
<Field>
<Field.Label htmlFor='password'>{t(changePasswordReason)}</Field.Label>
<Field.Row>
<PasswordInput
{...register('password', {
required: true,
validate: () => (!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}
/>
</Field.Row>
{errors?.password && (
<Field.Error aria-live='assertive' id={`${passwordId}-error`}>
{errors.password.message}
</Field.Error>
)}
<PasswordVerifier password={password} id={passwordVerifierId} />
{requiresPasswordConfirmation && (
<Field.Row>
<FieldGroup>
<Field>
<FieldLabel required htmlFor={passwordId}>
{t('registration.component.form.password')}
</FieldLabel>
<FieldRow>
<PasswordInput
{...register('passwordConfirmation', {
required: true,
deps: ['password'],
validate: (val: string) => 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`}
/>
</Field.Row>
</FieldRow>
{errors?.password && (
<FieldError aria-live='assertive' id={`${passwordId}-error`}>
{errors.password.message}
</FieldError>
)}
<PasswordVerifier password={password} id={passwordVerifierId} />
</Field>
{requiresPasswordConfirmation && (
<Field>
<FieldLabel required htmlFor={passwordConfirmationId}>
{t('registration.component.form.confirmPassword')}
</FieldLabel>
<FieldRow>
<PasswordInput
{...register('passwordConfirmation', {
required: t('registration.component.form.requiredField'),
deps: ['password'],
validate: (val: string) => (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}
/>
</FieldRow>
{errors.passwordConfirmation && (
<FieldError aria-live='assertive' id={`${passwordConfirmationId}-error`}>
{errors.passwordConfirmation?.message}
</FieldError>
)}
</Field>
)}
{errors && <Field.Error>{errors.password?.message}</Field.Error>}
</Field>
</FieldGroup>
</Form.Container>
<Form.Footer>
<Modal.FooterControllers>
<Button primary disabled={!isValid} type='submit'>
<ButtonGroup>
<Button primary disabled={isSubmitting} type='submit'>
{t('Reset')}
</Button>
</Modal.FooterControllers>
</ButtonGroup>
</Form.Footer>
</Form>
</HorizontalTemplate>
Expand Down

0 comments on commit 4ae2f77

Please sign in to comment.