diff --git a/.github/workflows/cypress-testing.yml b/.github/workflows/cypress-testing.yml index 5d0b90ed0..4bc0fc99b 100644 --- a/.github/workflows/cypress-testing.yml +++ b/.github/workflows/cypress-testing.yml @@ -4,7 +4,7 @@ on: push: branches: [master] pull_request: - branches: [master, test_cases_wa_group] + branches: [master] jobs: glific: @@ -96,7 +96,7 @@ jobs: git clone https://github.com/glific/cypress-testing.git echo done. go to dir. cd cypress-testing - git checkout main + git checkout hsm-templates cd .. cp -r cypress-testing/cypress cypress yarn add cypress@13.6.2 diff --git a/src/App.test.tsx b/src/App.test.tsx index dd82dfb05..fc08eae6b 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,4 +1,3 @@ -import 'mocks/matchMediaMock'; import { MemoryRouter } from 'react-router-dom'; import { MockedProvider } from '@apollo/client/testing'; import { waitFor, render, screen } from '@testing-library/react'; diff --git a/src/assets/images/AddGreenIcon.svg b/src/assets/images/AddGreenIcon.svg new file mode 100644 index 000000000..31ff0fb53 --- /dev/null +++ b/src/assets/images/AddGreenIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/icons/CrossIcon.svg b/src/assets/images/icons/CrossIcon.svg new file mode 100644 index 000000000..06b61d6d2 --- /dev/null +++ b/src/assets/images/icons/CrossIcon.svg @@ -0,0 +1,3 @@ + diff --git a/src/common/RichEditor.tsx b/src/common/RichEditor.tsx index dfd7c1ed0..dae00eb01 100644 --- a/src/common/RichEditor.tsx +++ b/src/common/RichEditor.tsx @@ -40,6 +40,7 @@ export const setDefaultValue = (editor: any, initialValue: any) => { const paragraph = $createParagraphNode(); paragraph.append($createTextNode(initialValue || '')); root.append(paragraph); + paragraph.selectEnd(); }); }; diff --git a/src/components/UI/Form/AutoComplete/AutoComplete.tsx b/src/components/UI/Form/AutoComplete/AutoComplete.tsx index abf98f200..4ed28ec89 100644 --- a/src/components/UI/Form/AutoComplete/AutoComplete.tsx +++ b/src/components/UI/Form/AutoComplete/AutoComplete.tsx @@ -117,7 +117,7 @@ export const AutoComplete = ({ })(); const getLabel = (option: any) => { - if (option[optionLabel]) { + if (option[optionLabel] || option[optionLabel] === '') { return option[optionLabel]; } if (additionalOptionLabel) { diff --git a/src/components/UI/Form/Calendar/Calendar.test.tsx b/src/components/UI/Form/Calendar/Calendar.test.tsx index ea621ce4c..a2858d5fb 100644 --- a/src/components/UI/Form/Calendar/Calendar.test.tsx +++ b/src/components/UI/Form/Calendar/Calendar.test.tsx @@ -3,6 +3,10 @@ import { backspace } from 'common/test-utils'; import { Calendar } from './Calendar'; import dayjs from 'dayjs'; +afterEach(() => { + (window as any).matchMedia = null; +}); + const setFieldValueMock = vi.fn(); describe('', () => { const getProps = () => ({ diff --git a/src/components/UI/Form/DateTimePicker/DateTimePicker.test.tsx b/src/components/UI/Form/DateTimePicker/DateTimePicker.test.tsx index 81a778d5f..f3591ef5f 100644 --- a/src/components/UI/Form/DateTimePicker/DateTimePicker.test.tsx +++ b/src/components/UI/Form/DateTimePicker/DateTimePicker.test.tsx @@ -4,6 +4,10 @@ import { DateTimePicker } from './DateTimePicker'; import dayjs from 'dayjs'; import { userEvent } from '@testing-library/user-event'; +afterEach(() => { + (window as any).matchMedia = null; +}); + describe('', () => { const onChangeMock = vi.fn(); const setFieldMock = vi.fn(); diff --git a/src/components/UI/Form/EmojiInput/Editor.test.tsx b/src/components/UI/Form/EmojiInput/Editor.test.tsx index 727f0befa..832088b12 100644 --- a/src/components/UI/Form/EmojiInput/Editor.test.tsx +++ b/src/components/UI/Form/EmojiInput/Editor.test.tsx @@ -1,4 +1,3 @@ -import 'mocks/matchMediaMock'; import { render } from '@testing-library/react'; import { Editor } from './Editor'; import { LexicalWrapper } from 'common/LexicalWrapper'; @@ -10,7 +9,7 @@ const mockIntersectionObserver = class { disconnect() {} }; -const handleChange = vi.fn; +const handleChange = vi.fn(); (window as any).IntersectionObserver = mockIntersectionObserver; diff --git a/src/components/UI/Form/EmojiInput/EmojiInput.test.tsx b/src/components/UI/Form/EmojiInput/EmojiInput.test.tsx index 70fdba170..c17bb9b1f 100644 --- a/src/components/UI/Form/EmojiInput/EmojiInput.test.tsx +++ b/src/components/UI/Form/EmojiInput/EmojiInput.test.tsx @@ -1,8 +1,8 @@ -import 'mocks/matchMediaMock'; import { render, screen } from '@testing-library/react'; import { EmojiInput } from './EmojiInput'; import userEvent from '@testing-library/user-event'; +import { LexicalWrapper } from 'common/LexicalWrapper'; const setFieldValueMock = vi.fn(); @@ -16,18 +16,20 @@ const mockIntersectionObserver = class { (window as any).IntersectionObserver = mockIntersectionObserver; const wrapper = ( - + + + ); vi.mock('components/UI/EmojiPicker/EmojiPicker', async (importOriginal) => { diff --git a/src/components/UI/Form/EmojiInput/EmojiInput.tsx b/src/components/UI/Form/EmojiInput/EmojiInput.tsx index d983497eb..e29870866 100644 --- a/src/components/UI/Form/EmojiInput/EmojiInput.tsx +++ b/src/components/UI/Form/EmojiInput/EmojiInput.tsx @@ -6,7 +6,6 @@ import { Editor } from './Editor'; import Styles from './EmojiInput.module.css'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { $createTextNode, $getSelection, $isRangeSelection } from 'lexical'; -import { LexicalWrapper } from 'common/LexicalWrapper'; export interface EmojiInputProps { field: { name: string; onChange?: any; value: any; onBlur?: any }; @@ -32,6 +31,7 @@ export const EmojiInput = ({ handleChange, handleBlur, translation, + form, ...props }: EmojiInputProps) => { const [showEmojiPicker, setShowEmojiPicker] = useState(false); @@ -49,9 +49,13 @@ export const EmojiInput = ({ ); const input = ( - - - + ); return ( diff --git a/src/components/UI/Form/TimePicker/TimePicker.test.tsx b/src/components/UI/Form/TimePicker/TimePicker.test.tsx index f53585062..6e1290644 100644 --- a/src/components/UI/Form/TimePicker/TimePicker.test.tsx +++ b/src/components/UI/Form/TimePicker/TimePicker.test.tsx @@ -3,6 +3,10 @@ import UserEvent from '@testing-library/user-event'; import { TimePicker } from './TimePicker'; +afterEach(() => { + (window as any).matchMedia = null; +}); + const setFieldValueMock = vi.fn(); const timePickerProps: any = (disabled: boolean) => { return { diff --git a/src/components/UI/Form/WhatsAppEditor/WhatsAppEditor.test.tsx b/src/components/UI/Form/WhatsAppEditor/WhatsAppEditor.test.tsx index 5b059697b..95f87f411 100644 --- a/src/components/UI/Form/WhatsAppEditor/WhatsAppEditor.test.tsx +++ b/src/components/UI/Form/WhatsAppEditor/WhatsAppEditor.test.tsx @@ -1,4 +1,3 @@ -import 'mocks/matchMediaMock'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { vi } from 'vitest'; diff --git a/src/components/UI/MessageDialog/MessageDialog.test.tsx b/src/components/UI/MessageDialog/MessageDialog.test.tsx index b600f9c78..84862975e 100644 --- a/src/components/UI/MessageDialog/MessageDialog.test.tsx +++ b/src/components/UI/MessageDialog/MessageDialog.test.tsx @@ -1,4 +1,3 @@ -import 'mocks/matchMediaMock'; import { fireEvent, render } from '@testing-library/react'; import { MockedProvider } from '@apollo/client/testing'; diff --git a/src/containers/Chat/Chat.test.tsx b/src/containers/Chat/Chat.test.tsx index c9895901b..05c13e58f 100644 --- a/src/containers/Chat/Chat.test.tsx +++ b/src/containers/Chat/Chat.test.tsx @@ -1,5 +1,5 @@ import { cleanup, render, screen, waitFor } from '@testing-library/react'; -import 'mocks/matchMediaMock'; + import { MockedProvider } from '@apollo/client/testing'; import { setUserSession } from 'services/AuthService'; diff --git a/src/containers/Chat/ChatConversations/ConversationList/ConversationList.test.tsx b/src/containers/Chat/ChatConversations/ConversationList/ConversationList.test.tsx index 2fafe5a2c..7ae239c96 100644 --- a/src/containers/Chat/ChatConversations/ConversationList/ConversationList.test.tsx +++ b/src/containers/Chat/ChatConversations/ConversationList/ConversationList.test.tsx @@ -1,4 +1,3 @@ -import 'mocks/matchMediaMock'; import { MemoryRouter, BrowserRouter as Router } from 'react-router-dom'; import { render, waitFor, screen, fireEvent } from '@testing-library/react'; import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'; diff --git a/src/containers/Chat/ChatInterface/ChatInterface.test.tsx b/src/containers/Chat/ChatInterface/ChatInterface.test.tsx index 3eb29282c..8bfc55c40 100644 --- a/src/containers/Chat/ChatInterface/ChatInterface.test.tsx +++ b/src/containers/Chat/ChatInterface/ChatInterface.test.tsx @@ -1,4 +1,3 @@ -import 'mocks/matchMediaMock'; import { MemoryRouter } from 'react-router-dom'; import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'; import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'; diff --git a/src/containers/Chat/ChatMessages/ChatInput/ChatInput.test.tsx b/src/containers/Chat/ChatMessages/ChatInput/ChatInput.test.tsx index 8dd9648c0..46afeed58 100644 --- a/src/containers/Chat/ChatMessages/ChatInput/ChatInput.test.tsx +++ b/src/containers/Chat/ChatMessages/ChatInput/ChatInput.test.tsx @@ -1,4 +1,3 @@ -import 'mocks/matchMediaMock'; import { MockedProvider } from '@apollo/client/testing'; import { render, waitFor, fireEvent, screen } from '@testing-library/react'; import { vi } from 'vitest'; diff --git a/src/containers/Chat/ChatMessages/ChatMessages.test.tsx b/src/containers/Chat/ChatMessages/ChatMessages.test.tsx index e6bd31cff..ee92fc5c4 100644 --- a/src/containers/Chat/ChatMessages/ChatMessages.test.tsx +++ b/src/containers/Chat/ChatMessages/ChatMessages.test.tsx @@ -1,4 +1,3 @@ -import 'mocks/matchMediaMock'; import { InMemoryCache } from '@apollo/client'; import { MockedProvider } from '@apollo/client/testing'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; diff --git a/src/containers/Consulting/ConsultingList/ExportConsulting/ExportConsulting.test.tsx b/src/containers/Consulting/ConsultingList/ExportConsulting/ExportConsulting.test.tsx index 80ac4265d..7fe9658c4 100644 --- a/src/containers/Consulting/ConsultingList/ExportConsulting/ExportConsulting.test.tsx +++ b/src/containers/Consulting/ConsultingList/ExportConsulting/ExportConsulting.test.tsx @@ -8,6 +8,10 @@ import { getAllOrganizations } from 'mocks/Organization'; import { getOrganizationList, exportConsulting as exportConsultingMock } from 'mocks/Consulting'; import * as utils from 'common/utils'; +afterEach(() => { + (window as any).matchMedia = null; +}); + const mocks = [...getAllOrganizations, getOrganizationList, exportConsultingMock]; setUserSession(JSON.stringify({ organization: { id: '1' }, roles: ['Admin'] })); diff --git a/src/containers/Form/FormLayout.tsx b/src/containers/Form/FormLayout.tsx index acf268228..b707e273e 100644 --- a/src/containers/Form/FormLayout.tsx +++ b/src/containers/Form/FormLayout.tsx @@ -22,6 +22,7 @@ import { organizationHasDynamicRole } from 'common/utils'; import { getUserRole } from 'context/role'; import styles from './FormLayout.module.css'; import { HelpDataProps } from 'common/HelpData'; +import { LexicalWrapper } from 'common/LexicalWrapper'; export interface FormLayoutProps { deleteItemQuery: DocumentNode; @@ -536,67 +537,69 @@ export const FormLayout = ({ }; const form = ( -
-
- {formFieldItems.map((field, index) => { - const key = index; - - if (field.skip) { - return null; - } - - return ( - - {field.label && ( - - {field.label} - - )} - - - ); - })} -
- - {additionalAction ? ( + + +
+ {formFieldItems.map((field, index) => { + const key = index; + + if (field.skip) { + return null; + } + + return ( + + {field.label && ( + + {field.label} + + )} + + + ); + })} +
+ {additionalAction ? ( + + ) : null} + - ) : null} - - - {deleteButton} + + {deleteButton} +
-
- + + ); const handleDeleteItem = () => { diff --git a/src/containers/InteractiveMessage/InteractiveMessage.test.tsx b/src/containers/InteractiveMessage/InteractiveMessage.test.tsx index 2a1498af7..c276404f1 100644 --- a/src/containers/InteractiveMessage/InteractiveMessage.test.tsx +++ b/src/containers/InteractiveMessage/InteractiveMessage.test.tsx @@ -1,6 +1,5 @@ import { render, screen, waitFor, fireEvent, cleanup } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import 'mocks/matchMediaMock'; import { MockedProvider } from '@apollo/client/testing'; import axios from 'axios'; import { Route, MemoryRouter, Routes } from 'react-router-dom'; @@ -471,7 +470,7 @@ describe('translates the template', () => { }); }); - test('it translates an already exisiting template', async () => { + test('it shows error on translating an already exisiting template', async () => { render(renderInteractiveMessage('1', [translateInteractiveTemplateMock(true)])); await waitFor(() => { diff --git a/src/containers/InteractiveMessage/InteractiveMessage.tsx b/src/containers/InteractiveMessage/InteractiveMessage.tsx index f201043e1..1c2acc4ef 100644 --- a/src/containers/InteractiveMessage/InteractiveMessage.tsx +++ b/src/containers/InteractiveMessage/InteractiveMessage.tsx @@ -83,6 +83,7 @@ export const InteractiveMessage = () => { const [language, setLanguage] = useState({}); const [languageOptions, setLanguageOptions] = useState([]); const [editorState, setEditorState] = useState(''); + const [dynamicMedia, setDynamicMedia] = useState(false); const [saveClicked, setSaveClicked] = useState(false); diff --git a/src/containers/Template/Form/HSM/HSM.module.css b/src/containers/Template/Form/HSM/HSM.module.css index e5e7e58a0..caf736fa7 100644 --- a/src/containers/Template/Form/HSM/HSM.module.css +++ b/src/containers/Template/Form/HSM/HSM.module.css @@ -3,7 +3,7 @@ height: 29px; } -.IsActive { +.Checkbox { color: #555555; font-weight: 400; line-height: 18px; diff --git a/src/containers/Template/Form/HSM/HSM.test.tsx b/src/containers/Template/Form/HSM/HSM.test.tsx index d56121f2c..cd8781cab 100644 --- a/src/containers/Template/Form/HSM/HSM.test.tsx +++ b/src/containers/Template/Form/HSM/HSM.test.tsx @@ -1,11 +1,13 @@ -import 'mocks/matchMediaMock'; import { render, waitFor, within, fireEvent, screen } from '@testing-library/react'; import { MockedProvider } from '@apollo/client/testing'; import userEvent from '@testing-library/user-event'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; -import * as Notification from 'common/notification'; import { HSM } from './HSM'; -import { TEMPLATE_MOCKS } from 'containers/Template/Template.test.helper'; +import { + TEMPLATE_MOCKS, + getHSMTemplateTypeMedia, + getHSMTemplateTypeText, +} from 'containers/Template/Template.test.helper'; const mocks = TEMPLATE_MOCKS; @@ -15,8 +17,9 @@ beforeEach(() => { describe('Edit mode', () => { test('HSM form is loaded correctly in edit mode', async () => { - const { getByText } = render( - + const MOCKS = [...mocks, getHSMTemplateTypeText, getHSMTemplateTypeText]; + const { getByText, getAllByRole } = render( + } /> @@ -27,6 +30,35 @@ describe('Edit mode', () => { await waitFor(() => { expect(getByText('Edit HSM Template')).toBeInTheDocument(); }); + + await waitFor(() => { + expect(getAllByRole('textbox')[0]).toHaveValue('account_balance'); + }); + }); + + test('HSM templates with media', async () => { + const MOCKS = [...mocks, getHSMTemplateTypeMedia, getHSMTemplateTypeMedia]; + const { getByText, getAllByRole } = render( + + + + } /> + + + + ); + + await waitFor(() => { + expect(getByText('Edit HSM Template')).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(getAllByRole('textbox')[0]).toHaveValue('account_update'); + }); + + await waitFor(() => { + expect(screen.getAllByRole('combobox')[1]).toHaveValue('IMAGE'); + }); }); }); @@ -49,10 +81,9 @@ describe('Add mode', () => { const { queryByText } = within(container.querySelector('form') as HTMLElement); fireEvent.click(screen.getByTestId('submitActionButton')); - // we should have 2 errors + // we should have 1 errors await waitFor(() => { expect(queryByText('Title is required.')).toBeInTheDocument(); - expect(queryByText('Message is required.')).toBeInTheDocument(); }); fireEvent.change(container.querySelector('input[name="label"]') as HTMLInputElement, { @@ -64,111 +95,81 @@ describe('Add mode', () => { fireEvent.click(screen.getByTestId('submitActionButton')); - // we should still have 2 errors + // we should still have 1 errors await waitFor(() => { expect(queryByText('Title length is too long.')).toBeInTheDocument(); - expect(queryByText('Message is required.')).toBeInTheDocument(); }); }); test('it should create a template message', async () => { - const notificationSpy = vi.spyOn(Notification, 'setNotification'); render(template); await waitFor(() => { - expect(screen.getAllByTestId('AutocompleteInput')[0].querySelector('input')).toHaveValue( - 'English' - ); + expect(screen.getByText('Add a new HSM Template')).toBeInTheDocument(); }); - const title = screen.getAllByTestId('input')[0].querySelector('input') as HTMLInputElement; - const elementName = document.querySelector('input[name="shortcode"]') as HTMLInputElement; - const attachmentUrl = document.querySelector('input[name="attachmentURL"]') as HTMLInputElement; - const message = screen.getByTestId('editor-body') as HTMLElement; - const sampleMessage = screen.getByTestId('editor-example') as HTMLElement; - - await user.type(title, 'Hello'); + const inputs = screen.getAllByRole('textbox'); - await user.type(elementName, 'welcome'); + fireEvent.change(inputs[0], { target: { value: 'element_name' } }); + fireEvent.change(inputs[1], { target: { value: 'element_name' } }); + const lexicalEditor = inputs[2]; - // add message - await user.click(message); + await user.click(lexicalEditor); await user.tab(); - fireEvent.input(message, { data: 'Hi {{1}}, How are you' }); + fireEvent.input(lexicalEditor, { data: 'Hi, How are you' }); - // add Sample message - await user.click(sampleMessage); - await user.tab(); - fireEvent.input(sampleMessage, { data: 'Hi [Glific], How are you' }); + const autocompletes = screen.getAllByTestId('autocomplete-element'); + autocompletes[1].focus(); + fireEvent.keyDown(autocompletes[1], { key: 'ArrowDown' }); - const [_language, category, attachmentType] = screen.getAllByTestId('autocomplete-element'); - category.focus(); - fireEvent.keyDown(category, { key: 'ArrowDown' }); - fireEvent.keyDown(category, { key: 'ArrowDown' }); - fireEvent.keyDown(category, { key: 'Enter' }); + fireEvent.click(screen.getByText('ACCOUNT_UPDATE'), { key: 'Enter' }); fireEvent.click(screen.getByText('Allow meta to re-categorize template?')); - attachmentType.focus(); - fireEvent.keyDown(attachmentType, { key: 'ArrowDown' }); - fireEvent.keyDown(attachmentType, { key: 'ArrowDown' }); - fireEvent.keyDown(attachmentType, { key: 'Enter' }); - - await user.type( - attachmentUrl, - 'https://www.buildquickbots.com/whatsapp/media/sample/jpg/sample02.jpg' - ); + await waitFor(() => { + expect(screen.getByText('Hi, How are you')).toBeInTheDocument(); + }); - fireEvent.click(screen.getByTestId('submitActionButton')); + fireEvent.click(screen.getByText('Add Variable')); await waitFor(() => { - expect(notificationSpy).toHaveBeenCalled(); + expect(screen.getByText('Hi, How are you {{1}}')).toBeInTheDocument(); }); - }, 10000); + fireEvent.change(inputs[1], { target: { value: 'element_name' } }); + + fireEvent.change(screen.getByPlaceholderText('Define value'), { target: { value: 'User' } }); + + fireEvent.click(screen.getByTestId('submitActionButton')); + }); - test('it should check sample and body', async () => { + test('it should add and remove variables', async () => { render(template); await waitFor(() => { - expect(screen.getAllByTestId('AutocompleteInput')[0].querySelector('input')).toHaveValue( - 'English' - ); + expect(screen.getByText('Add a new HSM Template')).toBeInTheDocument(); }); - const title = screen.getAllByTestId('input')[0].querySelector('input') as HTMLInputElement; - const elementName = document.querySelector('input[name="shortcode"]') as HTMLInputElement; - const message = screen.getByTestId('editor-body') as HTMLElement; - const sampleMessage = screen.getByTestId('editor-example') as HTMLElement; + const inputs = screen.getAllByRole('textbox'); + const lexicalEditor = inputs[2]; - await user.type(title, 'Hello'); - - await user.type(elementName, 'welcome'); - - // add message - await user.click(message); + await user.click(lexicalEditor); await user.tab(); - fireEvent.input(message, { data: 'Hi {{1}}, How are you' }); + fireEvent.input(lexicalEditor, { data: 'Hi' }); - // add Sample message - await user.click(sampleMessage); - await user.tab(); - fireEvent.input(sampleMessage, { data: 'Hi Glific, How are you' }); + await waitFor(() => { + expect(screen.getByText('Hi')).toBeInTheDocument(); + }); - // save template - fireEvent.click(screen.getByTestId('submitActionButton')); + fireEvent.click(screen.getByText('Add Variable')); await waitFor(() => { - // expect an error - expect( - screen.getByText( - 'Message and sample look different. You have to replace variables eg. {{1}} with actual values enclosed in [ ] eg. Replace {{1}} with [Monica].' - ) - ).toBeInTheDocument(); + expect(screen.getByText('Hi {{1}}')).toBeInTheDocument(); }); + + fireEvent.click(screen.getAllByTestId('delete-variable')[0]); }); - test('add quick reply buttons when adding a template', async () => { - const notificationSpy = vi.spyOn(Notification, 'setNotification'); + test('it adds quick reply buttons', async () => { render(template); await waitFor(() => { @@ -176,56 +177,48 @@ describe('Add mode', () => { expect(language).toHaveValue('English'); }); - const title = screen.getAllByTestId('input')[0].querySelector('input') as HTMLInputElement; - const elementName = document.querySelector('input[name="shortcode"]') as HTMLInputElement; - const message = screen.getByTestId('editor-body') as HTMLElement; - const sampleMessage = screen.getByTestId('editor-example') as HTMLElement; + const inputs = screen.getAllByRole('textbox'); + + const elementName = inputs[0]; + const title = inputs[1]; await user.type(title, 'Hello'); await user.type(elementName, 'welcome'); - // add message - await user.click(message); - await user.tab(); - fireEvent.input(message, { data: 'Hi {{1}}, How are you' }); + const lexicalEditor = inputs[2]; - // add Sample message - await user.click(sampleMessage); + await user.click(lexicalEditor); await user.tab(); - fireEvent.input(sampleMessage, { data: 'Hi [Glific], How are you' }); + fireEvent.input(lexicalEditor, { data: 'Hi' }); - await user.click(screen.getAllByTestId('checkboxLabel')[1]); - await user.click(screen.getByText('Quick replies')); - await user.click(screen.getByTestId('addButton')); + await waitFor(() => { + expect(screen.getByText('Hi')).toBeInTheDocument(); + }); - const quickReply1 = screen - .getAllByTestId('quickReplyWrapper')[0] - .querySelector('input') as HTMLInputElement; - const quickReply2 = screen - .getAllByTestId('quickReplyWrapper')[1] - .querySelector('input') as HTMLInputElement; - - await user.type(quickReply1, 'Quick reply 1'); - fireEvent.blur(quickReply1); - await user.type(quickReply2, 'Quick reply 2'); - fireEvent.blur(quickReply2); - - // update category - const [_language, category] = screen.getAllByTestId('autocomplete-element'); - category.focus(); - fireEvent.keyDown(category, { key: 'ArrowDown' }); - fireEvent.keyDown(category, { key: 'ArrowDown' }); - fireEvent.keyDown(category, { key: 'Enter' }); + fireEvent.click(screen.getByText('Add buttons')); + fireEvent.click(screen.getByText('Quick replies')); - fireEvent.click(screen.getByTestId('submitActionButton')); + await user.click(screen.getByTestId('addButton')); - await waitFor(() => { - expect(notificationSpy).toHaveBeenCalled(); + fireEvent.change(screen.getByPlaceholderText('Quick reply 1 title'), { + target: { value: 'Yes' }, }); + + fireEvent.change(screen.getByPlaceholderText('Quick reply 2 title'), { + target: { value: 'No' }, + }); + + const autocompletes = screen.getAllByTestId('autocomplete-element'); + autocompletes[1].focus(); + fireEvent.keyDown(autocompletes[1], { key: 'ArrowDown' }); + + fireEvent.click(screen.getByText('ACCOUNT_UPDATE'), { key: 'Enter' }); + + fireEvent.click(screen.getByTestId('submitActionButton')); + fireEvent.click(screen.getByTestId('submitActionButton')); }); - test('add quick reply buttons with call to action', async () => { - const notificationSpy = vi.spyOn(Notification, 'setNotification'); + test('it adds call to action buttons', async () => { render(template); await waitFor(() => { @@ -233,51 +226,84 @@ describe('Add mode', () => { expect(language).toHaveValue('English'); }); - const title = screen.getAllByTestId('input')[0].querySelector('input') as HTMLInputElement; - const elementName = document.querySelector('input[name="shortcode"]') as HTMLInputElement; - const message = screen.getByTestId('editor-body') as HTMLElement; - const sampleMessage = screen.getByTestId('editor-example') as HTMLElement; + const inputs = screen.getAllByRole('textbox'); + + const elementName = inputs[0]; + const title = inputs[1]; await user.type(title, 'Hello'); await user.type(elementName, 'welcome'); - // add message - await user.click(message); - await user.tab(); - fireEvent.input(message, { data: 'Hi {{1}}, How are you' }); + const lexicalEditor = inputs[2]; - // add Sample message - await user.click(sampleMessage); + await user.click(lexicalEditor); await user.tab(); - fireEvent.input(sampleMessage, { data: 'Hi [[Glific], How are you' }); - - await user.click(screen.getAllByTestId('checkboxLabel')[1]); - await user.click(screen.getByText('Call to actions')); - await user.click(screen.getByText('Phone number')); - - const quickReply1 = screen - .getByTestId('buttonTitle') - .querySelector('input') as HTMLInputElement; - const quickReply2 = screen - .getByTestId('buttonValue') - .querySelector('input') as HTMLInputElement; - - await user.type(quickReply1, 'Call me'); - fireEvent.blur(quickReply1); - await user.type(quickReply2, '9876543210'); - fireEvent.blur(quickReply2); - - // update category - const [_language, category] = screen.getAllByTestId('autocomplete-element'); - category.focus(); - fireEvent.keyDown(category, { key: 'ArrowDown' }); - fireEvent.keyDown(category, { key: 'ArrowDown' }); - fireEvent.keyDown(category, { key: 'Enter' }); + fireEvent.input(lexicalEditor, { data: 'Hi' }); + + await waitFor(() => { + expect(screen.getByText('Hi')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Add buttons')); + fireEvent.click(screen.getByText('Call to actions')); + fireEvent.click(screen.getByText('Phone number')); + + fireEvent.change(screen.getByPlaceholderText('Button Title'), { target: { value: 'Call me' } }); + fireEvent.change(screen.getByPlaceholderText('Button Value'), { + target: { value: '9876543210' }, + }); + + fireEvent.click(screen.getByText('Add Call to action')); + fireEvent.click(screen.getAllByTestId('delete-icon')[1]); + + const autocompletes = screen.getAllByTestId('autocomplete-element'); + autocompletes[1].focus(); + fireEvent.keyDown(autocompletes[1], { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('ACCOUNT_UPDATE'), { key: 'Enter' }); + + fireEvent.click(screen.getByTestId('submitActionButton')); fireEvent.click(screen.getByTestId('submitActionButton')); + }); + + test('adding attachments', async () => { + render(template); + + await waitFor(() => { + expect(screen.getByText('Add a new HSM Template')).toBeInTheDocument(); + }); + + const autocompletes = screen.getAllByTestId('autocomplete-element'); + const inputs = screen.getAllByRole('textbox'); + + autocompletes[2].focus(); + fireEvent.keyDown(autocompletes[2], { key: 'ArrowDown' }); + fireEvent.click(screen.getByText('IMAGE'), { key: 'Enter' }); + + fireEvent.change(inputs[3], { target: { value: 'https://example.com/image.jpg' } }); + + await waitFor(() => { + expect(inputs[3]).toHaveValue('https://example.com/image.jpg'); + }); + }); + + test('it creates a translation of hsm template', async () => { + render(template); + + await waitFor(() => { + expect(screen.getByText('Add a new HSM Template')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Translate existing HSM?')); + + const autocompletes = screen.getAllByTestId('autocomplete-element'); + autocompletes[1].focus(); + fireEvent.keyDown(autocompletes[1], { key: 'ArrowDown' }); + + fireEvent.click(screen.getByText('account_balance'), { key: 'Enter' }); await waitFor(() => { - expect(notificationSpy).toHaveBeenCalled(); + expect(screen.getAllByRole('combobox')[1]).toHaveValue('account_balance'); }); }); }); diff --git a/src/containers/Template/Form/HSM/HSM.tsx b/src/containers/Template/Form/HSM/HSM.tsx index 182967ec6..2cafc04b8 100644 --- a/src/containers/Template/Form/HSM/HSM.tsx +++ b/src/containers/Template/Form/HSM/HSM.tsx @@ -4,10 +4,9 @@ import { useTranslation } from 'react-i18next'; import { useParams, useLocation } from 'react-router-dom'; import TemplateIcon from 'assets/images/icons/Template/UnselectedDark.svg?react'; -import { GET_HSM_CATEGORIES } from 'graphql/queries/Template'; +import { GET_HSM_CATEGORIES, GET_SHORTCODES } from 'graphql/queries/Template'; import { AutoComplete } from 'components/UI/Form/AutoComplete/AutoComplete'; import { Input } from 'components/UI/Form/Input/Input'; -import { EmojiInput } from 'components/UI/Form/EmojiInput/EmojiInput'; import { Loading } from 'components/UI/Layout/Loading/Loading'; import { Simulator } from 'components/simulator/Simulator'; import Template from '../Template'; @@ -29,10 +28,10 @@ export const HSM = () => { body: '', }); - const [shortcode, setShortcode] = useState(''); + const [exisitingShortCode, setExistingShortcode] = useState(''); + const [newShortcode, setNewShortcode] = useState(''); const [category, setCategory] = useState(undefined); - const [example, setExample] = useState(); - const [editorState, setEditorState] = useState(''); + const [languageVariant, setLanguageVariant] = useState(false); const [allowTemplateCategoryChange, setAllowTemplateCategoryChange] = useState(false); const { t } = useTranslation(); @@ -40,6 +39,13 @@ export const HSM = () => { const location: any = useLocation(); const { data: categoryList, loading } = useQuery(GET_HSM_CATEGORIES); + const { data: shortCodes } = useQuery(GET_SHORTCODES, { + variables: { + filter: { + isHsm: true, + }, + }, + }); if (loading) { return ; @@ -52,6 +58,13 @@ export const HSM = () => { }); } + const shortCodeOptions: any = []; + if (shortCodes) { + shortCodes.sessionTemplates.forEach((value: any, index: number) => { + shortCodeOptions.push({ label: value?.shortcode, id: index }); + }); + } + const removeFirstLineBreak = (text: any) => text?.length === 1 ? text.slice(0, 1).replace(/(\r\n|\n|\r)/, '') : text; @@ -101,20 +114,40 @@ export const HSM = () => { const formFields = [ { - component: EmojiInput, - name: 'example', - label: `${t('Sample message')}*`, - rows: 5, - convertToWhatsApp: true, - textArea: true, + component: Checkbox, + name: 'languageVariant', + title: ( + + Translate existing HSM? + + ), + handleChange: (value: any) => setLanguageVariant(value), + skip: isEditing, + }, + { + component: Input, + name: 'newShortCode', + placeholder: `${t('Element name')}*`, + label: `${t('Element name')}*`, + disabled: isEditing, + skip: languageVariant ? true : false, + onChange: (value: any) => { + setNewShortcode(value); + }, + }, + { + component: AutoComplete, + name: 'existingShortCode', + options: shortCodeOptions, + optionLabel: 'label', + multiple: false, + label: `${t('Element name')}*`, + placeholder: `${t('Element name')}*`, disabled: isEditing, - helperText: - 'Replace variables eg. {{1}} with actual values enclosed in [ ] eg. [12345] to show a complete message with meaningful word/statement/numbers/ special characters.', - handleChange: (value: any) => { - setExample(value); - getSimulatorMessage(value); + onChange: (event: any) => { + setExistingShortcode(event); }, - defaultValue: isEditing && editorState, + skip: languageVariant ? false : true, }, { component: AutoComplete, @@ -129,12 +162,13 @@ export const HSM = () => { onChange: (event: any) => { setCategory(event); }, + skip: isEditing, }, { component: Checkbox, name: 'allowTemplateCategoryChange', title: ( - + Allow meta to re-categorize template? ), @@ -144,13 +178,13 @@ export const HSM = () => { }, { component: Input, - name: 'shortcode', - placeholder: `${t('Element name')}*`, - label: `${t('Element name')}*`, + name: 'category', + type: 'text', + label: `${t('Category')}*`, + placeholder: `${t('Category')}*`, disabled: isEditing, - inputProp: { - onBlur: (event: any) => setShortcode(event.target.value), - }, + helperText: t('Select the most relevant category'), + skip: !isEditing, }, ]; @@ -163,14 +197,16 @@ export const HSM = () => { defaultAttribute={defaultAttribute} formField={formFields} getUrlAttachmentAndType={getAttachmentUrl} - getShortcode={shortcode} - getExample={example} setCategory={setCategory} category={category} onExampleChange={addButtonsToSampleMessage} - setExampleState={setEditorState} allowTemplateCategoryChange={allowTemplateCategoryChange} setAllowTemplateCategoryChange={setAllowTemplateCategoryChange} + languageVariant={languageVariant} + getSimulatorMessage={getSimulatorMessage} + setNewShortcode={setNewShortcode} + newShortCode={newShortcode} + existingShortCode={exisitingShortCode} />
diff --git a/src/containers/Template/Form/SpeedSend/SpeedSend.test.tsx b/src/containers/Template/Form/SpeedSend/SpeedSend.test.tsx index 64a5ba2e6..1fad0d934 100644 --- a/src/containers/Template/Form/SpeedSend/SpeedSend.test.tsx +++ b/src/containers/Template/Form/SpeedSend/SpeedSend.test.tsx @@ -1,18 +1,17 @@ -import 'mocks/matchMediaMock'; -import { render, within, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import { render, within, fireEvent, cleanup, waitFor, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { MockedProvider } from '@apollo/client/testing'; import { Routes, Route } from 'react-router-dom'; import { SpeedSendList } from 'containers/Template/List/SpeedSendList/SpeedSendList'; -import { TEMPLATE_MOCKS } from 'containers/Template/Template.test.helper'; +import { SPEED_SENDS_MOCKS } from 'containers/Template/Template.test.helper'; import { setUserSession } from 'services/AuthService'; import { SpeedSend } from './SpeedSend'; -beforeEach(() => { - cleanup(); -}); -const mocks = [...TEMPLATE_MOCKS, ...TEMPLATE_MOCKS]; +import * as Notification from 'common/notification'; + setUserSession(JSON.stringify({ roles: ['Admin'] })); +const mocks = SPEED_SENDS_MOCKS; + const mockIntersectionObserver = class { constructor() {} observe() {} @@ -22,6 +21,10 @@ const mockIntersectionObserver = class { (window as any).IntersectionObserver = mockIntersectionObserver; +afterEach(() => { + cleanup(); +}); + describe('SpeedSend', () => { test('cancel button should redirect to SpeedSendlist page', async () => { const { container, getByText } = render( @@ -71,7 +74,40 @@ describe('SpeedSend', () => { }); await waitFor(() => { - expect(queryByText('Message is required.')).toBeInTheDocument(); + expect(queryByText('Title is required.')).toBeInTheDocument(); + }); + }); + + test('should test translations', async () => { + const notificationSpy = vi.spyOn(Notification, 'setNotification'); + render( + + + + } /> + + + + ); + + await waitFor(() => { + expect(screen.getByText('Title')).toBeInTheDocument(); + expect(screen.getByText('Marathi')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Marathi')); + + await waitFor(() => { + expect(screen.getByText('Title')).toBeInTheDocument(); + expect(screen.getByText('English')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('English')); + + fireEvent.click(screen.getByTestId('submitActionButton')); + + await waitFor(() => { + expect(notificationSpy).toHaveBeenCalled(); }); }); }); diff --git a/src/containers/Template/Form/Template.test.tsx b/src/containers/Template/Form/Template.test.tsx index 4379f49e1..db1984b3e 100644 --- a/src/containers/Template/Form/Template.test.tsx +++ b/src/containers/Template/Form/Template.test.tsx @@ -1,4 +1,3 @@ -import 'mocks/matchMediaMock'; import { render, waitFor, fireEvent } from '@testing-library/react'; import { MockedProvider } from '@apollo/client/testing'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; @@ -6,19 +5,25 @@ import { vi } from 'vitest'; import * as FormLayout from 'containers/Form/FormLayout'; import Template from './Template'; -import { TEMPLATE_MOCKS } from '../Template.test.helper'; +import { TEMPLATE_MOCKS, getSpendSendTemplate } from '../Template.test.helper'; import { HSM_TEMPLATE_MOCKS, templateFormHSMFormFields } from './Template.test.helper'; beforeEach(() => { vi.restoreAllMocks(); }); -const defaultMocks = [...TEMPLATE_MOCKS, ...TEMPLATE_MOCKS]; +const defaultMocks = [ + ...TEMPLATE_MOCKS, + ...TEMPLATE_MOCKS, + getSpendSendTemplate, + getSpendSendTemplate, +]; const defaultProps = { listItemName: 'Speed sends', redirectionLink: 'speed-send', defaultAttribute: { isHsm: false }, icon: null, + getSimulatorMessage: vi.fn(), }; const templateEdit = (props: any = defaultProps, mocks: any = defaultMocks) => ( @@ -74,7 +79,8 @@ const hsmProps = { languageStyle: 'dropdown', formField: templateFormHSMFormFields, setCategory: vi.fn(), - setExampleState: vi.fn(), + getSimulatorMessage: vi.fn(), + setNewShortcode: vi.fn(), }; const hsmTemplateEdit = (templateId: string) => ( diff --git a/src/containers/Template/Form/Template.tsx b/src/containers/Template/Form/Template.tsx index 5a0888a6c..c68571d2b 100644 --- a/src/containers/Template/Form/Template.tsx +++ b/src/containers/Template/Form/Template.tsx @@ -27,6 +27,7 @@ import { import { CreateAutoComplete } from 'components/UI/Form/CreateAutoComplete/CreateAutoComplete'; import { validateMedia } from 'common/utils'; import styles from './Template.module.css'; +import { TemplateVariables } from '../TemplateVariables/TemplateVariables'; const regexForShortcode = /^[a-z0-9_]+$/g; @@ -102,6 +103,52 @@ const getTemplateAndButtons = (templateType: string, message: string, buttons: s return { buttons: result, template }; }; +const getExampleFromBody = (body: string, variables: Array) => { + return body.replace(/{{(\d+)}}/g, (match, number) => { + let index = parseInt(number) - 1; + + return variables[index]?.text + ? variables[index] + ? `[${variables[index]?.text}]` + : match + : `{{${number}}}`; + }); +}; + +const getVariables = (body: string, example?: string) => { + const variablePattern = /\{\{(\d+)\}\}/g; + const examplePattern = /\[([^\]]+)\]/g; + let match; + let foundIds = new Set(); + let examples = []; + let variables: any = []; + + // Extract {{id}} patterns from body + while ((match = variablePattern.exec(body)) !== null) { + foundIds.add(parseInt(match[1], 10)); + } + + // Extract example values if example string is provided + if (example) { + while ((match = examplePattern.exec(example)) !== null) { + examples.push(match[1]); + } + } + + // Sort the IDs + let sortedIds = Array.from(foundIds).sort((a: any, b: any) => a - b); + + // Create variables array with ids and examples or empty text + sortedIds.forEach((id, index) => { + variables.push({ + text: examples[index] || '', + id: index + 1, + }); + }); + + return variables; +}; + export interface TemplateProps { listItemName: string; redirectionLink: string; @@ -110,15 +157,17 @@ export interface TemplateProps { formField?: any; customStyle?: any; getUrlAttachmentAndType?: any; - getShortcode?: any; - getExample?: any; setCategory?: any; category?: any; onExampleChange?: any; languageStyle?: string; - setExampleState?: any; + getSimulatorMessage?: any; allowTemplateCategoryChange?: boolean; setAllowTemplateCategoryChange?: any; + languageVariant?: boolean; + newShortCode?: any; + setNewShortcode?: any; + existingShortCode?: any; } interface CallToActionTemplate { @@ -139,15 +188,17 @@ const Template = ({ formField, customStyle, getUrlAttachmentAndType, - getShortcode, - getExample, setCategory, category, onExampleChange = () => {}, - languageStyle = 'dropdown', - setExampleState, allowTemplateCategoryChange, setAllowTemplateCategoryChange, + languageStyle = 'dropdown', + getSimulatorMessage, + languageVariant, + setNewShortcode, + newShortCode, + existingShortCode, }: TemplateProps) => { // "Audio" option is removed in case of HSM Template const mediaTypes = @@ -158,8 +209,6 @@ const Template = ({ const [tagId, setTagId] = useState(null); const [label, setLabel] = useState(''); const [body, setBody] = useState(''); - const [example, setExample] = useState(''); - const [shortcode, setShortcode] = useState(''); const [language, setLanguageId] = useState(null); const [type, setType] = useState(null); const [translations, setTranslations] = useState(); @@ -175,6 +224,7 @@ const Template = ({ >([]); const [isAddButtonChecked, setIsAddButtonChecked] = useState(false); const [nextLanguage, setNextLanguage] = useState(''); + const [variables, setVariables] = useState([]); const [editorState, setEditorState] = useState(''); const { t } = useTranslation(); const navigate = useNavigate(); @@ -218,13 +268,15 @@ const Template = ({ body, type, attachmentURL, - shortcode, - example, category, tagId, isActive, templateButtons, isAddButtonChecked, + languageVariant, + variables, + newShortCode, + existingShortCode, allowTemplateCategoryChange, }; @@ -264,36 +316,31 @@ const Template = ({ setLabel(labelValue); setIsActive(isActiveValue); - if (setAllowTemplateCategoryChange) { - setAllowTemplateCategoryChange(allowCategoryChangeValue); - } - + let variables: any = []; if (typeof bodyValue === 'string') { setBody(bodyValue || ''); setEditorState(bodyValue || ''); } if (exampleValue) { - let exampleBody: any; if (hasButtons) { setTemplateType(templateButtonType); - const { buttons: buttonsVal, template } = getTemplateAndButtons( + const { buttons: buttonsVal } = getTemplateAndButtons( templateButtonType, exampleValue, buttons ); - exampleBody = template; setTemplateButtons(buttonsVal); - } else { - exampleBody = exampleValue; } + variables = getVariables(bodyValue, exampleValue); + setVariables(variables); + onExampleChange(getExampleFromBody(bodyValue, variables)); + } - if (setExampleState) { - setExampleState(exampleValue); - } - setExample(exampleValue); - onExampleChange(exampleBody); + if (shortcodeValue && setNewShortcode) { + setNewShortcode(shortcodeValue); } + if (hasButtons) { setIsAddButtonChecked(hasButtons); } @@ -313,7 +360,7 @@ const Template = ({ const content = translationsCopy[currentLanguage]; setLabel(content.label); setBody(content.body || ''); - setEditorState(bodyValue || ''); + setEditorState(content.body || ''); } setTranslations(translationsValue); } @@ -322,15 +369,15 @@ const Template = ({ } else { setAttachmentURL(''); } - if (shortcodeValue) { - setTimeout(() => setShortcode(shortcodeValue), 0); - } if (categoryValue && setCategory) { - setCategory({ label: categoryValue, id: categoryValue }); + setCategory(categoryValue); } if (tagIdValue) { setTagId(tagIdValue); } + if (setAllowTemplateCategoryChange) { + setAllowTemplateCategoryChange(allowCategoryChangeValue); + } }; const updateStates = ({ @@ -388,28 +435,28 @@ const Template = ({ }; const HSMValidation = { - example: Yup.string() - .max(1024, t('Maximum 1024 characters are allowed')) - .when('body', ([body], schema: any) => - schema.test({ - test: (exampleValue: any) => { - const finalmessageValue = body && body.replace(/\{\{([1-9]|1[0-9])\}\}/g, '[]'); - const finalExampleValue = exampleValue && exampleValue.replace(/\[[^\]]*\]/g, '[]'); - return finalExampleValue === finalmessageValue; - }, - message: t( - 'Message and sample look different. You have to replace variables eg. {{1}} with actual values enclosed in [ ] eg. Replace {{1}} with [Monica].' - ), - }) - ) - .required('Example is required.'), category: Yup.object().nullable().required(t('Category is required.')), - shortcode: Yup.string() - .required(t('Element name is required.')) - .matches( - regexForShortcode, - 'Only lowercase alphanumeric characters and underscores are allowed.' - ), + variables: Yup.array().of( + Yup.object().shape({ + text: Yup.string().required('Variable is required').min(1, 'Text cannot be empty'), + }) + ), + newShortCode: Yup.string().when('languageVariant', { + is: (val: any) => val === true, + then: (schema) => schema.nullable(), + otherwise: (schema) => + schema + .required(t('Element name is required.')) + .matches( + regexForShortcode, + 'Only lowercase alphanumeric characters and underscores are allowed.' + ), + }), + existingShortCode: Yup.object().when('languageVariant', { + is: (val: any) => val === true, + then: (schema) => schema.nullable().required(t('Element name is required.')), + otherwise: (schema) => schema.nullable(), + }), }; const validateURL = (value: string) => { @@ -479,16 +526,6 @@ const Template = ({ } }, [languages]); - useEffect(() => { - setShortcode(getShortcode); - }, [getShortcode]); - - useEffect(() => { - if (getExample) { - setExample(getExample); - } - }, [getExample]); - useEffect(() => { if ((type === '' || type) && attachmentURL) { validateURL(attachmentURL); @@ -510,29 +547,34 @@ const Template = ({ // Removing buttons when checkbox is checked or unchecked useEffect(() => { - if (getExample) { - const { message }: any = getTemplateAndButton(getExample); + if (isEditing) { + const { message }: any = getTemplateAndButton(getExampleFromBody(body, variables)); onExampleChange(message || ''); } }, [isAddButtonChecked]); // Converting buttons to template and vice-versa to show realtime update on simulator useEffect(() => { - if (templateButtons.length > 0) { + if (templateButtons.length > 0 && !isEditing) { const parse = convertButtonsToTemplate(templateButtons, templateType); const parsedText = parse.length ? `| ${parse.join(' | ')}` : null; - const { message }: any = getTemplateAndButton(example); + const { message }: any = getTemplateAndButton(getExampleFromBody(body, variables)); const sampleText: any = parsedText && message + parsedText; - if (sampleText) { onExampleChange(sampleText); } } }, [templateButtons]); + useEffect(() => { + if (getSimulatorMessage && !isEditing) { + getSimulatorMessage(getExampleFromBody(body, variables)); + } + }, [body, variables]); + if (languageLoading || templateLoading || tagLoading) { return ; } @@ -669,8 +711,20 @@ const Template = ({ onLanguageChange, }; + const handeInputChange = (event: any, row: any, index: any, eventType: any) => { + const { value } = event.target; + const obj = { ...row }; + obj[eventType] = value; + + const result = templateButtons.map((val: any, idx: number) => { + if (idx === index) return obj; + return val; + }); + + setTemplateButtons(result); + }; + const formFields = [ - formIsActive, languageComponent, { component: Input, @@ -684,7 +738,7 @@ const Template = ({ onBlur: (event: any) => setLabel(event.target.value), }, }, - + formIsActive, { component: EmojiInput, name: 'body', @@ -694,7 +748,7 @@ const Template = ({ textArea: true, disabled: defaultAttribute.isHsm && isEditing, helperText: defaultAttribute.isHsm - ? 'You can also use variable and interactive actions. Variable format: {{1}}, Button format: [Button text,Value] Value can be a URL or a phone number.' + ? 'You can provide variable values in your HSM templates to personalize the message. To add: click on the variable button and provide an example value for the variable in the field provided below' : null, handleChange: (value: any) => { setBody(value); @@ -703,19 +757,6 @@ const Template = ({ }, ]; - const handeInputChange = (event: any, row: any, index: any, eventType: any) => { - const { value } = event.target; - const obj = { ...row }; - obj[eventType] = value; - - const result = templateButtons.map((val: any, idx: number) => { - if (idx === index) return obj; - return val; - }); - - setTemplateButtons(result); - }; - const templateRadioOptions = [ { component: Checkbox, @@ -756,11 +797,24 @@ const Template = ({ helperText: t('Use this to categorize your templates.'), }; + const templateVariables = [ + { + component: TemplateVariables, + message: body, + variables: variables, + setVariables: setVariables, + getVariables: getVariables, + isEditing: isEditing, + }, + ]; + const hsmFields = formField && [ - ...formFields, - ...formField.slice(0, 1), + ...formFields.slice(0, 1), + ...formField.slice(0, 3), + ...formFields.slice(1), + ...templateVariables, ...templateRadioOptions, - ...formField.slice(1), + ...formField.slice(3), ...attachmentField, tags, ]; @@ -789,7 +843,7 @@ const Template = ({ // get template body const templateBody = getTemplateAndButton(body); - const templateExample = getTemplateAndButton(example); + const templateExample = getTemplateAndButton(getExampleFromBody(body, variables)); return { hasButtons: true, @@ -802,11 +856,11 @@ const Template = ({ const setPayload = (payload: any) => { let payloadCopy = payload; - let translationsCopy: any = {}; + let translationsCopy: any = {}; if (template) { - if (template.sessionTemplate.sessionTemplate.language.id === language.id) { - payloadCopy.languageId = language.id; + if (template.sessionTemplate.sessionTemplate.language.id === language?.id) { + payloadCopy.languageId = language?.id; if (payloadCopy.type) { payloadCopy.type = payloadCopy.type.id; // STICKER is a type of IMAGE @@ -819,7 +873,6 @@ const Template = ({ delete payloadCopy.language; if (payloadCopy.isHsm) { - payloadCopy.category = payloadCopy.category.label; if (isAddButtonChecked && templateType) { const templateButtonData = getButtonTemplatePayload(); Object.assign(payloadCopy, { ...templateButtonData }); @@ -828,6 +881,7 @@ const Template = ({ delete payloadCopy.example; delete payloadCopy.shortcode; delete payloadCopy.category; + delete payloadCopy.variables; delete payloadCopy.allowTemplateCategoryChange; } if (payloadCopy.type === 'TEXT') { @@ -848,7 +902,7 @@ const Template = ({ // Update template translation if (translations) { translationsCopy = JSON.parse(translations); - translationsCopy[language.id] = { + translationsCopy[language?.id] = { status: 'approved', languageId: language, label: payloadCopy.label, @@ -874,22 +928,35 @@ const Template = ({ payloadCopy.type = 'TEXT'; } if (payloadCopy.isHsm) { - payloadCopy.category = payloadCopy.category.label; - + payloadCopy.category = category.label; + if (payloadCopy.body) { + payloadCopy.example = getExampleFromBody(payloadCopy.body, variables); + } + if (languageVariant) { + payloadCopy.shortcode = existingShortCode.label; + } else { + payloadCopy.shortcode = newShortCode; + } if (isAddButtonChecked && templateType) { const templateButtonData = getButtonTemplatePayload(); Object.assign(payloadCopy, { ...templateButtonData }); } } else { delete payloadCopy.example; + delete payloadCopy.isActive; delete payloadCopy.shortcode; delete payloadCopy.category; delete payloadCopy.allowTemplateCategoryChange; } + delete payloadCopy.languageVariant; + delete payloadCopy.getShortcode; delete payloadCopy.isAddButtonChecked; delete payloadCopy.templateButtons; delete payloadCopy.language; + delete payloadCopy.variables; + delete payloadCopy.existingShortCode; + delete payloadCopy.newShortCode; if (payloadCopy.type === 'TEXT') { delete payloadCopy.attachmentURL; @@ -900,7 +967,6 @@ const Template = ({ if (tagId) { payloadCopy.tagId = payload.tagId.id; } - return payloadCopy; }; @@ -921,9 +987,6 @@ const Template = ({ const validation: any = { language: Yup.object().nullable().required('Language is required.'), label: Yup.string().required(t('Title is required.')).max(50, t('Title length is too long.')), - body: Yup.string() - .required(t('Message is required.')) - .max(1024, 'Maximum 1024 characters are allowed'), type: Yup.object() .nullable() .when('attachmentURL', { @@ -936,6 +999,9 @@ const Template = ({ is: (val: any) => val && val.id, then: (schema) => schema.required(t('Attachment URL is required.')), }), + body: Yup.string() + .required(t('Message is required.')) + .max(1024, 'Maximum 1024 characters are allowed'), }; if (defaultAttribute.isHsm && isAddButtonChecked) { diff --git a/src/containers/Template/Template.test.helper.ts b/src/containers/Template/Template.test.helper.ts index bc47713e7..44a257ea2 100644 --- a/src/containers/Template/Template.test.helper.ts +++ b/src/containers/Template/Template.test.helper.ts @@ -5,8 +5,9 @@ import { GET_HSM_CATEGORIES, GET_TEMPLATES_COUNT, FILTER_SESSION_TEMPLATES, + GET_SHORTCODES, } from 'graphql/queries/Template'; -import { DELETE_TEMPLATE, CREATE_TEMPLATE } from 'graphql/mutations/Template'; +import { DELETE_TEMPLATE, CREATE_TEMPLATE, UPDATE_TEMPLATE } from 'graphql/mutations/Template'; import { getOrganizationLanguagesQuery, getOrganizationLanguagesQueryByOrder, @@ -234,30 +235,68 @@ export const whatsappHsmCategories = [ }, ]; -const getTemplateData = { +const getTemplateDataTypeText = { sessionTemplate: { sessionTemplate: { id: '1', - label: 'important', - body: 'important template', - example: 'important template', - category: null, - shortcode: 'important template', - isActive: true, - translations: '{}', + body: 'You can now view your Account Balance or Mini statement for Account ending with {{1}} simply by selecting one of the options below.', + label: 'Account Balance', + isHsm: true, + updatedAt: '2024-06-25T12:25:27Z', + translations: + '{"1":{"uuid":"cc584565-8d3a-4d64-838a-4601578189f4","status":"APPROVED","number_parameters":1,"language_id":2,"label":"Account Balance","example":" अब आप नीचे दिए विकल्पों में से एक का चयन करके [003] के साथ समाप्त होने वाले खाते के लिए अपना खाता शेष या मिनी स्टेटमेंट देख सकते हैं। | [अकाउंट बैलेंस देखें] | [देखें मिनी स्टेटमेंट]","body":" अब आप नीचे दिए विकल्पों में से एक का चयन करके {{1}} के साथ समाप्त होने वाले खाते के लिए अपना खाता शेष या मिनी स्टेटमेंट देख सकते हैं। | [अकाउंट बैलेंस देखें] | [देखें मिनी स्टेटमेंट]"}}', type: 'TEXT', - isHsm: false, language: { + __typename: 'Language', id: '1', label: 'English', }, + isActive: true, MessageMedia: null, - hasButtons: false, - buttons: null, + tag: { + id: '1', + label: 'Messages', + }, + category: 'ACCOUNT_UPDATE', + shortcode: 'account_balance', + example: + 'You can now view your Account Balance or Mini statement for Account ending with [003] simply by selecting one of the options below.', + hasButtons: true, + buttons: + '[{"type":"QUICK_REPLY","text":"View Account Balance"},{"type":"QUICK_REPLY","text":"View Mini Statement"}]', + buttonType: 'QUICK_REPLY', + allowTemplateCategoryChange: false, + }, + }, +}; + +const getTemplateDataTypeMedia = { + sessionTemplate: { + sessionTemplate: { + MessageMedia: null, + body: 'Hi {{1}},\n\nYour account image was updated on {{2}} by {{3}} with above. | [Visit Website,https://www.gupshup.io/developer/[message]]', buttonType: null, - updatedAt: '2020-12-01T18:00:32Z', + buttons: '[]', + category: 'UTILITY', + example: + 'Hi [Anil],\n\nYour account image was updated on [19th December] by [Saurav] with above. | [Visit Website,https://www.gupshup.io/developer/[message]]', + hasButtons: false, + id: '5', + isActive: false, + isHsm: true, + label: 'Account Update', + language: { + __typename: 'Language', + id: '1', + label: 'English', + }, + shortcode: 'account_update', tag: null, - allowTemplateCategoryChange: false, + translations: + '{"2":{"number_parameters":3,"language_id":2,"body":"हाय {{1}}, n n आपके खाते की छवि {{2}} पर {{3}} द्वारा अद्यतन की गई थी।"}}', + type: 'IMAGE', + updatedAt: '2024-07-03T08:17:28Z', + allowTemplateCategoryChange: true, }, }, }; @@ -308,7 +347,7 @@ const createHsmWithButtontemplate = { buttons: '[{"type":"QUICK_REPLY","text":"Quick reply 1"},{"type":"QUICK_REPLY","text":"Quick reply 2"}]', buttonType: 'QUICK_REPLY', - allowTemplateCategoryChange: true, + allowTemplateCategoryChange: false, }, errors: null, }, @@ -442,6 +481,178 @@ const createMediaMessage = { }, }; +const getShortCodeQuery = { + request: { + query: GET_SHORTCODES, + variables: { filter: { isHsm: true } }, + }, + result: { + data: { + sessionTemplates: [ + { + __typename: 'SessionTemplate', + shortcode: 'account_balance', + }, + { + __typename: 'SessionTemplate', + shortcode: 'movie_ticket', + }, + { + __typename: 'SessionTemplate', + shortcode: 'movie_ticket', + }, + { + __typename: 'SessionTemplate', + shortcode: 'personalized_bill', + }, + { + __typename: 'SessionTemplate', + shortcode: 'account_update', + }, + { + __typename: 'SessionTemplate', + shortcode: 'bill', + }, + { + __typename: 'SessionTemplate', + shortcode: '', + }, + { + __typename: 'SessionTemplate', + shortcode: '', + }, + { + __typename: 'SessionTemplate', + shortcode: 'otp', + }, + { + __typename: 'SessionTemplate', + shortcode: 'user-registration', + }, + ], + }, + }, +}; + +export const getHSMTemplateTypeText = { + request: { + query: GET_TEMPLATE, + variables: { + id: '1', + }, + }, + result: { + data: getTemplateDataTypeText, + }, +}; + +export const getHSMTemplateTypeMedia = { + request: { + query: GET_TEMPLATE, + variables: { + id: '1', + }, + }, + result: { + data: getTemplateDataTypeMedia, + }, +}; + +export const getSpendSendTemplate = { + request: { + query: GET_TEMPLATE, + variables: { + id: '1', + }, + }, + result: { + data: { + sessionTemplate: { + sessionTemplate: { + MessageMedia: { + __typename: 'MessageMedia', + caption: 'message', + id: '5', + sourceUrl: + 'https://images.ctfassets.net/hrltx12pl8hq/28ECAQiPJZ78hxatLTa7Ts/2f695d869736ae3b0de3e56ceaca3958/free-nature-images.jpg?fit=fill&w=1200&h=630', + }, + __typename: 'SessionTemplate', + allowTemplateCategoryChange: true, + body: 'message', + buttonType: null, + buttons: '[]', + category: null, + example: null, + hasButtons: false, + id: '11', + isActive: true, + isHsm: false, + label: 'title', + language: { + id: '1', + label: 'English', + }, + shortcode: null, + tag: null, + translations: + '{"2":{"status":"approved","languageId":{"localized":true,"locale":"hi","label":"Hindi","id":"2","__typename":"Language"},"label":"hey","isHsm":false,"body":"hindi translations","MessageMedia":null}}', + type: 'IMAGE', + updatedAt: '2024-07-10T09:43:25Z', + }, + }, + }, + }, +}; + +export const updateSessiontemplate = { + request: { + query: UPDATE_TEMPLATE, + variables: { + id: '1', + input: { + translations: + '{"2":{"status":"approved","languageId":{"localized":true,"locale":"hi","label":"Hindi","id":"2","__typename":"Language"},"label":"hey","isHsm":false,"body":"hindi translations","MessageMedia":null},"undefined":{"status":"approved","languageId":null,"label":"title","body":"message","MessageMedia":{"type":"IMAGE","sourceUrl":"https://images.ctfassets.net/hrltx12pl8hq/28ECAQiPJZ78hxatLTa7Ts/2f695d869736ae3b0de3e56ceaca3958/free-nature-images.jpg?fit=fill&w=1200&h=630"},"isHsm":false}}', + }, + }, + }, + result: { + data: { + updateSessionTemplate: { + __typename: 'SessionTemplateResult', + sessionTemplate: { + MessageMedia: { + __typename: 'MessageMedia', + caption: 'message', + id: '5', + sourceUrl: + 'https://images.ctfassets.net/hrltx12pl8hq/28ECAQiPJZ78hxatLTa7Ts/2f695d869736ae3b0de3e56ceaca3958/free-nature-images.jpg?fit=fill&w=1200&h=630', + }, + __typename: 'SessionTemplate', + allowTemplateCategoryChange: true, + body: 'message', + buttonType: null, + buttons: '[]', + category: null, + example: null, + hasButtons: false, + id: '11', + isActive: true, + label: 'title', + language: { + __typename: 'Language', + id: '1', + label: 'English', + }, + shortcode: null, + translations: + '{"2":{"status":"approved","languageId":{"localized":true,"locale":"hi","label":"Hindi","id":"2","__typename":"Language"},"label":"hey","isHsm":false,"body":"hindi translations","MessageMedia":null}}', + type: 'IMAGE', + }, + }, + }, + }, +}; + export const TEMPLATE_MOCKS = [ getFilterTagQuery, createHSMtemplate, @@ -449,6 +660,7 @@ export const TEMPLATE_MOCKS = [ createHsmWithButtontemplate, createHsmWithPhonetemplate, createMediaMessage, + getShortCodeQuery, { request: { query: CREATE_TEMPLATE, @@ -490,28 +702,6 @@ export const TEMPLATE_MOCKS = [ }, }, }, - { - request: { - query: GET_TEMPLATE, - variables: { - id: '1', - }, - }, - result: { - data: getTemplateData, - }, - }, - { - request: { - query: GET_TEMPLATE, - variables: { - id: '1', - }, - }, - result: { - data: getTemplateData, - }, - }, { request: { query: DELETE_TEMPLATE, @@ -574,6 +764,10 @@ export const TEMPLATE_MOCKS = [ ...whatsappHsmCategories, speedSendOrderWith, speedSendOrderWith, + getSpendSendTemplate, + updateSessiontemplate, + getOrganizationLanguagesQuery, + getOrganizationLanguagesQuery, ]; const getHSMTemplate = (id: string, status: string) => ({ @@ -594,7 +788,6 @@ const getHSMTemplate = (id: string, status: string) => ({ type: 'TEXT', numberParameters: 2, updatedAt: '2021-07-28T08:00:24Z', - allowTemplateCategoryChange: true, }); export const HSM_LIST = [ @@ -681,3 +874,37 @@ export const HSM_LIST = [ }, }, ]; + +const getTemplatesCount = { + request: { + query: GET_TEMPLATES_COUNT, + variables: { filter: { isHsm: false } }, + }, + result: { + data: { + countSessionTemplates: 3, + }, + }, +}; + +export const SPEED_SENDS_MOCKS = [ + getSpendSendTemplate, + getSpendSendTemplate, + getSpendSendTemplate, + speedSendOrderWith, + speedSendOrderWith, + speedSend, + speedSend, + filterTemplateQuery, + getTemplatesCount, + getTemplatesCount, + updateSessiontemplate, + getOrganizationLanguagesQuery, + getOrganizationLanguagesQueryByOrder, + getOrganizationLanguagesQueryByOrder, + getOrganizationLanguagesQueryByOrder, + getFilterTagQuery, + getFilterTagQuery, + getFilterTagQuery, + updateSessiontemplate, +]; diff --git a/src/containers/Template/TemplateVariables/TemplateVariable.module.css b/src/containers/Template/TemplateVariables/TemplateVariable.module.css new file mode 100644 index 000000000..e38ea67a3 --- /dev/null +++ b/src/containers/Template/TemplateVariables/TemplateVariable.module.css @@ -0,0 +1,59 @@ +.AddVariablesContainer { + margin: 1rem 0; +} + +.AddVariable { + display: flex; + column-gap: 0.8rem; + border-radius: 8px; +} + +.AddVariablesContainer h2 { + font-size: 1rem; + color: #555555; +} + +.Variables { + display: flex; + flex-direction: column; + row-gap: 0.5rem; + width: 100%; +} + +.VariableContainer { + display: flex; + align-items: center; + column-gap: 12px; +} + +.Variable { + display: flex; + flex-direction: column; + align-items: stretch; + border-radius: 4px; + font-size: 14px; + width: 100%; +} + +.VariableNumber { + font-size: 14.5px; + padding-right: 14px; + border-right: 2px solid #999999; + height: 100%; + display: flex; + align-items: center; + justify-items: center; +} + +.DangerText { + margin-left: 14px; + font-size: 12px; + margin-top: 4px; + line-height: 18px; + font-weight: 400; + color: #fb5c5c; +} + +.DeleteIcon { + cursor: pointer; +} diff --git a/src/containers/Template/TemplateVariables/TemplateVariables.tsx b/src/containers/Template/TemplateVariables/TemplateVariables.tsx new file mode 100644 index 000000000..4b128d65c --- /dev/null +++ b/src/containers/Template/TemplateVariables/TemplateVariables.tsx @@ -0,0 +1,108 @@ +import { Button } from 'components/UI/Form/Button/Button'; +import AddIcon from 'assets/images/AddGreenIcon.svg?react'; +import styles from './TemplateVariable.module.css'; +import { FormHelperText, OutlinedInput } from '@mui/material'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { setDefaultValue } from 'common/RichEditor'; +import DeleteIcon from 'assets/images/icons/CrossIcon.svg?react'; +import { useEffect } from 'react'; + +export interface TemplateOptionsProps { + form: { touched: any; errors: any; values: any; setFieldValue: any }; + message: any; + variables: Array; + setVariables: any; + getVariables: any; + isEditing: boolean; +} + +export const TemplateVariables = ({ + form: { touched, errors }, + message, + variables, + setVariables, + getVariables, + isEditing, +}: TemplateOptionsProps) => { + const [editor] = useLexicalComposerContext(); + + const handleAddVariable = () => { + setVariables([...variables, { text: '', id: variables.length + 1 }]); + setDefaultValue(editor, `${message?.trim(' ')} {{${variables.length + 1}}}`); + editor.focus(); + }; + + const handleRemoveVariable = (id: number) => { + const regex = new RegExp(`\\{\\{${id}\\}\\}`, 'g'); + + const updatedMessage = message.replace(regex, '').trim(); + setDefaultValue(editor, updatedMessage); + + const newVariables = variables.filter((variable) => variable.id !== id); + setVariables(newVariables); + }; + + useEffect(() => { + setVariables(getVariables(message)); + }, [message]); + + return ( +
+ +
+
+ {variables.length !== 0 &&

Set custom variable values for the message

} + {variables.map((variable: any, index: number) => ( +
+
+ {`{{${variable.id}}}`}
+ } + fullWidth + label="Name" + placeholder={'Define value '} + notched={false} + disabled={isEditing} + defaultValue={isEditing ? variable.text : ''} + onChange={(event) => { + let currentVariable = variables.find((v) => v.id === variable.id); + currentVariable.text = event.target.value; + setVariables( + variables.map((v) => (v.id === variable.id ? currentVariable : v)) + ); + }} + /> + + {errors.variables && touched.variables && touched.variables[index] ? ( + + {errors.variables[index]?.text} + + ) : null} +
+ handleRemoveVariable(variable.id)} + data-testid="delete-variable" + /> +
+ ))} +
+
+ + ); +}; diff --git a/src/containers/Ticket/TicketList/ExportTicket/ExportTicket.test.tsx b/src/containers/Ticket/TicketList/ExportTicket/ExportTicket.test.tsx index f9dbbffe0..978f483f4 100644 --- a/src/containers/Ticket/TicketList/ExportTicket/ExportTicket.test.tsx +++ b/src/containers/Ticket/TicketList/ExportTicket/ExportTicket.test.tsx @@ -8,6 +8,10 @@ import { getAllOrganizations } from 'mocks/Organization'; import * as utils from 'common/utils'; import { exportTicketsMock } from 'mocks/Ticket'; +afterEach(() => { + (window as any).matchMedia = null; +}); + const mocks = [...getAllOrganizations, exportTicketsMock]; setUserSession(JSON.stringify({ organization: { id: '1' }, roles: ['Admin'] })); diff --git a/src/containers/WaGroups/GroupChatInterface/GroupInterface.test.tsx b/src/containers/WaGroups/GroupChatInterface/GroupInterface.test.tsx index 35a0ab68e..a63f2fb3c 100644 --- a/src/containers/WaGroups/GroupChatInterface/GroupInterface.test.tsx +++ b/src/containers/WaGroups/GroupChatInterface/GroupInterface.test.tsx @@ -1,4 +1,3 @@ -import 'mocks/matchMediaMock'; import { MemoryRouter } from 'react-router-dom'; import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'; import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'; diff --git a/src/graphql/queries/Template.ts b/src/graphql/queries/Template.ts index aa293dd61..94b3f7fc0 100644 --- a/src/graphql/queries/Template.ts +++ b/src/graphql/queries/Template.ts @@ -81,3 +81,10 @@ export const GET_HSM_CATEGORIES = gql` whatsappHsmCategories } `; +export const GET_SHORTCODES = gql` + query sessionTemplates($filter: SessionTemplateFilter!) { + sessionTemplates(filter: $filter) { + shortcode + } + } +`; diff --git a/src/mocks/InteractiveMessage.tsx b/src/mocks/InteractiveMessage.tsx index 9bef88bc5..554cda637 100644 --- a/src/mocks/InteractiveMessage.tsx +++ b/src/mocks/InteractiveMessage.tsx @@ -146,7 +146,7 @@ export const getInteractiveCountQuery = { const quickReplyMock = { id: '1', interactiveContent: - '{"type":"quick_reply","options":[{"type":"text","title":"Correct"},{"type":"text","title":"Re-enter details"}],"content":{"type":"text","text":"Please *confirm* if the below details are correct-\\n\\n*Name:* @results.name\\n*Profile of:* @results.role","header":"Details Confirmation"}}', + '{"type":"quick_reply","options":[{"type":"text","title":"Correct"},{"type":"text","title":"Re-enter details"}],"content":{"type":"text","text":"Please *confirm* if the below details are correct-\\n\\n*Name:* results.name\\n*Profile of:* results.role","header":"Details Confirmation"}}', label: 'Details Confirmation', language: { __typename: 'Language', @@ -156,29 +156,29 @@ const quickReplyMock = { sendWithTitle: true, tag: null, translations: - '{"2":{"type":"quick_reply","options":[{"type":"text","title":"सही"},{"type":"text","title":"विवरण प"}],"content":{"type":"text","text":"कृपया पुष्टि करें कि नीचे दी गई जानकारी सही है या नहीं-\\n\\n*नाम:* @results.name\\n*प्रोफ़ाइल:* @results.role","header":"विवरण पुष्टि"}},"1":{"type":"quick_reply","options":[{"type":"text","title":"Correct"},{"type":"text","title":"Re-enter details"}],"content":{"type":"text","text":"Please *confirm* if the below details are correct-\\n\\n*Name:* @results.name\\n*Profile of:* @results.role","header":"Details Confirmation"}}}', + '{"2":{"type":"quick_reply","options":[{"type":"text","title":"सही"},{"type":"text","title":"विवरण प"}],"content":{"type":"text","text":"कृपया पुष्टि करें कि नीचे दी गई जानकारी सही है या नहीं-\\n\\n*नाम:* results.name\\n*प्रोफ़ाइल:* results.role","header":"विवरण पुष्टि"}},"1":{"type":"quick_reply","options":[{"type":"text","title":"Correct"},{"type":"text","title":"Re-enter details"}],"content":{"type":"text","text":"Please *confirm* if the below details are correct-\\n\\n*Name:* results.name\\n*Profile of:* results.role","header":"Details Confirmation"}}}', type: 'QUICK_REPLY', }; const quickReplyMockInput = { type: 'QUICK_REPLY', interactiveContent: - '{"type":"quick_reply","content":{"type":"text","header":"Details Confirmation","text":"Please *confirm* if the below details are correct-\\n\\n*Name:* @results.name\\n*Profile of:* @results.role"},"options":[{"type":"text","title":"Correct"},{"type":"text","title":"Re-enter details"}]}', + '{"type":"quick_reply","content":{"type":"text","header":"Details Confirmation","text":"Please *confirm* if the below details are correct-\\n\\n*Name:* results.name\\n*Profile of:* results.role"},"options":[{"type":"text","title":"Correct"},{"type":"text","title":"Re-enter details"}]}', languageId: '1', label: 'Details Confirmation', sendWithTitle: true, translations: - '{"1":{"type":"quick_reply","content":{"type":"text","header":"Details Confirmation","text":"Please *confirm* if the below details are correct-\\n\\n*Name:* @results.name\\n*Profile of:* @results.role"},"options":[{"type":"text","title":"Correct"},{"type":"text","title":"Re-enter details"}]},"2":{"type":"quick_reply","options":[{"type":"text","title":"सही"},{"type":"text","title":"विवरण प"}],"content":{"type":"text","text":"कृपया पुष्टि करें कि नीचे दी गई जानकारी सही है या नहीं-\\n\\n*नाम:* @results.name\\n*प्रोफ़ाइल:* @results.role","header":"विवरण पुष्टि"}}}', + '{"1":{"type":"quick_reply","content":{"type":"text","header":"Details Confirmation","text":"Please *confirm* if the below details are correct-\\n\\n*Name:* results.name\\n*Profile of:* results.role"},"options":[{"type":"text","title":"Correct"},{"type":"text","title":"Re-enter details"}]},"2":{"type":"quick_reply","options":[{"type":"text","title":"सही"},{"type":"text","title":"विवरण प"}],"content":{"type":"text","text":"कृपया पुष्टि करें कि नीचे दी गई जानकारी सही है या नहीं-\\n\\n*नाम:* results.name\\n*प्रोफ़ाइल:* results.role","header":"विवरण पुष्टि"}}}', }; const quickReplyMockInput2 = { type: 'QUICK_REPLY', interactiveContent: - '{"type":"quick_reply","options":[{"type":"text","title":"Correct"},{"type":"text","title":"Re-enter details"}],"content":{"type":"text","text":"Please *confirm* if the below details are correct-\\n\\n*Name:* @results.name\\n*Profile of:* @results.role","header":"Details Confirmation"}}', + '{"type":"quick_reply","options":[{"type":"text","title":"Correct"},{"type":"text","title":"Re-enter details"}],"content":{"type":"text","text":"Please *confirm* if the below details are correct-\\n\\n*Name:* results.name\\n*Profile of:* results.role","header":"Details Confirmation"}}', languageId: '1', sendWithTitle: true, translations: - '{"1":{"type":"quick_reply","content":{"type":"text","header":"Details Confirmation","text":"Please *confirm* if the below details are correct-\\n\\n*Name:* @results.name\\n*Profile of:* @results.role"},"options":[{"type":"text","title":"Correct"},{"type":"text","title":"Re-enter details"}]},"2":{"type":"quick_reply","content":{"type":"text","header":"विवरण पुष्टि","text":"कृपया पुष्टि करें कि नीचे दी गई जानकारी सही है या नहीं-\\n\\n*नाम:* @results.name\\n*प्रोफ़ाइल:* @results.role"},"options":[{"type":"text","title":"सही"},{"type":"text","title":"विवरण प"}]}}', + '{"1":{"type":"quick_reply","content":{"type":"text","header":"Details Confirmation","text":"Please *confirm* if the below details are correct-\\n\\n*Name:* results.name\\n*Profile of:* results.role"},"options":[{"type":"text","title":"Correct"},{"type":"text","title":"Re-enter details"}]},"2":{"type":"quick_reply","content":{"type":"text","header":"विवरण पुष्टि","text":"कृपया पुष्टि करें कि नीचे दी गई जानकारी सही है या नहीं-\\n\\n*नाम:* results.name\\n*प्रोफ़ाइल:* results.role"},"options":[{"type":"text","title":"सही"},{"type":"text","title":"विवरण प"}]}}', }; const quickReplyMedia = { @@ -244,12 +244,13 @@ const createInteractiveCustomMock = () => ({ input: { type: 'LIST', interactiveContent: - '{"type":"list","title":"new title","body":"😀","globalButtons":[{"type":"text","title":"Section 1"}],"items":[{"title":"title","subtitle":"title","options":[{"type":"text","title":"red","description":"red is color"}]}]}', + '{"type":"list","title":"new title","body":"😀\\n\\n","globalButtons":[{"type":"text","title":"Section 1"}],"items":[{"title":"title","subtitle":"title","options":[{"type":"text","title":"red","description":"red is color"}]}]}', languageId: '2', label: 'new title', sendWithTitle: true, + tag: null, translations: - '{"2":{"type":"list","title":"new title","body":"😀","globalButtons":[{"type":"text","title":"Section 1"}],"items":[{"title":"title","subtitle":"title","options":[{"type":"text","title":"red","description":"red is color"}]}]}}', + '{"2":{"type":"list","title":"new title","body":"😀\\n\\n","globalButtons":[{"type":"text","title":"Section 1"}],"items":[{"title":"title","subtitle":"title","options":[{"type":"text","title":"red","description":"red is color"}]}]}}', }, }, }, diff --git a/src/routes/AuthenticatedRoute/AuthenticatedRoute.test.tsx b/src/routes/AuthenticatedRoute/AuthenticatedRoute.test.tsx index 06076bd7b..f41e173d5 100644 --- a/src/routes/AuthenticatedRoute/AuthenticatedRoute.test.tsx +++ b/src/routes/AuthenticatedRoute/AuthenticatedRoute.test.tsx @@ -1,4 +1,3 @@ -import 'mocks/matchMediaMock'; import { Suspense } from 'react'; import { render, waitFor } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; diff --git a/src/setupTests.ts b/src/setupTests.ts index 6c6bf31cf..bd6c67bea 100644 --- a/src/setupTests.ts +++ b/src/setupTests.ts @@ -1,3 +1,4 @@ +import 'mocks/matchMediaMock'; import { vi } from 'vitest'; import { TextEncoder, TextDecoder } from 'util'; import '@testing-library/jest-dom/vitest';