diff --git a/src/app/common/components/ContactForm/ContactForm.component.spec.tsx b/src/app/common/components/ContactForm/ContactForm.component.spec.tsx new file mode 100644 index 000000000..916ca721b --- /dev/null +++ b/src/app/common/components/ContactForm/ContactForm.component.spec.tsx @@ -0,0 +1,215 @@ +import { + cleanup, fireEvent, render, screen, waitFor, +} from '@testing-library/react'; +import user from '@testing-library/user-event'; + +import EmailApi from '@/app/api/email/email.api'; + +import '@testing-library/jest-dom'; + +import ContactForm from './ContactForm.component'; + +// needed to render component without errors +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: (query: unknown) => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => {}, + }), +}); + +// mock ReCAPTCHA component +const onCaptchaMock = jest.fn(); +jest.mock('react-google-recaptcha', () => jest.fn(({ onChange }) => ( +
+ +
+))); + +// mock backend api calls +jest.mock('@/app/api/email/email.api', () => ({ + send: jest.fn(() => {}), +})); + +describe('ContactForm test', () => { + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + it('should be rendered', async () => { + render( + , + ); + + const textareaMessage = screen.getByPlaceholderText(/Наші серця/i); + const inputEmail = screen.getByPlaceholderText(/E-mail/i); + const buttonSend = screen.getByText(/Відправити/i); + + await waitFor(() => { + expect(textareaMessage).toBeInTheDocument(); + expect(inputEmail).toBeInTheDocument(); + expect(buttonSend).toBeInTheDocument(); + }); + }); + + it('should send Email with filled fields', async () => { + render( + , + ); + + // Arrange + const textareaMessage = screen.getByPlaceholderText(/Наші серця/i) as HTMLTextAreaElement; + const inputEmail = screen.getByPlaceholderText(/E-mail/i) as HTMLInputElement; + const captchaButton = screen.getByText(/Verify ReCAPTCHA/i); + const sendButton = screen.getByText(/Відправити/i); + + const message = 'Some interesting message'; + const email = 'test@mail.com'; + + // Act + await waitFor(async () => { + user.type(inputEmail, email); + user.type(textareaMessage, message); + user.click(captchaButton); + user.click(sendButton); + }); + + // Assert + expect(textareaMessage.value).toBe(message); + expect(inputEmail.value).toBe(email); + expect(onCaptchaMock).toHaveBeenCalled(); + expect(EmailApi.send).toHaveBeenCalled(); + }); + + it('should not send Email with invalid email field', async () => { + render( + , + ); + + // Arrange + const textareaMessage = screen.getByPlaceholderText(/Наші серця/i) as HTMLTextAreaElement; + const inputEmail = screen.getByPlaceholderText(/E-mail/i) as HTMLInputElement; + const captchaButton = screen.getByText(/Verify ReCAPTCHA/i); + const sendButton = screen.getByText(/Відправити/i); + + const message = 'Some interesting message'; + const email = 'invalid email'; + + // Act + await waitFor(async () => { + user.type(inputEmail, email); + user.type(textareaMessage, message); + user.click(captchaButton); + user.click(sendButton); + }); + + // Assert + expect(textareaMessage.value).toBe(message); + expect(inputEmail.value).toBe(email); + expect(onCaptchaMock).toHaveBeenCalled(); + expect(EmailApi.send).not.toHaveBeenCalled(); + }); + + it('should not send Email with epty message field', async () => { + render( + , + ); + + // Arrange + const textareaMessage = screen.getByPlaceholderText(/Наші серця/i) as HTMLTextAreaElement; + const inputEmail = screen.getByPlaceholderText(/E-mail/i) as HTMLInputElement; + const captchaButton = screen.getByText(/Verify ReCAPTCHA/i); + const sendButton = screen.getByText(/Відправити/i); + + const email = 'valid@email.com'; + + // Act + await waitFor(async () => { + user.type(inputEmail, email); + user.click(captchaButton); + user.click(sendButton); + }); + + // Assert + expect(textareaMessage.value).toBe(''); + expect(inputEmail.value).toBe(email); + expect(onCaptchaMock).toHaveBeenCalled(); + expect(EmailApi.send).not.toHaveBeenCalled(); + }); + + it('should not send Email without ReCAPTCHA', async () => { + render( + , + ); + + // Arrange + const textareaMessage = screen.getByPlaceholderText(/Наші серця/i) as HTMLTextAreaElement; + const inputEmail = screen.getByPlaceholderText(/E-mail/i) as HTMLInputElement; + const sendButton = screen.getByText(/Відправити/i); + + const message = 'Some interesting message'; + const email = 'valid@email.com'; + + // Act + await waitFor(async () => { + user.type(inputEmail, email); + user.type(textareaMessage, message); + user.click(sendButton); + }); + + // Assert + expect(inputEmail.value).toBe(email); + expect(textareaMessage.value).toBe(message); + expect(onCaptchaMock).not.toHaveBeenCalled(); + expect(EmailApi.send).not.toHaveBeenCalled(); + }); + + it('should check text amount restrictions and Email validation', async () => { + render( + , + ); + + // Arrange + const textareaMessage = screen.getByPlaceholderText(/Наші серця/i) as HTMLTextAreaElement; + const inputEmail = screen.getByPlaceholderText(/E-mail/i); + const buttonSend = screen.getByText(/Відправити/i); + + const descriptionRestriction = 500; + const invalidEmail = 'invalid@email.c'; + const text = 'String which excides text amount limit'; + const veryLongText = text.repeat(13); + + // Act + await waitFor(async () => { + user.type(inputEmail, invalidEmail); + + // user.type() takes too much time to input all the text, so fireEvent.change() partially + // fills description and user.type() tries to exceed text amount restrictions\ + fireEvent.change(textareaMessage, { target: { value: veryLongText } }); + user.type(textareaMessage, text); + + user.click(buttonSend); + }); + + // Assert + const validationMessage = await screen.findByText(/E-mail може містити/i); + expect(validationMessage).toBeInTheDocument(); + expect(textareaMessage.value.length).toBe(descriptionRestriction); + expect(EmailApi.send).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/common/components/ContactForm/ContactForm.component.tsx b/src/app/common/components/ContactForm/ContactForm.component.tsx index 51d969107..89c5b416a 100644 --- a/src/app/common/components/ContactForm/ContactForm.component.tsx +++ b/src/app/common/components/ContactForm/ContactForm.component.tsx @@ -1,12 +1,13 @@ import './ContactForm.styles.scss'; -import { LegacyRef, forwardRef, useImperativeHandle, useRef, useState } from 'react'; +import { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import ReCAPTCHA from 'react-google-recaptcha'; import { Button, Form, Input, message } from 'antd'; import EmailApi from '@/app/api/email/email.api'; import Email from '@/models/email/email.model'; + import { ERROR_MESSAGES } from '../../constants/error-messages.constants'; const MAX_SYMBOLS = 500; @@ -40,6 +41,20 @@ const ContactForm = forwardRef((customClass: Props, ref) => { }, })); + const successMessage = () => { + messageApi.open({ + type: 'success', + content: 'Лист успішно надісланий', + }); + }; + + const errorMessage = (error: string) => { + messageApi.open({ + type: 'error', + content: error, + }); + }; + const onFinish = () => { if (isVerified) { const token = recaptchaRef?.current?.getValue(); @@ -56,33 +71,17 @@ const ContactForm = forwardRef((customClass: Props, ref) => { .catch((error) => { if (error === 429) { errorMessage(MESSAGE_LIMIT); - } - else { + } else { errorMessage(SOMETHING_IS_WRONG); } }); recaptchaRef.current?.reset(); setIsVerified(false); - } - else { + } else { errorMessage(RECAPTCHA_CHECK); } }; - const successMessage = () => { - messageApi.open({ - type: 'success', - content: 'Лист успішно надісланий', - }); - }; - - const errorMessage = (message: string) => { - messageApi.open({ - type: 'error', - content: message, - }); - }; - return (
{messageContextHolder} @@ -114,6 +113,7 @@ const ContactForm = forwardRef((customClass: Props, ref) => { name="message" autoSize={{ minRows: 4, maxRows: 4 }} placeholder="Наші серця, очі та вуха відкриті до твоїх креативних повідомлень!" + showCount maxLength={MAX_SYMBOLS} onChange={handleChange} /> @@ -140,7 +140,7 @@ const ContactForm = forwardRef((customClass: Props, ref) => {
textarea.ant-input { + border: 1px solid #D3CDCA; + } } .formWrapper { @@ -49,14 +53,6 @@ .contactForm { @include mut.flexed($direction: column, $gap: 24px); - .custom-character-counter { - margin-bottom: -12px; - text-align: right; - font-family: "Roboto", "Helvetica Neue", sans-serif !important; - font-size: 16px; - color: #00000073; - } - textarea { resize: none; outline: none; @@ -80,15 +76,12 @@ overflow: auto !important; } + .ant-input-affix-wrapper > textarea.ant-input { + @include mut.with-font($font-family: ft.$roboto-font, $font-weight: 300, $font-size: 20px); + } + .textareaBlock { position: relative; - - .amountSymbols { - position: absolute; - bottom: f.pxToRem(10px); - right: f.pxToRem(10px); - color: #C4C4C4; - } } .ant-input:hover, @@ -115,9 +108,6 @@ background-color: #E04031; } - .ant-form-item .ant-form-item-explain-error { - } - button { @include mut.with-font($font-family: ft.$roboto-font, $font-weight: 500, $font-size: 20px); line-height: 23px; @@ -136,6 +126,10 @@ } } + .ant-input-show-count { + z-index: 0 !important; + } + .required-input { position: relative; } @@ -197,8 +191,6 @@ } .contactForm { - @include mut.flexed($direction: column, $gap: 16px); - button { width: 100%; padding: f.pxToRem(15px) 0; @@ -210,12 +202,8 @@ line-height: 21px; } - .ant-input-textarea-show-count::after { - top: f.pxToRem(110px); - } - - .ant-input-textarea-show-count .ant-input-data-count { - margin-bottom: f.pxToRem(-18px); + .ant-input-affix-wrapper > textarea.ant-input { + @include mut.with-font($font-family: ft.$roboto-font, $font-weight: 300, $font-size: 16px); } } } @@ -225,4 +213,8 @@ .formWrapper .formTitleContainer .formTitle { text-align: center; } -} \ No newline at end of file + + .ant-form-item-explain-error { + padding: 0 45px; + } +}