mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(ui): update set password form (#3020)
This commit is contained in:
parent
4a407ad4de
commit
4a84162722
23 changed files with 336 additions and 73 deletions
|
@ -35,9 +35,9 @@ describe('smoke testing', () => {
|
|||
|
||||
expect(page.url()).toBe(new URL('register/username/password', logtoUrl).href);
|
||||
|
||||
const passwordField = await page.waitForSelector('input[name=new-password]');
|
||||
const confirmPasswordField = await page.waitForSelector('input[name=confirm-new-password]');
|
||||
const saveButton = await page.waitForSelector('button');
|
||||
const passwordField = await page.waitForSelector('input[name=newPassword]');
|
||||
const confirmPasswordField = await page.waitForSelector('input[name=confirmPassword]');
|
||||
const saveButton = await page.waitForSelector('button[name=submit]');
|
||||
await passwordField.type(consolePassword);
|
||||
await confirmPasswordField.type(consolePassword);
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ const translation = {
|
|||
link_another_email: 'Link another email', // UNTRANSLATED
|
||||
link_another_phone: 'Link another phone', // UNTRANSLATED
|
||||
link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED
|
||||
show_password: 'Show password', // UNTRANSLATED
|
||||
},
|
||||
description: {
|
||||
email: 'Email',
|
||||
|
|
|
@ -36,6 +36,7 @@ const translation = {
|
|||
link_another_email: 'Link another email',
|
||||
link_another_phone: 'Link another phone',
|
||||
link_another_email_or_phone: 'Link another email or phone',
|
||||
show_password: 'Show password',
|
||||
},
|
||||
description: {
|
||||
email: 'email',
|
||||
|
|
|
@ -38,6 +38,7 @@ const translation = {
|
|||
link_another_email: 'Link another email', // UNTRANSLATED
|
||||
link_another_phone: 'Link another phone', // UNTRANSLATED
|
||||
link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED
|
||||
show_password: 'Show password', // UNTRANSLATED
|
||||
},
|
||||
description: {
|
||||
email: 'email',
|
||||
|
|
|
@ -38,6 +38,7 @@ const translation = {
|
|||
link_another_email: 'Link another email', // UNTRANSLATED
|
||||
link_another_phone: 'Link another phone', // UNTRANSLATED
|
||||
link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED
|
||||
show_password: 'Show password', // UNTRANSLATED
|
||||
},
|
||||
description: {
|
||||
email: '이메일',
|
||||
|
|
|
@ -38,6 +38,7 @@ const translation = {
|
|||
link_another_email: 'Link another email', // UNTRANSLATED
|
||||
link_another_phone: 'Link another phone', // UNTRANSLATED
|
||||
link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED
|
||||
show_password: 'Show password', // UNTRANSLATED
|
||||
},
|
||||
description: {
|
||||
email: 'e-mail',
|
||||
|
|
|
@ -38,6 +38,7 @@ const translation = {
|
|||
link_another_email: 'Link another email', // UNTRANSLATED
|
||||
link_another_phone: 'Link another phone', // UNTRANSLATED
|
||||
link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED
|
||||
show_password: 'Show password', // UNTRANSLATED
|
||||
},
|
||||
description: {
|
||||
email: 'email',
|
||||
|
|
|
@ -38,6 +38,7 @@ const translation = {
|
|||
link_another_email: 'Link another email', // UNTRANSLATED
|
||||
link_another_phone: 'Link another phone', // UNTRANSLATED
|
||||
link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED
|
||||
show_password: 'Show password', // UNTRANSLATED
|
||||
},
|
||||
description: {
|
||||
email: 'e-posta adresi',
|
||||
|
|
|
@ -38,6 +38,7 @@ const translation = {
|
|||
link_another_email: 'Link another email', // UNTRANSLATED
|
||||
link_another_phone: 'Link another phone', // UNTRANSLATED
|
||||
link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED
|
||||
show_password: 'Show password', // UNTRANSLATED
|
||||
},
|
||||
description: {
|
||||
email: '邮箱',
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
"react": "^18.0.0",
|
||||
"react-device-detect": "^2.2.2",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-hook-form": "^7.34.0",
|
||||
"react-i18next": "^11.18.3",
|
||||
"react-modal": "^3.15.1",
|
||||
"react-router-dom": "^6.2.2",
|
||||
|
|
90
packages/ui/src/components/InputField/index.module.scss
Normal file
90
packages/ui/src/components/InputField/index.module.scss
Normal file
|
@ -0,0 +1,90 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.inputField {
|
||||
position: relative;
|
||||
@include _.flex-row;
|
||||
border: _.border(var(--color-line-border));
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
transition-property: outline, border;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition-duration: 0.2s;
|
||||
|
||||
// fix in safari input field line-height issue
|
||||
height: 44px;
|
||||
|
||||
input {
|
||||
padding: 0 _.unit(4);
|
||||
flex: 1;
|
||||
background: none;
|
||||
caret-color: var(--color-brand-default);
|
||||
font: var(--font-body-1);
|
||||
color: var(--color-type-primary);
|
||||
align-self: stretch;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-type-secondary);
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
padding-right: _.unit(10);
|
||||
}
|
||||
}
|
||||
|
||||
.suffix {
|
||||
position: absolute;
|
||||
right: _.unit(4);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: _.unit(8);
|
||||
height: _.unit(8);
|
||||
display: none;
|
||||
|
||||
&.visible {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border: _.border(var(--color-brand-default));
|
||||
|
||||
input {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.focusVisible {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&.danger {
|
||||
border: _.border(var(--color-danger-default));
|
||||
|
||||
input {
|
||||
caret-color: var(--color-danger-default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
margin-top: _.unit(1);
|
||||
margin-left: _.unit(0.5);
|
||||
}
|
||||
|
||||
:global(body.desktop) {
|
||||
.inputField {
|
||||
outline: 3px solid transparent;
|
||||
|
||||
input {
|
||||
font: var(--font-body-2);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
outline-color: var(--color-overlay-brand-focused);
|
||||
}
|
||||
|
||||
&.danger:focus-within {
|
||||
outline-color: var(--color-overlay-danger-focused);
|
||||
}
|
||||
}
|
||||
}
|
40
packages/ui/src/components/InputField/index.tsx
Normal file
40
packages/ui/src/components/InputField/index.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import classNames from 'classnames';
|
||||
import type { ForwardedRef, HTMLProps, ReactElement } from 'react';
|
||||
import { forwardRef, cloneElement } from 'react';
|
||||
|
||||
import type { ErrorType } from '@/components/ErrorMessage';
|
||||
import ErrorMessage from '@/components/ErrorMessage';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = HTMLProps<HTMLInputElement> & {
|
||||
className?: string;
|
||||
error?: ErrorType;
|
||||
isDanger?: boolean;
|
||||
suffix?: ReactElement;
|
||||
isSuffixFocusVisible?: boolean;
|
||||
isSuffixVisible?: boolean;
|
||||
};
|
||||
|
||||
const InputField = (
|
||||
{ className, error, isDanger, suffix, isSuffixFocusVisible, isSuffixVisible, ...props }: Props,
|
||||
reference: ForwardedRef<HTMLInputElement>
|
||||
) => (
|
||||
<div className={className}>
|
||||
<div className={classNames(styles.inputField, isDanger && styles.danger)}>
|
||||
<input {...props} ref={reference} />
|
||||
{suffix &&
|
||||
cloneElement(suffix, {
|
||||
className: classNames([
|
||||
suffix.props.className,
|
||||
styles.suffix,
|
||||
isSuffixFocusVisible && styles.focusVisible,
|
||||
isSuffixVisible && styles.visible,
|
||||
]),
|
||||
})}
|
||||
</div>
|
||||
{error && <ErrorMessage error={error} className={styles.errorMessage} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default forwardRef(InputField);
|
|
@ -7,7 +7,7 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkBox {
|
||||
.checkbox {
|
||||
margin-right: _.unit(2);
|
||||
fill: var(--color-type-secondary);
|
||||
cursor: pointer;
|
||||
|
|
|
@ -40,7 +40,7 @@ const TermsOfUse = ({ name, className, termsUrl, isChecked, onChange, onTermsCli
|
|||
' ': toggle,
|
||||
})}
|
||||
>
|
||||
<Checkbox name={name} checked={isChecked} className={styles.checkBox} />
|
||||
<Checkbox name={name} checked={isChecked} className={styles.checkbox} />
|
||||
<div className={styles.content}>
|
||||
{prefix}
|
||||
<TextLink
|
||||
|
|
41
packages/ui/src/containers/SetPassword/TogglePassword.tsx
Normal file
41
packages/ui/src/containers/SetPassword/TogglePassword.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Checkbox from '@/components/Checkbox';
|
||||
import { onKeyDownHandler } from '@/utils/a11y';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
isChecked?: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
};
|
||||
|
||||
const TogglePassword = ({ isChecked, onChange }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const toggle = () => {
|
||||
onChange(!isChecked);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="radio"
|
||||
aria-checked={isChecked}
|
||||
tabIndex={0}
|
||||
className={styles.passwordSwitch}
|
||||
onClick={toggle}
|
||||
onKeyDown={onKeyDownHandler({
|
||||
Escape: () => {
|
||||
onChange(false);
|
||||
},
|
||||
Enter: toggle,
|
||||
' ': toggle,
|
||||
})}
|
||||
>
|
||||
<Checkbox name="toggle-password" checked={isChecked} className={styles.checkbox} />
|
||||
<div>{t('action.show_password')}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TogglePassword;
|
|
@ -8,7 +8,8 @@
|
|||
}
|
||||
|
||||
.inputField,
|
||||
.formErrors {
|
||||
.formErrors,
|
||||
.passwordSwitch {
|
||||
margin-bottom: _.unit(4);
|
||||
}
|
||||
|
||||
|
@ -17,3 +18,16 @@
|
|||
margin-top: _.unit(-3);
|
||||
}
|
||||
}
|
||||
|
||||
.passwordSwitch {
|
||||
@include _.flex-row;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
|
||||
.checkbox {
|
||||
margin-right: _.unit(2);
|
||||
fill: var(--color-type-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,28 +14,36 @@ describe('<SetPassword />', () => {
|
|||
const { queryByText, container } = render(
|
||||
<SetPassword errorMessage="error" onSubmit={submit} />
|
||||
);
|
||||
expect(container.querySelector('input[name="new-password"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="confirm-new-password"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="newPassword"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="confirmPassword"]')).not.toBeNull();
|
||||
expect(queryByText('error')).not.toBeNull();
|
||||
expect(queryByText('action.save_password')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('password are required', () => {
|
||||
test('password is required', async () => {
|
||||
const { queryByText, getByText } = render(
|
||||
<SetPassword clearErrorMessage={clearError} onSubmit={submit} />
|
||||
);
|
||||
|
||||
const submitButton = getByText('action.save_password');
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
expect(clearError).toBeCalled();
|
||||
expect(queryByText('password_required')).not.toBeNull();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText('password_required')).not.toBeNull();
|
||||
});
|
||||
|
||||
expect(submit).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('password less than 6 chars should throw', () => {
|
||||
test('password less than 6 chars should throw', async () => {
|
||||
const { queryByText, getByText, container } = render(<SetPassword onSubmit={submit} />);
|
||||
const submitButton = getByText('action.save_password');
|
||||
const passwordInput = container.querySelector('input[name="new-password"]');
|
||||
const passwordInput = container.querySelector('input[name="newPassword"]');
|
||||
|
||||
if (passwordInput) {
|
||||
fireEvent.change(passwordInput, { target: { value: '12345' } });
|
||||
|
@ -45,7 +53,9 @@ describe('<SetPassword />', () => {
|
|||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
expect(queryByText('password_min_length')).not.toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(queryByText('password_min_length')).not.toBeNull();
|
||||
});
|
||||
|
||||
expect(submit).not.toBeCalled();
|
||||
|
||||
|
@ -56,14 +66,16 @@ describe('<SetPassword />', () => {
|
|||
}
|
||||
});
|
||||
|
||||
expect(queryByText('password_min_length')).toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(queryByText('password_min_length')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test('password mismatch with confirmPassword should throw', () => {
|
||||
test('password mismatch with confirmPassword should throw', async () => {
|
||||
const { queryByText, getByText, container } = render(<SetPassword onSubmit={submit} />);
|
||||
const submitButton = getByText('action.save_password');
|
||||
const passwordInput = container.querySelector('input[name="new-password"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]');
|
||||
const passwordInput = container.querySelector('input[name="newPassword"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirmPassword"]');
|
||||
|
||||
act(() => {
|
||||
if (passwordInput) {
|
||||
|
@ -77,7 +89,9 @@ describe('<SetPassword />', () => {
|
|||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
expect(queryByText('passwords_do_not_match')).not.toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(queryByText('passwords_do_not_match')).not.toBeNull();
|
||||
});
|
||||
|
||||
expect(submit).not.toBeCalled();
|
||||
|
||||
|
@ -88,14 +102,16 @@ describe('<SetPassword />', () => {
|
|||
}
|
||||
});
|
||||
|
||||
expect(queryByText('passwords_do_not_match')).toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(queryByText('passwords_do_not_match')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test('should submit properly', async () => {
|
||||
const { queryByText, getByText, container } = render(<SetPassword onSubmit={submit} />);
|
||||
const submitButton = getByText('action.save_password');
|
||||
const passwordInput = container.querySelector('input[name="new-password"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]');
|
||||
const passwordInput = container.querySelector('input[name="newPassword"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirmPassword"]');
|
||||
|
||||
act(() => {
|
||||
if (passwordInput) {
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import classNames from 'classnames';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ClearIcon from '@/assets/icons/clear-icon.svg';
|
||||
import Button from '@/components/Button';
|
||||
import IconButton from '@/components/Button/IconButton';
|
||||
import ErrorMessage from '@/components/ErrorMessage';
|
||||
import Input from '@/components/Input';
|
||||
import useForm from '@/hooks/use-form';
|
||||
import { passwordValidation, confirmPasswordValidation } from '@/utils/field-validations';
|
||||
import InputField from '@/components/InputField';
|
||||
import { passwordErrorWatcher } from '@/utils/form';
|
||||
|
||||
import TogglePassword from './TogglePassword';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
|
@ -20,15 +23,10 @@ type Props = {
|
|||
};
|
||||
|
||||
type FieldState = {
|
||||
password: string;
|
||||
newPassword: string;
|
||||
confirmPassword: string;
|
||||
};
|
||||
|
||||
const defaultState: FieldState = {
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
};
|
||||
|
||||
const SetPassword = ({
|
||||
className,
|
||||
autoFocus,
|
||||
|
@ -37,53 +35,93 @@ const SetPassword = ({
|
|||
clearErrorMessage,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { fieldValue, setFieldValue, register, validateForm } = useForm(defaultState);
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
watch,
|
||||
resetField,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FieldState>({
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: { newPassword: '', confirmPassword: '' },
|
||||
});
|
||||
|
||||
const onSubmitHandler = useCallback(
|
||||
async (event?: React.FormEvent<HTMLFormElement>) => {
|
||||
event?.preventDefault();
|
||||
|
||||
(event?: React.FormEvent<HTMLFormElement>) => {
|
||||
clearErrorMessage?.();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit(fieldValue.password);
|
||||
void handleSubmit((data, event) => {
|
||||
onSubmit(data.newPassword);
|
||||
event?.preventDefault();
|
||||
})(event);
|
||||
},
|
||||
[clearErrorMessage, validateForm, onSubmit, fieldValue.password]
|
||||
[clearErrorMessage, handleSubmit, onSubmit]
|
||||
);
|
||||
|
||||
const newPasswordError = passwordErrorWatcher(errors.newPassword);
|
||||
|
||||
return (
|
||||
<form className={classNames(styles.form, className)} onSubmit={onSubmitHandler}>
|
||||
<Input
|
||||
<InputField
|
||||
required
|
||||
className={styles.inputField}
|
||||
name="new-password"
|
||||
type="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="new-password"
|
||||
placeholder={t('input.password')}
|
||||
autoFocus={autoFocus}
|
||||
{...register('password', passwordValidation)}
|
||||
onClear={() => {
|
||||
setFieldValue((state) => ({ ...state, password: '' }));
|
||||
}}
|
||||
isDanger={!!newPasswordError}
|
||||
error={newPasswordError}
|
||||
aria-invalid={!!newPasswordError}
|
||||
{...register('newPassword', { required: true, minLength: 6 })}
|
||||
isSuffixFocusVisible={!!watch('newPassword')}
|
||||
suffix={
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
resetField('newPassword');
|
||||
}}
|
||||
>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
|
||||
<InputField
|
||||
required
|
||||
className={styles.inputField}
|
||||
name="confirm-new-password"
|
||||
type="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="new-password"
|
||||
placeholder={t('input.confirm_password')}
|
||||
{...register('confirmPassword', (confirmPassword) =>
|
||||
confirmPasswordValidation(fieldValue.password, confirmPassword)
|
||||
)}
|
||||
isErrorStyling={false}
|
||||
onClear={() => {
|
||||
setFieldValue((state) => ({ ...state, confirmPassword: '' }));
|
||||
}}
|
||||
error={errors.confirmPassword && 'passwords_do_not_match'}
|
||||
aria-invalid={!!errors.confirmPassword}
|
||||
{...register('confirmPassword', {
|
||||
validate: (value) => value === watch('newPassword'),
|
||||
})}
|
||||
isSuffixFocusVisible={!!watch('confirmPassword')}
|
||||
suffix={
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
resetField('confirmPassword');
|
||||
}}
|
||||
>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
|
||||
{errorMessage && <ErrorMessage className={styles.formErrors}>{errorMessage}</ErrorMessage>}
|
||||
|
||||
<Button title="action.save_password" onClick={async () => onSubmitHandler()} />
|
||||
<TogglePassword isChecked={showPassword} onChange={setShowPassword} />
|
||||
|
||||
<Button
|
||||
name="submit"
|
||||
title="action.save_password"
|
||||
onClick={async () => {
|
||||
onSubmitHandler();
|
||||
}}
|
||||
/>
|
||||
|
||||
<input hidden type="submit" />
|
||||
</form>
|
||||
|
|
|
@ -24,8 +24,8 @@ describe('SetPassword', () => {
|
|||
<SetPassword />
|
||||
</SettingsProvider>
|
||||
);
|
||||
expect(container.querySelector('input[name="new-password"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="confirm-new-password"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="newPassword"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="confirmPassword"]')).not.toBeNull();
|
||||
expect(queryByText('action.save_password')).not.toBeNull();
|
||||
});
|
||||
|
||||
|
@ -36,8 +36,8 @@ describe('SetPassword', () => {
|
|||
</SettingsProvider>
|
||||
);
|
||||
const submitButton = getByText('action.save_password');
|
||||
const passwordInput = container.querySelector('input[name="new-password"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]');
|
||||
const passwordInput = container.querySelector('input[name="newPassword"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirmPassword"]');
|
||||
|
||||
act(() => {
|
||||
if (passwordInput) {
|
||||
|
|
|
@ -36,8 +36,8 @@ describe('<PasswordRegisterWithUsername />', () => {
|
|||
</SettingsProvider>
|
||||
);
|
||||
|
||||
expect(container.querySelector('input[name="new-password"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="confirm-new-password"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="newPassword"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="confirmPassword"]')).not.toBeNull();
|
||||
expect(queryByText('action.save_password')).not.toBeNull();
|
||||
});
|
||||
|
||||
|
@ -56,7 +56,7 @@ describe('<PasswordRegisterWithUsername />', () => {
|
|||
</SettingsProvider>
|
||||
);
|
||||
|
||||
expect(container.querySelector('input[name="new-password"]')).toBeNull();
|
||||
expect(container.querySelector('input[name="newPassword"]')).toBeNull();
|
||||
expect(queryByText('description.not_found')).not.toBeNull();
|
||||
});
|
||||
|
||||
|
@ -68,8 +68,8 @@ describe('<PasswordRegisterWithUsername />', () => {
|
|||
);
|
||||
|
||||
const submitButton = getByText('action.save_password');
|
||||
const passwordInput = container.querySelector('input[name="new-password"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]');
|
||||
const passwordInput = container.querySelector('input[name="newPassword"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirmPassword"]');
|
||||
|
||||
act(() => {
|
||||
if (passwordInput) {
|
||||
|
|
|
@ -27,16 +27,16 @@ describe('ForgotPassword', () => {
|
|||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(container.querySelector('input[name="new-password"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="confirm-new-password"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="newPassword"]')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="confirmPassword"]')).not.toBeNull();
|
||||
expect(queryByText('action.save_password')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('should submit properly', async () => {
|
||||
const { getByText, container } = renderWithPageContext(<ResetPassword />);
|
||||
const submitButton = getByText('action.save_password');
|
||||
const passwordInput = container.querySelector('input[name="new-password"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]');
|
||||
const passwordInput = container.querySelector('input[name="newPassword"]');
|
||||
const confirmPasswordInput = container.querySelector('input[name="confirmPassword"]');
|
||||
|
||||
act(() => {
|
||||
if (passwordInput) {
|
||||
|
|
13
packages/ui/src/utils/form.ts
Normal file
13
packages/ui/src/utils/form.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import type { FieldError } from 'react-hook-form';
|
||||
|
||||
import type { ErrorType } from '@/components/ErrorMessage';
|
||||
|
||||
export const passwordErrorWatcher = (error?: FieldError): ErrorType | undefined => {
|
||||
switch (error?.type) {
|
||||
case 'required':
|
||||
return 'password_required';
|
||||
case 'minLength':
|
||||
return { code: 'password_min_length', data: { min: 6 } };
|
||||
default:
|
||||
}
|
||||
};
|
|
@ -813,6 +813,7 @@ importers:
|
|||
react: ^18.0.0
|
||||
react-device-detect: ^2.2.2
|
||||
react-dom: ^18.0.0
|
||||
react-hook-form: ^7.34.0
|
||||
react-i18next: ^11.18.3
|
||||
react-modal: ^3.15.1
|
||||
react-router-dom: ^6.2.2
|
||||
|
@ -868,6 +869,7 @@ importers:
|
|||
react: 18.2.0
|
||||
react-device-detect: 2.2.2_biqbaboplfbrettd7655fr4n2y
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
react-hook-form: 7.34.0_react@18.2.0
|
||||
react-i18next: 11.18.3_shxxmfhtk2bc4pbx5cyq3uoph4
|
||||
react-modal: 3.15.1_biqbaboplfbrettd7655fr4n2y
|
||||
react-router-dom: 6.2.2_biqbaboplfbrettd7655fr4n2y
|
||||
|
|
Loading…
Reference in a new issue