Skip to content

Commit

Permalink
(fix) Tweak change password form validation (#1069)
Browse files Browse the repository at this point in the history
  • Loading branch information
denniskigen authored Jul 3, 2024
1 parent d083978 commit 561bad4
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { Controller, FormProvider, useForm, type SubmitHandler } from 'react-hook-form';
import { Controller, useForm, type SubmitHandler } from 'react-hook-form';
import { Button, Form, PasswordInput, InlineLoading, ModalBody, ModalFooter, ModalHeader, Stack } from '@carbon/react';
import { showSnackbar } from '@openmrs/esm-framework';
import { changeUserPassword } from './change-password.resource';
Expand Down Expand Up @@ -39,8 +39,11 @@ const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({ close }) => {
path: ['passwordConfirmation'],
});

const methods = useForm<z.infer<typeof changePasswordFormSchema>>({
mode: 'all',
const {
handleSubmit,
control,
formState: { errors },
} = useForm({
resolver: zodResolver(changePasswordFormSchema),
});

Expand Down Expand Up @@ -73,69 +76,67 @@ const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({ close }) => {
const onError = () => setIsChangingPassword(false);

return (
<FormProvider {...methods}>
<Form onSubmit={methods.handleSubmit(onSubmit, onError)}>
<ModalHeader closeModal={close} title={t('changePassword', 'Change password')} />
<ModalBody>
<Stack gap={5} className={styles.languageOptionsContainer}>
<Controller
name="oldPassword"
control={methods.control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<PasswordInput
id="oldPassword"
invalid={!!error}
invalidText={error?.message}
labelText={t('oldPassword', 'Old password')}
onChange={onChange}
value={value}
/>
)}
/>
<Controller
name="newPassword"
control={methods.control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<PasswordInput
id="newPassword"
invalid={!!error}
invalidText={error?.message}
labelText={t('newPassword', 'New password')}
onChange={onChange}
value={value}
/>
)}
/>
<Controller
name="passwordConfirmation"
control={methods.control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<PasswordInput
id="passwordConfirmation"
invalid={!!error}
invalidText={error?.message}
labelText={t('confirmPassword', 'Confirm new password')}
onChange={onChange}
value={value}
/>
)}
/>
</Stack>
</ModalBody>
<ModalFooter>
<Button kind="secondary" onClick={close}>
{t('cancel', 'Cancel')}
</Button>
<Button className={styles.submitButton} disabled={isChangingPassword} type="submit">
{isChangingPassword ? (
<InlineLoading description={t('changingLanguage', 'Changing password') + '...'} />
) : (
<span>{t('change', 'Change')}</span>
<Form onSubmit={handleSubmit(onSubmit, onError)}>
<ModalHeader closeModal={close} title={t('changePassword', 'Change password')} />
<ModalBody>
<Stack gap={5} className={styles.languageOptionsContainer}>
<Controller
name="oldPassword"
control={control}
render={({ field: { onChange, value } }) => (
<PasswordInput
id="oldPassword"
invalid={!!errors?.oldPassword}
invalidText={errors?.oldPassword?.message}
labelText={t('oldPassword', 'Old password')}
onChange={onChange}
value={value}
/>
)}
</Button>
</ModalFooter>
</Form>
</FormProvider>
/>
<Controller
name="newPassword"
control={control}
render={({ field: { onChange, value } }) => (
<PasswordInput
id="newPassword"
invalid={!!errors?.newPassword}
invalidText={errors?.newPassword?.message}
labelText={t('newPassword', 'New password')}
onChange={onChange}
value={value}
/>
)}
/>
<Controller
name="passwordConfirmation"
control={control}
render={({ field: { onChange, value } }) => (
<PasswordInput
id="passwordConfirmation"
invalid={!!errors?.passwordConfirmation}
invalidText={errors?.passwordConfirmation?.message}
labelText={t('confirmPassword', 'Confirm new password')}
onChange={onChange}
value={value}
/>
)}
/>
</Stack>
</ModalBody>
<ModalFooter>
<Button kind="secondary" onClick={close}>
{t('cancel', 'Cancel')}
</Button>
<Button className={styles.submitButton} disabled={isChangingPassword} type="submit">
{isChangingPassword ? (
<InlineLoading description={t('changingLanguage', 'Changing password') + '...'} />
) : (
<span>{t('change', 'Change')}</span>
)}
</Button>
</ModalFooter>
</Form>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import ChangePasswordModal from './change-password.modal';
import { changeUserPassword } from './change-password.resource';

jest.mock('./change-password.resource', () => ({
changeUserPassword: jest.fn().mockResolvedValue({}),
}));

const mockClose = jest.fn();
const mockChangeUserPassword = changeUserPassword as jest.Mock;

describe('ChangePasswordModal', () => {
beforeEach(() => {
render(<ChangePasswordModal close={mockClose} />);
});

it('validates the form before submitting', async () => {
const submitButton = screen.getByRole('button', {
name: /change/i,
});
const oldPasswordInput = screen.getByLabelText(/old password/i);
const newPasswordInput = screen.getByLabelText(/^new password$/i);
const confirmPasswordInput = screen.getByLabelText(/confirm new password/i);

expect(screen.getByRole('heading', { name: /Change password/i })).toBeInTheDocument();
expect(oldPasswordInput).toBeInTheDocument();
expect(newPasswordInput).toBeInTheDocument();
expect(confirmPasswordInput).toBeInTheDocument();

await userEvent.click(submitButton);

expect(screen.getByText(/old password is required/i)).toBeInTheDocument();
expect(screen.getByText(/new password is required/i)).toBeInTheDocument();
expect(screen.getByText(/password confirmation is required/i)).toBeInTheDocument();

await userEvent.type(oldPasswordInput, 'P@ssw0rd123!');
await userEvent.type(newPasswordInput, 'N3wP@ssw0rd456!');
await userEvent.type(confirmPasswordInput, 'N3wP@ssw0rd456');

expect(screen.getByText(/passwords do not match/i)).toBeInTheDocument();

await userEvent.clear(confirmPasswordInput);
await userEvent.type(confirmPasswordInput, 'N3wP@ssw0rd456!');

await userEvent.click(submitButton);

expect(mockChangeUserPassword).toHaveBeenCalledTimes(1);
expect(mockChangeUserPassword).toHaveBeenCalledWith('P@ssw0rd123!', 'N3wP@ssw0rd456!');
});
});

0 comments on commit 561bad4

Please sign in to comment.