diff --git a/packages/ui/src/containers/EmailForm/EmailForm.test.tsx b/packages/ui/src/containers/EmailForm/EmailForm.test.tsx new file mode 100644 index 000000000..fa26bc460 --- /dev/null +++ b/packages/ui/src/containers/EmailForm/EmailForm.test.tsx @@ -0,0 +1,172 @@ +import { fireEvent, waitFor, act } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; + +import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; +import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; + +import EmailForm from './EmailForm'; + +const onSubmit = jest.fn(); +const clearErrorMessage = jest.fn(); + +describe('', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('render', () => { + const { queryByText, container } = renderWithPageContext( + + + + ); + expect(container.querySelector('input[name="email"]')).not.toBeNull(); + expect(queryByText('action.continue')).not.toBeNull(); + }); + + test('render with terms settings', () => { + const { queryByText } = renderWithPageContext( + + + + + + ); + expect(queryByText('description.terms_of_use')).not.toBeNull(); + }); + + test('render with terms settings but hasTerms param set to false', () => { + const { queryByText } = renderWithPageContext( + + + + + + ); + expect(queryByText('description.terms_of_use')).toBeNull(); + }); + + test('required email with error message', () => { + const { queryByText, container, getByText } = renderWithPageContext( + + + + ); + const submitButton = getByText('action.continue'); + + fireEvent.click(submitButton); + expect(queryByText('invalid_email')).not.toBeNull(); + expect(onSubmit).not.toBeCalled(); + + const emailInput = container.querySelector('input[name="email"]'); + + if (emailInput) { + fireEvent.change(emailInput, { target: { value: 'foo' } }); + expect(queryByText('invalid_email')).not.toBeNull(); + + fireEvent.change(emailInput, { target: { value: 'foo@logto.io' } }); + expect(queryByText('invalid_email')).toBeNull(); + } + }); + + test('should display and clear the form error message as expected', () => { + const { queryByText, container, getByText } = renderWithPageContext( + + + + ); + + expect(queryByText('form error')).not.toBeNull(); + + const emailInput = container.querySelector('input[name="email"]'); + + if (emailInput) { + fireEvent.change(emailInput, { target: { value: 'foo' } }); + expect(clearErrorMessage).toBeCalled(); + } + }); + + test('should blocked by terms validation with terms settings enabled', async () => { + const { container, getByText } = renderWithPageContext( + + + + + + ); + + const emailInput = container.querySelector('input[name="email"]'); + + if (emailInput) { + fireEvent.change(emailInput, { target: { value: 'foo@logto.io' } }); + } + + const submitButton = getByText('action.continue'); + + act(() => { + fireEvent.click(submitButton); + }); + + await waitFor(() => { + expect(onSubmit).not.toBeCalled(); + }); + }); + + test('should call onSubmit properly with terms settings enabled but hasTerms param set to false', async () => { + const { container, getByText } = renderWithPageContext( + + + + + + ); + + const emailInput = container.querySelector('input[name="email"]'); + + if (emailInput) { + fireEvent.change(emailInput, { target: { value: 'foo@logto.io' } }); + } + + const submitButton = getByText('action.continue'); + + act(() => { + fireEvent.click(submitButton); + }); + + await waitFor(() => { + expect(onSubmit).toBeCalledWith('foo@logto.io'); + }); + }); + + test('should call onSubmit method properly with terms settings enabled and checked', async () => { + const { container, getByText } = renderWithPageContext( + + + + + + ); + const emailInput = container.querySelector('input[name="email"]'); + + if (emailInput) { + fireEvent.change(emailInput, { target: { value: 'foo@logto.io' } }); + } + + const termsButton = getByText('description.agree_with_terms'); + fireEvent.click(termsButton); + + const submitButton = getByText('action.continue'); + + act(() => { + fireEvent.click(submitButton); + }); + + await waitFor(() => { + expect(onSubmit).toBeCalledWith('foo@logto.io'); + }); + }); +}); diff --git a/packages/ui/src/containers/EmailForm/EmailForm.tsx b/packages/ui/src/containers/EmailForm/EmailForm.tsx new file mode 100644 index 000000000..be7b49ad2 --- /dev/null +++ b/packages/ui/src/containers/EmailForm/EmailForm.tsx @@ -0,0 +1,104 @@ +import classNames from 'classnames'; +import { useCallback, useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; + +import Button from '@/components/Button'; +import ErrorMessage from '@/components/ErrorMessage'; +import Input from '@/components/Input'; +import PasswordlessSwitch from '@/containers/PasswordlessSwitch'; +import TermsOfUse from '@/containers/TermsOfUse'; +import useForm from '@/hooks/use-form'; +import useTerms from '@/hooks/use-terms'; +import { emailValidation } from '@/utils/field-validations'; + +import * as styles from './index.module.scss'; + +type Props = { + className?: string; + // eslint-disable-next-line react/boolean-prop-naming + autoFocus?: boolean; + hasTerms?: boolean; + hasSwitch?: boolean; + errorMessage?: string; + clearErrorMessage?: () => void; + onSubmit: (email: string) => Promise; +}; + +type FieldState = { + email: string; +}; + +const defaultState: FieldState = { email: '' }; + +const EmailForm = ({ + autoFocus, + hasTerms = true, + hasSwitch = false, + errorMessage, + clearErrorMessage, + className, + onSubmit, +}: Props) => { + const { t } = useTranslation(); + + const { termsValidation } = useTerms(); + const { fieldValue, setFieldValue, register, validateForm } = useForm(defaultState); + + /* Clear the form error when input field is updated */ + const errorMessageRef = useRef(errorMessage); + + useEffect(() => { + // eslint-disable-next-line @silverhand/fp/no-mutation + errorMessageRef.current = errorMessage; + }, [errorMessage]); + + useEffect(() => { + if (errorMessageRef.current) { + clearErrorMessage?.(); + } + }, [clearErrorMessage, errorMessageRef, fieldValue.email]); + + const onSubmitHandler = useCallback( + async (event?: React.FormEvent) => { + event?.preventDefault(); + + if (!validateForm()) { + return; + } + + if (hasTerms && !(await termsValidation())) { + return; + } + + await onSubmit(fieldValue.email); + }, + [validateForm, hasTerms, termsValidation, onSubmit, fieldValue.email] + ); + + return ( +
+ { + setFieldValue((state) => ({ ...state, email: '' })); + }} + /> + {errorMessage && {errorMessage}} + {hasSwitch && } + {hasTerms && } +