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;
+ }
+}