@capillarytech/creatives-library 8.0.327 → 8.0.329

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/package.json +1 -1
  2. package/services/api.js +17 -0
  3. package/services/tests/api.test.js +85 -0
  4. package/utils/commonUtils.js +10 -0
  5. package/utils/tagValidations.js +2 -3
  6. package/utils/tests/commonUtil.test.js +169 -0
  7. package/utils/tests/tagValidations.test.js +1 -35
  8. package/v2Components/CapTagList/index.js +22 -14
  9. package/v2Components/CapTagList/style.scss +0 -48
  10. package/v2Components/CapTagListWithInput/index.js +0 -4
  11. package/v2Components/CapWhatsappCTA/index.js +0 -2
  12. package/v2Components/CommonTestAndPreview/AddTestCustomer.js +42 -0
  13. package/v2Components/CommonTestAndPreview/CustomerCreationModal.js +155 -0
  14. package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +93 -0
  15. package/v2Components/CommonTestAndPreview/SendTestMessage.js +79 -51
  16. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +134 -34
  17. package/v2Components/CommonTestAndPreview/actions.js +10 -0
  18. package/v2Components/CommonTestAndPreview/constants.js +15 -1
  19. package/v2Components/CommonTestAndPreview/index.js +315 -15
  20. package/v2Components/CommonTestAndPreview/messages.js +106 -0
  21. package/v2Components/CommonTestAndPreview/reducer.js +10 -0
  22. package/v2Components/CommonTestAndPreview/tests/AddTestCustomer.test.js +66 -0
  23. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +648 -0
  24. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +24 -0
  25. package/v2Components/CommonTestAndPreview/tests/CustomerCreationModal.test.js +174 -0
  26. package/v2Components/CommonTestAndPreview/tests/ExistingCustomerModal.test.js +114 -0
  27. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +52 -0
  28. package/v2Components/CommonTestAndPreview/tests/constants.test.js +31 -1
  29. package/v2Components/CommonTestAndPreview/tests/index.test.js +36 -0
  30. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +71 -0
  31. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +17 -0
  32. package/v2Components/FormBuilder/index.js +0 -7
  33. package/v2Components/HtmlEditor/HTMLEditor.js +1 -6
  34. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -1
  35. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +2 -927
  36. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +0 -3
  37. package/v2Containers/BeeEditor/index.js +0 -3
  38. package/v2Containers/CreativesContainer/SlideBoxContent.js +1 -28
  39. package/v2Containers/CreativesContainer/index.js +0 -3
  40. package/v2Containers/Email/index.js +0 -1
  41. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1 -7
  42. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +0 -3
  43. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +2 -20
  44. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +1 -16
  45. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +0 -3
  46. package/v2Containers/EmailWrapper/index.js +0 -4
  47. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +0 -1
  48. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +0 -9
  49. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +0 -19
  50. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +0 -3
  51. package/v2Containers/InAppWrapper/index.js +0 -3
  52. package/v2Containers/MobilePush/Create/index.js +0 -2
  53. package/v2Containers/MobilePush/Edit/index.js +0 -2
  54. package/v2Containers/MobilepushWrapper/index.js +1 -3
  55. package/v2Containers/Rcs/index.js +0 -1
  56. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +1886 -1754
  57. package/v2Containers/Sms/Create/index.js +0 -2
  58. package/v2Containers/Sms/Edit/index.js +0 -2
  59. package/v2Containers/SmsTrai/Edit/index.js +0 -2
  60. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +351 -318
  61. package/v2Containers/SmsWrapper/index.js +0 -2
  62. package/v2Containers/TagList/index.js +2 -41
  63. package/v2Containers/TagList/messages.js +0 -4
  64. package/v2Containers/TagList/tests/TagList.test.js +20 -122
  65. package/v2Containers/TagList/tests/mockdata.js +0 -17
  66. package/v2Containers/Viber/index.js +0 -5
  67. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +2 -0
  68. package/v2Containers/WebPush/Create/index.js +1 -9
  69. package/v2Containers/Whatsapp/index.js +0 -5
  70. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +5586 -5232
  71. package/v2Containers/Zalo/index.js +0 -2
  72. package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +0 -63
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Unit tests for CustomerCreationModal
3
+ *
4
+ * Email/mobile are read-only; lookup runs in parent (index.js) before the modal opens.
5
+ */
6
+
7
+ import React from 'react';
8
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
9
+ import { IntlProvider } from 'react-intl';
10
+ import PropTypes from 'prop-types';
11
+ import CustomerCreationModal from '../CustomerCreationModal';
12
+ import { CHANNELS, CUSTOMER_MODAL_NEW } from '../constants';
13
+
14
+ const mockMessages = {
15
+ 'app.v2Components.TestAndPreviewSlidebox.customerCreationModalTitle': 'Add new test customer',
16
+ 'app.v2Components.TestAndPreviewSlidebox.customerCreationModalDescription': 'This customer profile will be available for testing.',
17
+ 'app.v2Components.TestAndPreviewSlidebox.customerName': 'Name',
18
+ 'app.v2Components.TestAndPreviewSlidebox.customerNameOptional': '(Optional)',
19
+ 'app.v2Components.TestAndPreviewSlidebox.customerNamePlaceholder': 'Enter the name',
20
+ 'app.v2Components.TestAndPreviewSlidebox.customerEmailPlaceholder': 'Enter the Email',
21
+ 'app.v2Components.TestAndPreviewSlidebox.customerMobilePlaceholder': 'Enter the Mobile Number',
22
+ 'app.v2Components.TestAndPreviewSlidebox.saveButton': 'Save',
23
+ 'app.v2Components.TestAndPreviewSlidebox.cancelButton': 'Cancel',
24
+ 'app.v2Components.TestAndPreviewSlidebox.customerEmail': 'Email',
25
+ 'app.v2Components.TestAndPreviewSlidebox.customerMobileNumber': 'Mobile Number',
26
+ };
27
+
28
+ const TestWrapper = ({ children }) => (
29
+ <IntlProvider locale="en" messages={mockMessages}>
30
+ {children}
31
+ </IntlProvider>
32
+ );
33
+ TestWrapper.propTypes = { children: PropTypes.node };
34
+
35
+ describe('CustomerCreationModal', () => {
36
+ const defaultProps = {
37
+ customerModal: [true, CUSTOMER_MODAL_NEW],
38
+ onCloseCustomerModal: jest.fn(),
39
+ channel: CHANNELS.EMAIL,
40
+ customerData: { name: '', email: '', mobile: '' },
41
+ setCustomerData: jest.fn(),
42
+ onSave: jest.fn(),
43
+ };
44
+
45
+ beforeEach(() => {
46
+ jest.clearAllMocks();
47
+ });
48
+
49
+ it('renders modal with title and form fields when visible (EMAIL)', () => {
50
+ render(
51
+ <TestWrapper>
52
+ <CustomerCreationModal {...defaultProps} channel={CHANNELS.EMAIL} />
53
+ </TestWrapper>
54
+ );
55
+ expect(screen.getByText(/add new test customer/i)).toBeTruthy();
56
+ expect(screen.getByPlaceholderText(/enter the name/i)).toBeTruthy();
57
+ expect(screen.getByPlaceholderText(/enter the email/i)).toBeTruthy();
58
+ expect(screen.queryByPlaceholderText(/enter the mobile number/i)).toBeNull();
59
+ expect(screen.getByRole('button', { name: /save/i })).toBeTruthy();
60
+ expect(screen.getByRole('button', { name: /cancel/i })).toBeTruthy();
61
+ });
62
+
63
+ it('renders mobile field when channel is SMS', () => {
64
+ render(
65
+ <TestWrapper>
66
+ <CustomerCreationModal
67
+ {...defaultProps}
68
+ channel={CHANNELS.SMS}
69
+ customerData={{ name: '', email: '', mobile: '' }}
70
+ />
71
+ </TestWrapper>
72
+ );
73
+ expect(screen.getByPlaceholderText(/enter the mobile number/i)).toBeTruthy();
74
+ expect(screen.queryByPlaceholderText(/enter the email/i)).toBeNull();
75
+ });
76
+
77
+ it('calls onCloseCustomerModal when Cancel is clicked', () => {
78
+ render(
79
+ <TestWrapper>
80
+ <CustomerCreationModal {...defaultProps} />
81
+ </TestWrapper>
82
+ );
83
+ fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
84
+ expect(defaultProps.onCloseCustomerModal).toHaveBeenCalled();
85
+ });
86
+
87
+ it('calls setCustomerData when name input changes', () => {
88
+ render(
89
+ <TestWrapper>
90
+ <CustomerCreationModal {...defaultProps} />
91
+ </TestWrapper>
92
+ );
93
+ fireEvent.change(screen.getByPlaceholderText(/enter the name/i), { target: { value: 'John' } });
94
+ expect(defaultProps.setCustomerData).toHaveBeenCalledWith(expect.objectContaining({ name: 'John' }));
95
+ });
96
+
97
+ it('calls onSave when Save is clicked with empty validation object', async () => {
98
+ render(
99
+ <TestWrapper>
100
+ <CustomerCreationModal {...defaultProps} customerData={{ name: '', email: 'a@b.co', mobile: '' }} />
101
+ </TestWrapper>
102
+ );
103
+ const saveBtn = screen.getByRole('button', { name: /save/i });
104
+ fireEvent.click(saveBtn);
105
+ await waitFor(() => expect(defaultProps.onSave).toHaveBeenCalled());
106
+ expect(defaultProps.onSave).toHaveBeenCalledWith(
107
+ { email: '', mobile: '' },
108
+ expect.any(Function),
109
+ );
110
+ });
111
+
112
+ it('disables Save when required email is missing (EMAIL channel)', () => {
113
+ render(
114
+ <TestWrapper>
115
+ <CustomerCreationModal
116
+ {...defaultProps}
117
+ channel={CHANNELS.EMAIL}
118
+ customerData={{ name: '', email: '', mobile: '' }}
119
+ />
120
+ </TestWrapper>
121
+ );
122
+ const saveBtn = screen.getByRole('button', { name: /save/i });
123
+ expect(saveBtn.disabled).toBe(true);
124
+ });
125
+
126
+ it('disables Save when required mobile is missing (SMS channel)', () => {
127
+ render(
128
+ <TestWrapper>
129
+ <CustomerCreationModal
130
+ {...defaultProps}
131
+ channel={CHANNELS.SMS}
132
+ customerData={{ name: '', email: '', mobile: '' }}
133
+ />
134
+ </TestWrapper>
135
+ );
136
+ const saveBtn = screen.getByRole('button', { name: /save/i });
137
+ expect(saveBtn.disabled).toBe(true);
138
+ });
139
+
140
+ it('shows (Optional) on name label when channel is SMS', () => {
141
+ render(
142
+ <TestWrapper>
143
+ <CustomerCreationModal
144
+ {...defaultProps}
145
+ channel={CHANNELS.SMS}
146
+ customerData={{ name: '', email: '', mobile: '' }}
147
+ />
148
+ </TestWrapper>
149
+ );
150
+ expect(screen.getAllByText(/\(optional\)/i)).toHaveLength(1);
151
+ });
152
+
153
+ it('shows (Optional) on name label when channel is EMAIL', () => {
154
+ render(
155
+ <TestWrapper>
156
+ <CustomerCreationModal
157
+ {...defaultProps}
158
+ channel={CHANNELS.EMAIL}
159
+ customerData={{ name: '', email: '', mobile: '' }}
160
+ />
161
+ </TestWrapper>
162
+ );
163
+ expect(screen.getAllByText(/\(optional\)/i)).toHaveLength(1);
164
+ });
165
+
166
+ it('renders modal description', () => {
167
+ render(
168
+ <TestWrapper>
169
+ <CustomerCreationModal {...defaultProps} />
170
+ </TestWrapper>
171
+ );
172
+ expect(screen.getByText(/this customer profile will be available for testing/i)).toBeTruthy();
173
+ });
174
+ });
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Unit tests for ExistingCustomerModal
3
+ */
4
+
5
+ import React from 'react';
6
+ import { render, screen, fireEvent } from '@testing-library/react';
7
+ import { IntlProvider } from 'react-intl';
8
+ import PropTypes from 'prop-types';
9
+ import ExistingCustomerModal from '../ExistingCustomerModal';
10
+ import { CHANNELS, CUSTOMER_MODAL_EXISTING } from '../constants';
11
+
12
+ const mockMessages = {
13
+ 'app.v2Components.TestAndPreviewSlidebox.customerCreationModalTitle': 'Add new test customer',
14
+ 'app.v2Components.TestAndPreviewSlidebox.existingCustomerModalDescription': 'This user profile already exists in the system.',
15
+ 'app.v2Components.TestAndPreviewSlidebox.saveButton': 'Save',
16
+ 'app.v2Components.TestAndPreviewSlidebox.cancelButton': 'Cancel',
17
+ 'app.v2Components.TestAndPreviewSlidebox.customerEmail': 'Email',
18
+ 'app.v2Components.TestAndPreviewSlidebox.customerMobileNumber': 'Mobile Number',
19
+ 'app.v2Components.TestAndPreviewSlidebox.customerID': 'Customer ID',
20
+ };
21
+
22
+ const TestWrapper = ({ children }) => (
23
+ <IntlProvider locale="en" messages={mockMessages}>
24
+ {children}
25
+ </IntlProvider>
26
+ );
27
+ TestWrapper.propTypes = { children: PropTypes.node };
28
+
29
+ describe('ExistingCustomerModal', () => {
30
+ const defaultProps = {
31
+ customerModal: [true, CUSTOMER_MODAL_EXISTING],
32
+ onCloseCustomerModal: jest.fn(),
33
+ customerData: { name: 'John Doe', email: 'john@example.com', mobile: '', customerId: 'cust-123' },
34
+ channel: CHANNELS.EMAIL,
35
+ onSave: jest.fn(),
36
+ };
37
+
38
+ beforeEach(() => {
39
+ jest.clearAllMocks();
40
+ });
41
+
42
+ it('renders modal with customer details when visible', () => {
43
+ render(
44
+ <TestWrapper>
45
+ <ExistingCustomerModal {...defaultProps} />
46
+ </TestWrapper>
47
+ );
48
+ expect(screen.getByText(/add new test customer/i)).toBeTruthy();
49
+ expect(screen.getByText(/this user profile already exists/i)).toBeTruthy();
50
+ expect(screen.getByText('John Doe')).toBeTruthy();
51
+ expect(screen.getByText(/john@example.com/)).toBeTruthy();
52
+ expect(screen.getByText(/cust-123/)).toBeTruthy();
53
+ expect(screen.getByRole('button', { name: /save/i })).toBeTruthy();
54
+ expect(screen.getByRole('button', { name: /cancel/i })).toBeTruthy();
55
+ });
56
+
57
+ it('shows mobile when channel is SMS', () => {
58
+ render(
59
+ <TestWrapper>
60
+ <ExistingCustomerModal
61
+ {...defaultProps}
62
+ channel={CHANNELS.SMS}
63
+ customerData={{ name: 'Jane', email: '', mobile: '9123456789', customerId: 'cust-456' }}
64
+ />
65
+ </TestWrapper>
66
+ );
67
+ expect(screen.getByText(/9123456789/)).toBeTruthy();
68
+ });
69
+
70
+ it('calls onCloseCustomerModal when Cancel is clicked', () => {
71
+ render(
72
+ <TestWrapper>
73
+ <ExistingCustomerModal {...defaultProps} />
74
+ </TestWrapper>
75
+ );
76
+ fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
77
+ expect(defaultProps.onCloseCustomerModal).toHaveBeenCalled();
78
+ });
79
+
80
+ it('calls onSave when Save is clicked', () => {
81
+ render(
82
+ <TestWrapper>
83
+ <ExistingCustomerModal {...defaultProps} />
84
+ </TestWrapper>
85
+ );
86
+ fireEvent.click(screen.getByRole('button', { name: /save/i }));
87
+ expect(defaultProps.onSave).toHaveBeenCalled();
88
+ expect(defaultProps.onSave.mock.calls[0][0]).toEqual({});
89
+ expect(typeof defaultProps.onSave.mock.calls[0][1]).toBe('function');
90
+ });
91
+
92
+ it('shows dash for missing name', () => {
93
+ render(
94
+ <TestWrapper>
95
+ <ExistingCustomerModal {...defaultProps} customerData={{ name: '', email: 'a@b.co', mobile: '', customerId: 'c1' }} />
96
+ </TestWrapper>
97
+ );
98
+ expect(screen.getByText('-')).toBeTruthy();
99
+ });
100
+
101
+ it('does not show email row when channel is EMAIL but customerData.email is empty', () => {
102
+ render(
103
+ <TestWrapper>
104
+ <ExistingCustomerModal
105
+ {...defaultProps}
106
+ customerData={{ name: 'No Email', email: '', mobile: '', customerId: 'c2' }}
107
+ />
108
+ </TestWrapper>
109
+ );
110
+ expect(screen.getByText('No Email')).toBeTruthy();
111
+ expect(screen.getByText('Customer ID')).toBeTruthy();
112
+ expect(screen.getByText('c2')).toBeTruthy();
113
+ });
114
+ });
@@ -138,6 +138,9 @@ describe('SendTestMessage', () => {
138
138
  smsTraiDltEnabled: false,
139
139
  registeredSenderIds: [],
140
140
  isChannelSmsFallbackPreviewEnabled: false,
141
+ renderAddTestCustomerButton: jest.fn(() => null),
142
+ searchValue: '',
143
+ setSearchValue: jest.fn(),
141
144
  };
142
145
 
143
146
  beforeEach(() => {
@@ -654,4 +657,53 @@ describe('SendTestMessage', () => {
654
657
  consoleSpy.mockRestore();
655
658
  });
656
659
  });
660
+
661
+ describe('notFoundContent (Add as test customer)', () => {
662
+ it('should call renderAddTestCustomerButton and use result as notFoundContent', () => {
663
+ const renderAddTestCustomerButton = jest.fn(() => (
664
+ <button type="button" data-testid="add-test-customer-btn">
665
+ Add as test customer
666
+ </button>
667
+ ));
668
+ const props = {
669
+ ...defaultProps,
670
+ renderAddTestCustomerButton,
671
+ };
672
+ render(
673
+ <TestWrapper>
674
+ <SendTestMessage {...props} />
675
+ </TestWrapper>
676
+ );
677
+ expect(renderAddTestCustomerButton).toHaveBeenCalled();
678
+ });
679
+
680
+ it('should render notFoundContent as a clickable button when provided', () => {
681
+ const onAddClick = jest.fn();
682
+ const renderAddTestCustomerButton = jest.fn(() => (
683
+ <button type="button" data-testid="add-test-customer-btn" onClick={onAddClick}>
684
+ Add as test customer
685
+ </button>
686
+ ));
687
+ const props = {
688
+ ...defaultProps,
689
+ renderAddTestCustomerButton,
690
+ };
691
+ const { container } = render(
692
+ <TestWrapper>
693
+ <SendTestMessage {...props} />
694
+ </TestWrapper>
695
+ );
696
+ // notFoundContent is passed to TreeSelect and shown when dropdown is open with no match.
697
+ // We assert the render function returns a button that can be clicked.
698
+ const notFoundContent = renderAddTestCustomerButton.mock.results[0]?.value;
699
+ expect(notFoundContent).toBeTruthy();
700
+ expect(notFoundContent.props['data-testid']).toBe('add-test-customer-btn');
701
+ expect(notFoundContent.type).toBe('button');
702
+ // Simulate click on the returned element's onClick
703
+ if (notFoundContent.props.onClick) {
704
+ notFoundContent.props.onClick();
705
+ expect(onAddClick).toHaveBeenCalled();
706
+ }
707
+ });
708
+ });
657
709
  });
@@ -54,8 +54,13 @@ import {
54
54
  // API Channel Constants
55
55
  API_CHANNEL_PUSH,
56
56
  // Identifier Type Constants
57
- IDENTIFIER_TYPE_MOBILE,
58
57
  IDENTIFIER_TYPE_EMAIL,
58
+ IDENTIFIER_TYPE_MOBILE,
59
+ IDENTIFIER_TYPE_PHONE,
60
+ INPUT_HAS_ERROR_CLASS,
61
+ // Validation Regex
62
+ EMAIL_REGEX,
63
+ PHONE_REGEX,
59
64
  // Channel Name Constants
60
65
  CHANNEL_NAME_INAPP,
61
66
  // Channel Mapping Constants
@@ -206,6 +211,31 @@ describe('CommonTestAndPreview Constants', () => {
206
211
  });
207
212
  });
208
213
 
214
+ describe('Identifier Type Constants', () => {
215
+ it('should export identifier type constants', () => {
216
+ expect(IDENTIFIER_TYPE_EMAIL).toBe('email');
217
+ expect(IDENTIFIER_TYPE_MOBILE).toBe('mobile');
218
+ expect(IDENTIFIER_TYPE_PHONE).toBe('phone');
219
+ });
220
+ });
221
+
222
+ describe('Validation Regex', () => {
223
+ it('should export EMAIL_REGEX that validates email format', () => {
224
+ expect(EMAIL_REGEX.test('user@example.com')).toBe(true);
225
+ expect(EMAIL_REGEX.test('invalid')).toBe(false);
226
+ });
227
+ it('should export PHONE_REGEX that validates phone format', () => {
228
+ expect(PHONE_REGEX.test('9123456789')).toBe(true);
229
+ expect(PHONE_REGEX.test('123')).toBe(false);
230
+ });
231
+ });
232
+
233
+ describe('Input has error class', () => {
234
+ it('should export INPUT_HAS_ERROR_CLASS', () => {
235
+ expect(INPUT_HAS_ERROR_CLASS).toBe(' has-input-error');
236
+ });
237
+ });
238
+
209
239
  describe('Channel Name Constants', () => {
210
240
  it('should export channel name constants', () => {
211
241
  expect(CHANNEL_NAME_INAPP).toBe('INAPP');
@@ -86,6 +86,12 @@ jest.mock('../../../utils/cdnTransformation', () => ({
86
86
  getCdnUrl: jest.fn(({ url }) => `cdn_${url}`),
87
87
  }));
88
88
 
89
+ // Mock services/api for add test customer flow
90
+ jest.mock('../../../services/api', () => ({
91
+ getMembersLookup: jest.fn(),
92
+ createTestCustomer: jest.fn(),
93
+ }));
94
+
89
95
  // Mock messages - using actual message IDs from messages.js
90
96
  const mockMessages = {
91
97
  'app.v2Components.TestAndPreviewSlidebox.testAndPreviewHeader': { defaultMessage: 'Preview and Test' },
@@ -131,6 +137,7 @@ describe('CommonTestAndPreview', () => {
131
137
  clearPreviewErrors: jest.fn(),
132
138
  getSenderDetailsRequested: jest.fn(),
133
139
  getWeCrmAccountsRequested: jest.fn(),
140
+ addTestCustomer: jest.fn(),
134
141
  };
135
142
 
136
143
  const defaultProps = {
@@ -1603,6 +1610,35 @@ describe('CommonTestAndPreview', () => {
1603
1610
 
1604
1611
  expect(screen.getByTestId('send-test-message')).toBeTruthy();
1605
1612
  });
1613
+
1614
+ it('should show error notification when sendTestMessageRequested callback receives false', async () => {
1615
+ const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
1616
+ mockActions.createMessageMetaRequested.mockImplementation((payload, metaId, cb) => {
1617
+ if (cb) cb({ entity: 'meta-123' });
1618
+ });
1619
+ mockActions.sendTestMessageRequested.mockImplementation((payload, cb) => {
1620
+ if (cb) cb(false);
1621
+ });
1622
+ const props = {
1623
+ ...defaultProps,
1624
+ selectedTestEntities: ['user-1'],
1625
+ testGroups: [],
1626
+ };
1627
+
1628
+ render(
1629
+ <TestWrapper>
1630
+ <CommonTestAndPreview {...props} />
1631
+ </TestWrapper>
1632
+ );
1633
+
1634
+ expect(lastSendTestMessageProps).toBeTruthy();
1635
+ expect(lastSendTestMessageProps.handleSendTestMessage).toBeDefined();
1636
+ lastSendTestMessageProps.handleSendTestMessage();
1637
+
1638
+ await waitFor(() => {
1639
+ expect(CapNotification.error).toHaveBeenCalled();
1640
+ });
1641
+ });
1606
1642
  });
1607
1643
 
1608
1644
  describe('Content Extraction', () => {
@@ -24,6 +24,7 @@ import {
24
24
  GET_TEST_CUSTOMERS_REQUESTED,
25
25
  GET_TEST_CUSTOMERS_SUCCESS,
26
26
  GET_TEST_CUSTOMERS_FAILURE,
27
+ ADD_TEST_CUSTOMER,
27
28
  GET_TEST_GROUPS_REQUESTED,
28
29
  GET_TEST_GROUPS_SUCCESS,
29
30
  GET_TEST_GROUPS_FAILURE,
@@ -611,6 +612,76 @@ describe('previewAndTestReducer', () => {
611
612
  });
612
613
  });
613
614
 
615
+ describe('ADD_TEST_CUSTOMER', () => {
616
+ it('should add new customer to testCustomers list', () => {
617
+ const customer = {
618
+ userId: 'cust-1',
619
+ customerId: 'cust-1',
620
+ name: 'John',
621
+ email: 'john@example.com',
622
+ mobile: '',
623
+ };
624
+ const action = {
625
+ type: ADD_TEST_CUSTOMER,
626
+ payload: { customer },
627
+ };
628
+ const result = previewAndTestReducer(initialState, action);
629
+
630
+ const list = result.get('testCustomers');
631
+ expect(Array.isArray(list) ? list.length : list.size).toBe(1);
632
+ const first = Array.isArray(list) ? list[0] : list.get(0);
633
+ const id = first.userId != null ? first.userId : first.get('userId');
634
+ const name = first.name != null ? first.name : first.get('name');
635
+ expect(id).toBe('cust-1');
636
+ expect(name).toBe('John');
637
+ });
638
+
639
+ it('should not add duplicate customer when userId already in list', () => {
640
+ const existing = fromJS([
641
+ { userId: 'cust-1', customerId: 'cust-1', name: 'John', email: 'john@example.com', mobile: '' },
642
+ ]);
643
+ const stateWithCustomer = initialState.set('testCustomers', existing);
644
+ const customer = {
645
+ userId: 'cust-1',
646
+ customerId: 'cust-1',
647
+ name: 'John Updated',
648
+ email: 'john@example.com',
649
+ mobile: '',
650
+ };
651
+ const action = {
652
+ type: ADD_TEST_CUSTOMER,
653
+ payload: { customer },
654
+ };
655
+ const result = previewAndTestReducer(stateWithCustomer, action);
656
+
657
+ const list = result.get('testCustomers');
658
+ expect(Array.isArray(list) ? list.length : list.size).toBe(1);
659
+ const first = Array.isArray(list) ? list[0] : list.get(0);
660
+ const name = first.name != null ? first.name : first.get('name');
661
+ expect(name).toBe('John');
662
+ });
663
+
664
+ it('should use customerId when userId is missing', () => {
665
+ const customer = {
666
+ customerId: 'cust-2',
667
+ name: 'Jane',
668
+ email: 'jane@example.com',
669
+ mobile: '',
670
+ };
671
+ const action = {
672
+ type: ADD_TEST_CUSTOMER,
673
+ payload: { customer },
674
+ };
675
+ const result = previewAndTestReducer(initialState, action);
676
+
677
+ const list = result.get('testCustomers');
678
+ expect(Array.isArray(list) ? list.length : list.size).toBe(1);
679
+ const first = Array.isArray(list) ? list[0] : list.get(0);
680
+ const id = first.customerId != null ? first.customerId : first.get('customerId');
681
+ expect(id).toBe('cust-2');
682
+ });
683
+ });
684
+
614
685
  describe('GET_TEST_GROUPS_REQUESTED', () => {
615
686
  it('should set fetching flag and clear error', () => {
616
687
  const action = { type: GET_TEST_GROUPS_REQUESTED };
@@ -431,6 +431,23 @@ describe('CommonTestAndPreview Selectors', () => {
431
431
  // Should handle null gracefully - returns null when prefilledValues is null
432
432
  expect(result).toBeNull();
433
433
  });
434
+
435
+ it('should return undefined when commonTestAndPreview substate is missing', () => {
436
+ const selector = makeSelectPrefilledValues();
437
+ const result = selector(fromJS({}));
438
+
439
+ expect(result).toBeUndefined();
440
+ });
441
+
442
+ it('should return null when prefilledValues is undefined', () => {
443
+ const stateWithUndefined = fromJS({
444
+ commonTestAndPreview: {},
445
+ });
446
+ const selector = makeSelectPrefilledValues();
447
+ const result = selector(stateWithUndefined);
448
+
449
+ expect(result).toBeNull();
450
+ });
434
451
  });
435
452
 
436
453
  describe('makeSelectTestMessageResponse', () => {
@@ -2990,7 +2990,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
2990
2990
  selectedOfferDetails={this.props.selectedOfferDetails}
2991
2991
  eventContextTags={this.props?.eventContextTags}
2992
2992
  restrictPersonalization={this.props.restrictPersonalization}
2993
- waitEventContextTags={this.props?.waitEventContextTags}
2994
2993
  />
2995
2994
  </CapColumn>
2996
2995
  );
@@ -3020,7 +3019,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3020
3019
  userLocale={this.props.userLocale}
3021
3020
  selectedOfferDetails={this.props.selectedOfferDetails}
3022
3021
  eventContextTags={this.props?.eventContextTags}
3023
- waitEventContextTags={this.props?.waitEventContextTags}
3024
3022
  moduleFilterEnabled={this.props.location && this.props.location.query && this.props.location.query.type !== 'embedded'}
3025
3023
  containerStyle={val.style || {}}
3026
3024
  inputProps={val.inputProps || {}}
@@ -3660,7 +3658,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3660
3658
  channel={channel}
3661
3659
  eventContextTags={this.props?.eventContextTags}
3662
3660
  restrictPersonalization={this.props.restrictPersonalization}
3663
- waitEventContextTags={this.props?.waitEventContextTags}
3664
3661
  getPopupContainer={this.props.tagListGetPopupContainer}
3665
3662
  popoverOverlayStyle={this.props.tagListPopoverOverlayStyle}
3666
3663
  popoverOverlayClassName={this.props.tagListPopoverOverlayClassName}
@@ -3710,7 +3707,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3710
3707
  userLocale={this.state.translationLang}
3711
3708
  selectedOfferDetails={this.props.selectedOfferDetails}
3712
3709
  eventContextTags={this.props?.eventContextTags}
3713
- waitEventContextTags={this.props?.waitEventContextTags}
3714
3710
  moduleFilterEnabled={moduleFilterEnabledForCapTagList}
3715
3711
  containerStyle={val.style || {}}
3716
3712
  inputProps={val.inputProps || {}}
@@ -4005,7 +4001,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4005
4001
  onContextChange={this.props.onContextChange}
4006
4002
  moduleFilterEnabled={isModuleFilterEnabled}
4007
4003
  eventContextTags={this.props?.eventContextTags}
4008
- waitEventContextTags={this.props?.waitEventContextTags}
4009
4004
  />
4010
4005
  </CapColumn>
4011
4006
  );
@@ -4308,7 +4303,6 @@ FormBuilder.defaultProps = {
4308
4303
  userLocale: localStorage.getItem('jlocale') || 'en',
4309
4304
  showLiquidErrorInFooter: () => {},
4310
4305
  metaDataStatus: "",
4311
- waitEventContextTags: {},
4312
4306
  isTestAndPreviewMode: false, // Default to false to maintain existing behavior
4313
4307
  };
4314
4308
 
@@ -4359,7 +4353,6 @@ FormBuilder.propTypes = {
4359
4353
  moduleType: PropTypes.string.isRequired,
4360
4354
  showLiquidErrorInFooter: PropTypes.func.isRequired,
4361
4355
  eventContextTags: PropTypes.array.isRequired,
4362
- waitEventContextTags: PropTypes.object,
4363
4356
  forwardedTags: PropTypes.object.isRequired,
4364
4357
  isLoyaltyModule: PropTypes.bool.isRequired,
4365
4358
  isTestAndPreviewMode: PropTypes.bool, // Add new prop type
@@ -94,7 +94,6 @@ const HTMLEditor = forwardRef(({
94
94
  injectedTags = {},
95
95
  location,
96
96
  eventContextTags = [],
97
- waitEventContextTags,
98
97
  selectedOfferDetails = [],
99
98
  channel,
100
99
  userLocale = 'en',
@@ -362,7 +361,7 @@ const HTMLEditor = forwardRef(({
362
361
  const issueCounts = calculateIssueCounts();
363
362
  const isContentEmpty = !currentContent || currentContent.trim() === '';
364
363
 
365
- // hasErrors = only Rule Group #1 (Input & Sanitization) - gates Done/Update/Preview/Test
364
+ // hasErrors = only Rule Group #1 (Input & Sanitization) gates Done/Update/Preview/Test
366
365
  const newState = {
367
366
  isContentEmpty,
368
367
  issueCounts,
@@ -664,7 +663,6 @@ const HTMLEditor = forwardRef(({
664
663
  injectedTags={injectedTags}
665
664
  location={location}
666
665
  eventContextTags={eventContextTags}
667
- waitEventContextTags={waitEventContextTags}
668
666
  selectedOfferDetails={selectedOfferDetails}
669
667
  channel={channel}
670
668
  userLocale={userLocale}
@@ -736,7 +734,6 @@ const HTMLEditor = forwardRef(({
736
734
  injectedTags={injectedTags}
737
735
  location={location}
738
736
  eventContextTags={eventContextTags}
739
- waitEventContextTags={waitEventContextTags}
740
737
  selectedOfferDetails={selectedOfferDetails}
741
738
  channel={channel}
742
739
  userLocale={userLocale}
@@ -775,7 +772,6 @@ HTMLEditor.propTypes = {
775
772
  injectedTags: PropTypes.object,
776
773
  location: PropTypes.object,
777
774
  eventContextTags: PropTypes.array,
778
- waitEventContextTags: PropTypes.object,
779
775
  selectedOfferDetails: PropTypes.array,
780
776
  channel: PropTypes.string,
781
777
  userLocale: PropTypes.string,
@@ -809,7 +805,6 @@ HTMLEditor.defaultProps = {
809
805
  injectedTags: {},
810
806
  location: null,
811
807
  eventContextTags: [],
812
- waitEventContextTags: {},
813
808
  selectedOfferDetails: [],
814
809
  channel: null,
815
810
  userLocale: 'en',
@@ -225,7 +225,6 @@ const defaultProps = {
225
225
  injectedTags: {},
226
226
  location: { query: {} },
227
227
  eventContextTags: [],
228
- waitEventContextTags: {},
229
228
  selectedOfferDetails: [],
230
229
  channel: 'EMAIL',
231
230
  userLocale: 'en',