@capillarytech/creatives-library 8.0.298 → 8.0.299-alpha.3

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 (27) 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/tests/commonUtil.test.js +169 -0
  6. package/v2Components/CommonTestAndPreview/AddTestCustomer.js +42 -0
  7. package/v2Components/CommonTestAndPreview/CustomerCreationModal.js +284 -0
  8. package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +72 -0
  9. package/v2Components/CommonTestAndPreview/SendTestMessage.js +78 -49
  10. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +189 -4
  11. package/v2Components/CommonTestAndPreview/actions.js +10 -0
  12. package/v2Components/CommonTestAndPreview/constants.js +18 -1
  13. package/v2Components/CommonTestAndPreview/index.js +259 -14
  14. package/v2Components/CommonTestAndPreview/messages.js +94 -0
  15. package/v2Components/CommonTestAndPreview/reducer.js +10 -0
  16. package/v2Components/CommonTestAndPreview/tests/AddTestCustomer.test.js +66 -0
  17. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +653 -0
  18. package/v2Components/CommonTestAndPreview/tests/CustomerCreationModal.test.js +316 -0
  19. package/v2Components/CommonTestAndPreview/tests/ExistingCustomerModal.test.js +114 -0
  20. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +53 -0
  21. package/v2Components/CommonTestAndPreview/tests/constants.test.js +25 -2
  22. package/v2Components/CommonTestAndPreview/tests/index.test.js +7 -0
  23. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +71 -0
  24. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +17 -0
  25. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +1408 -1276
  26. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +321 -288
  27. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +5246 -4872
@@ -17,6 +17,9 @@ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
17
17
  import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
18
18
  import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
19
19
  import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
20
+ import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
21
+ import CustomerCreationModal from './CustomerCreationModal';
22
+ import { createTestCustomer } from '../../services/api';
20
23
 
21
24
  // Import messages and styles
22
25
  import messages from './messages';
@@ -27,10 +30,16 @@ import LeftPanelContent from './LeftPanelContent';
27
30
  import CustomValuesEditor from './CustomValuesEditor';
28
31
  import SendTestMessage from './SendTestMessage';
29
32
  import PreviewSection from './PreviewSection';
30
-
33
+ import AddTestCustomerButton from './AddTestCustomer';
34
+ import ExistingCustomerModal from './ExistingCustomerModal';
31
35
  // Import constants
32
36
  import {
33
37
  CHANNELS,
38
+ CUSTOMER_MODAL_NEW,
39
+ CUSTOMER_MODAL_EXISTING,
40
+ IDENTIFIER_TYPE_EMAIL,
41
+ IDENTIFIER_TYPE_MOBILE,
42
+ IDENTIFIER_TYPE_PHONE,
34
43
  TEST,
35
44
  DESKTOP,
36
45
  ANDROID,
@@ -74,6 +83,8 @@ import {
74
83
 
75
84
  // Import utilities
76
85
  import { getCdnUrl } from '../../utils/cdnTransformation';
86
+ import { isValidEmail, isValidMobile } from '../../utils/commonUtils';
87
+ import { getMembersLookup } from '../../services/api';
77
88
 
78
89
  /**
79
90
  * Preview Component Factory - REMOVED IN PHASE 5
@@ -130,6 +141,11 @@ const CommonTestAndPreview = (props) => {
130
141
  const [customValues, setCustomValues] = useState({});
131
142
  const [showJSON, setShowJSON] = useState(false);
132
143
  const [tagsExtracted, setTagsExtracted] = useState(false);
144
+ const [searchValue, setSearchValue] = useState("");
145
+ const [customerModal, setCustomerModal] = useState([false, ""]);
146
+ const [isCustomerDataLoading, setIsCustomerDataLoading] = useState(false);
147
+ const [customerData, setCustomerData] = useState({ name: '', email: '', mobile: '', customerId: '' });
148
+
133
149
  // Initialize device based on channel: SMS uses Android/iOS, others use Desktop/Mobile
134
150
  // Initialize device based on channel: SMS, WhatsApp, RCS, InApp, MobilePush, and Viber use Android/iOS, others use Desktop/Mobile
135
151
  const initialDevice = (channel === CHANNELS.SMS || channel === CHANNELS.WHATSAPP || channel === CHANNELS.RCS || channel === CHANNELS.INAPP || channel === CHANNELS.MOBILEPUSH || channel === CHANNELS.VIBER) ? ANDROID : DESKTOP;
@@ -348,7 +364,7 @@ const CommonTestAndPreview = (props) => {
348
364
  return content || '';
349
365
  }, [channel, formData, currentTab, beeContent, content, beeInstance]);
350
366
 
351
- // Build test entities tree data
367
+ // Build test entities tree data from testCustomers prop (includes customers added via addTestCustomer action)
352
368
  const testEntitiesTreeData = useMemo(() => {
353
369
  const groupsNode = {
354
370
  title: 'Groups',
@@ -361,7 +377,10 @@ const CommonTestAndPreview = (props) => {
361
377
  title: 'Individuals',
362
378
  value: 'customers-node',
363
379
  selectable: false,
364
- children: testCustomers?.map((customer) => ({ title: customer.name, value: customer?.userId })),
380
+ children: testCustomers?.map((customer) => ({
381
+ title: customer?.name?.trim() || customer?.email?.trim() || customer?.mobile?.trim() || customer?.userId || customer?.customerId,
382
+ value: customer?.userId ?? customer?.customerId,
383
+ })) || [],
365
384
  };
366
385
 
367
386
  return [groupsNode, customersNode];
@@ -388,6 +407,91 @@ const CommonTestAndPreview = (props) => {
388
407
  return resolvedText;
389
408
  };
390
409
 
410
+ /**
411
+ * Common handler for saving test customers (both new and existing)
412
+ */
413
+ const handleSaveTestCustomer = async (validationErrors = {},setIsLoading = false) => {
414
+ // Check for validation errors before saving (for new customers)
415
+ if (customerModal[1] === CUSTOMER_MODAL_NEW && (validationErrors.email || validationErrors.mobile)) {
416
+ return;
417
+ }
418
+
419
+ setIsLoading(true);
420
+
421
+ try {
422
+ let payload;
423
+
424
+ if (customerModal[1] === CUSTOMER_MODAL_EXISTING) {
425
+ // For existing customers, use customerId
426
+ payload = {
427
+ customerId: customerData.customerId
428
+ };
429
+ } else {
430
+ // For new customers, use customer object
431
+ payload = {
432
+ customer: {
433
+ firstName: customerData.name || "",
434
+ mobile: customerData.mobile || "",
435
+ email: customerData.email || ""
436
+ }
437
+ };
438
+ }
439
+
440
+ const response = await createTestCustomer(payload);
441
+
442
+
443
+ // Handle success: add to test customers list and selection (existing and new)
444
+ if (response && response.success) {
445
+ CapNotification.success({
446
+ message: formatMessage(messages.newTestCustomerAddedSuccess),
447
+ });
448
+ // API may return customerId in response.response (e.g. { response: { customerId: 438845651 } })
449
+ const res = response?.response || response;
450
+ const addedId = customerModal[1] === CUSTOMER_MODAL_EXISTING
451
+ ? customerData?.customerId
452
+ : res?.customerId;
453
+ if (addedId) {
454
+ actions.addTestCustomer({
455
+ userId: addedId,
456
+ customerId: addedId,
457
+ name: customerData?.name?.trim() || '',
458
+ email: customerData?.email || '',
459
+ mobile: customerData?.mobile || '',
460
+ });
461
+ setSelectedTestEntities((prev) => [...prev, addedId]);
462
+ }
463
+ handleCloseCustomerModal();
464
+ } else {
465
+ // Show error notification for unsuccessful response
466
+ CapNotification.error({
467
+ message: formatMessage(messages.errorTitle),
468
+ description: response?.message || formatMessage(messages.failedToAddTestCustomer),
469
+ });
470
+ }
471
+ } catch (error) {
472
+ if (customerModal[1] === CUSTOMER_MODAL_EXISTING) {
473
+ // Show error notification for caught exceptions (existing customers only)
474
+ CapNotification.error({
475
+ message: formatMessage(messages.errorTitle),
476
+ description: error?.message || formatMessage(messages.errorAddingTestCustomer),
477
+ });
478
+ }
479
+ } finally {
480
+ setIsLoading(false);
481
+ }
482
+ };
483
+
484
+ const handleCloseCustomerModal = () => {
485
+ setCustomerModal([false, ""]);
486
+ setSearchValue('');
487
+ setCustomerData({
488
+ name: '',
489
+ email: '',
490
+ mobile: '',
491
+ customerId: '',
492
+ });
493
+ };
494
+
391
495
  /**
392
496
  * Prepare payload for preview API based on channel
393
497
  */
@@ -2472,7 +2576,6 @@ const CommonTestAndPreview = (props) => {
2472
2576
  * Handle extract tags
2473
2577
  */
2474
2578
  const handleExtractTags = () => {
2475
- // Get content based on channel
2476
2579
  let contentToExtract = getCurrentContent;
2477
2580
 
2478
2581
  if (channel === CHANNELS.EMAIL && formData) {
@@ -2512,6 +2615,107 @@ const CommonTestAndPreview = (props) => {
2512
2615
  setSelectedTestEntities(value);
2513
2616
  };
2514
2617
 
2618
+ /**
2619
+ * Map API customerDetails item to our customerData shape
2620
+ */
2621
+ const mapCustomerDetailsToCustomerData = (detail, identifierValue) => {
2622
+ const firstName = detail.firstName || '';
2623
+ const lastName = detail.lastName || '';
2624
+ const name = [firstName, lastName].filter(Boolean).join(' ').trim() || '';
2625
+ const getIdentifierValue = (type) => {
2626
+ const fromIdentifiers = detail.identifiers?.find((i) => i.type === type)?.value;
2627
+ if (fromIdentifiers) return fromIdentifiers;
2628
+ const fromCommChannels = detail.commChannels?.find((c) => c.type === type)?.value;
2629
+ return fromCommChannels || (channel === CHANNELS.EMAIL && type === IDENTIFIER_TYPE_EMAIL ? identifierValue : channel === CHANNELS.SMS && type === IDENTIFIER_TYPE_MOBILE ? identifierValue : '');
2630
+ };
2631
+ return {
2632
+ name,
2633
+ email: channel === CHANNELS.EMAIL ? (getIdentifierValue(IDENTIFIER_TYPE_EMAIL) || identifierValue) : (getIdentifierValue(IDENTIFIER_TYPE_EMAIL) || ''),
2634
+ mobile: channel === CHANNELS.SMS ? (getIdentifierValue(IDENTIFIER_TYPE_MOBILE) || getIdentifierValue(IDENTIFIER_TYPE_PHONE) || identifierValue) : (getIdentifierValue(IDENTIFIER_TYPE_MOBILE) || getIdentifierValue(IDENTIFIER_TYPE_PHONE) || ''),
2635
+ customerId: detail.userId != null ? String(detail.userId) : '',
2636
+ };
2637
+ };
2638
+
2639
+ const handleAddTestCustomer = async () => {
2640
+ const identifierType = channel === CHANNELS.EMAIL ? IDENTIFIER_TYPE_EMAIL : IDENTIFIER_TYPE_MOBILE;
2641
+ const searchValueToCheck = searchValue || '';
2642
+
2643
+ // Check if this customer is already in the test customers list
2644
+ const existingTestCustomer = testCustomers?.find(customer => {
2645
+ if (channel === CHANNELS.EMAIL) {
2646
+ return customer.email === searchValueToCheck;
2647
+ } else if (channel === CHANNELS.SMS) {
2648
+ return customer.mobile === searchValueToCheck;
2649
+ }
2650
+ return false;
2651
+ });
2652
+
2653
+ if (existingTestCustomer) {
2654
+ const entityId = existingTestCustomer.userId ?? existingTestCustomer.customerId;
2655
+ if (entityId != null) {
2656
+ const id = String(entityId);
2657
+ setSelectedTestEntities((prev) =>
2658
+ prev.includes(id) ? prev : [...prev, id]
2659
+ );
2660
+ }
2661
+ setSearchValue('');
2662
+ CapNotification.success({
2663
+ message: formatMessage(messages.customerAlreadyInTestList),
2664
+ });
2665
+ return;
2666
+ }
2667
+
2668
+ setIsCustomerDataLoading(true);
2669
+
2670
+ try {
2671
+ const response = await getMembersLookup(identifierType, searchValueToCheck);
2672
+ const success = response?.success && !response?.status?.isError;
2673
+ const res = response?.response || {};
2674
+ const exists = res.exists || false;
2675
+ const details = res.customerDetails || [];
2676
+
2677
+ if (!success) {
2678
+ const errorMessage = response?.message || response?.status?.message || formatMessage(messages.memberLookupError);
2679
+ CapNotification.error({ title: formatMessage(messages.errorTitle), message: errorMessage });
2680
+ return;
2681
+ }
2682
+
2683
+ if (exists && details.length > 0) {
2684
+ const mapped = mapCustomerDetailsToCustomerData(details[0], searchValueToCheck);
2685
+ const customerIdFromLookup = mapped.customerId;
2686
+ const alreadyInTestListByCustomerId = customerIdFromLookup && testCustomers?.some(
2687
+ (c) => String(c?.customerId) === customerIdFromLookup || String(c?.userId) === customerIdFromLookup
2688
+ );
2689
+ if (alreadyInTestListByCustomerId) {
2690
+ setSelectedTestEntities((prev) =>
2691
+ prev.includes(customerIdFromLookup) ? prev : [...prev, customerIdFromLookup]
2692
+ );
2693
+ setSearchValue('');
2694
+ CapNotification.success({
2695
+ message: formatMessage(messages.customerAlreadyInTestList),
2696
+ });
2697
+ return;
2698
+ }
2699
+ setCustomerData(mapped);
2700
+ setCustomerModal([true, CUSTOMER_MODAL_EXISTING]);
2701
+ } else {
2702
+ setCustomerData({
2703
+ name: '',
2704
+ email: channel === CHANNELS.EMAIL ? searchValueToCheck : '',
2705
+ mobile: channel === CHANNELS.SMS ? searchValueToCheck : '',
2706
+ customerId: '',
2707
+ });
2708
+ setCustomerModal([true, CUSTOMER_MODAL_NEW]);
2709
+ }
2710
+ } catch {
2711
+ CapNotification.error({
2712
+ message: formatMessage(messages.memberLookupError),
2713
+ });
2714
+ } finally {
2715
+ setIsCustomerDataLoading(false);
2716
+ }
2717
+ };
2718
+
2515
2719
  /**
2516
2720
  * Handle send test message
2517
2721
  */
@@ -2630,6 +2834,7 @@ const CommonTestAndPreview = (props) => {
2630
2834
  content={getCurrentContent}
2631
2835
  channel={channel}
2632
2836
  isSendingTestMessage={isSendingTestMessage}
2837
+ renderAddTestCustomerButton={renderAddTestCustomerButton}
2633
2838
  formatMessage={formatMessage}
2634
2839
  deliverySettings={testPreviewDeliverySettings[channel]}
2635
2840
  senderDetailsOptions={senderDetailsByChannel[channel]}
@@ -2638,6 +2843,8 @@ const CommonTestAndPreview = (props) => {
2638
2843
  isLoadingSenderDetails={isLoadingSenderDetails}
2639
2844
  smsTraiDltEnabled={smsTraiDltEnabled}
2640
2845
  registeredSenderIds={registeredSenderIds}
2846
+ searchValue={searchValue}
2847
+ setSearchValue={setSearchValue}
2641
2848
  />
2642
2849
  );
2643
2850
 
@@ -2647,6 +2854,20 @@ const CommonTestAndPreview = (props) => {
2647
2854
  />
2648
2855
  );
2649
2856
 
2857
+ const renderAddTestCustomerButton = () => {
2858
+ const value = searchValue || "";
2859
+ const showAddButton =
2860
+ [CHANNELS.EMAIL, CHANNELS.SMS].includes(channel) &&
2861
+ (channel === CHANNELS.EMAIL ? isValidEmail(value) : isValidMobile(value));
2862
+ if (!showAddButton) return null;
2863
+ return (
2864
+ <AddTestCustomerButton
2865
+ searchValue={value}
2866
+ handleAddTestCustomer={handleAddTestCustomer}
2867
+ />
2868
+ );
2869
+ };
2870
+
2650
2871
  // Header content for the slidebox
2651
2872
  const slideboxHeader = (
2652
2873
  <CapRow className="test-preview-header">
@@ -2666,14 +2887,18 @@ const CommonTestAndPreview = (props) => {
2666
2887
  show={show}
2667
2888
  size="size-xl"
2668
2889
  content={(
2669
- <CapRow className="test-preview-container">
2670
- <CapRow className="test-and-preview-panels">
2671
- {/* Left Panel */}
2672
- <CapRow className="left-panel">
2673
- {channel === CHANNELS.ZALO ? null : renderLeftPanelContent()}
2674
- <CapDivider className="panel-divider" />
2675
-
2676
- {/* Send Test Message Section */}
2890
+ <CapSpin
2891
+ spinning={isCustomerDataLoading}
2892
+ className={`common-test-preview-lookup-spin ${isCustomerDataLoading ? 'common-test-preview-customer-loading' : ''}`}
2893
+ >
2894
+ <CapRow className="test-preview-container">
2895
+ <CapRow className="test-and-preview-panels">
2896
+ {/* Left Panel */}
2897
+ <CapRow className="left-panel">
2898
+ {channel === CHANNELS.ZALO ? null : renderLeftPanelContent()}
2899
+ <CapDivider className="panel-divider" />
2900
+
2901
+ {/* Send Test Message Section */}
2677
2902
  {config.enableTestMessage !== false && (
2678
2903
  <CapRow className="panel-section send-test-section">
2679
2904
  {renderSendTestMessage()}
@@ -2687,7 +2912,27 @@ const CommonTestAndPreview = (props) => {
2687
2912
  {renderPreview()}
2688
2913
  </CapRow>
2689
2914
  </CapRow>
2690
- </CapRow>
2915
+ {customerModal[0] && customerModal[1] === CUSTOMER_MODAL_EXISTING && (
2916
+ <ExistingCustomerModal
2917
+ customerData={customerData}
2918
+ setCustomerModal={setCustomerModal}
2919
+ customerModal={customerModal}
2920
+ channel={channel}
2921
+ onSave={handleSaveTestCustomer}
2922
+ />
2923
+ )}
2924
+ {customerModal[0] && customerModal[1] === CUSTOMER_MODAL_NEW && (
2925
+ <CustomerCreationModal
2926
+ customerData={customerData}
2927
+ setCustomerData={setCustomerData}
2928
+ setCustomerModal={setCustomerModal}
2929
+ customerModal={customerModal}
2930
+ onSave={handleSaveTestCustomer}
2931
+ channel={channel}
2932
+ />
2933
+ )}
2934
+ </CapRow>
2935
+ </CapSpin>
2691
2936
  )}
2692
2937
  />
2693
2938
  );
@@ -2789,4 +3034,4 @@ CommonTestAndPreview.defaultProps = {
2789
3034
  // Note: Redux connection is handled by the wrapper components (e.g., TestAndPreviewSlidebox)
2790
3035
  // This component receives all Redux props (including intl) from its parent
2791
3036
 
2792
- export default CommonTestAndPreview;
3037
+ export default CommonTestAndPreview;
@@ -8,10 +8,72 @@ import { defineMessages } from 'react-intl';
8
8
  export const scope = 'app.v2Components.TestAndPreviewSlidebox';
9
9
 
10
10
  export default defineMessages({
11
+ mobileAlreadyExists: {
12
+ id: `${scope}.mobileAlreadyExists`,
13
+ defaultMessage: 'This phone number matches with already existing user profile. Please remove or use a different number.',
14
+ },
15
+ customerNamePlaceholder: {
16
+ id: `${scope}.customerNamePlaceholder`,
17
+ defaultMessage: 'Enter the name',
18
+ },
19
+ customerEmailPlaceholder: {
20
+ id: `${scope}.customerEmailPlaceholder`,
21
+ defaultMessage: 'Enter the Email',
22
+ },
23
+ customerMobilePlaceholder: {
24
+ id: `${scope}.customerMobilePlaceholder`,
25
+ defaultMessage: 'Enter the Mobile Number',
26
+ },
27
+ customerAlreadyInTestList: {
28
+ id: `${scope}.customerAlreadyInTestList`,
29
+ defaultMessage: 'This customer is already in the test customers list.',
30
+ },
31
+ emailAlreadyExists: {
32
+ id: `${scope}.emailAlreadyExists`,
33
+ defaultMessage: 'This email matches with already existing user profile. Please remove or use a different email.',
34
+ },
35
+ customerName: {
36
+ id: `${scope}.customerName`,
37
+ defaultMessage: 'Name',
38
+ },
39
+ customerEmail: {
40
+ id: `${scope}.customerEmail`,
41
+ defaultMessage: 'Email',
42
+ },
43
+ customerMobile: {
44
+ id: `${scope}.customerMobileNumber`,
45
+ defaultMessage: 'Mobile Number',
46
+ },
47
+ saveButton: {
48
+ id: `${scope}.saveButton`,
49
+ defaultMessage: 'Save',
50
+ },
51
+ cancelButton: {
52
+ id: `${scope}.cancelButton`,
53
+ defaultMessage: 'Cancel',
54
+ },
55
+ customerID: {
56
+ id: `${scope}.customerID`,
57
+ defaultMessage: 'Customer ID',
58
+ },
59
+ existingCustomerModalDescription: {
60
+ id: `${scope}.existingCustomerModalDescription`,
61
+ defaultMessage: 'This user profile already exists in the system. Would you like to add them to Test Customer list?'
62
+ },
63
+
64
+ customerCreationModalTitle: {
65
+ id: `${scope}.customerCreationModalTitle`,
66
+ defaultMessage: 'Add new test customer',
67
+ },
68
+ customerCreationModalDescription: {
69
+ id: `${scope}.customerCreationModalDescription`,
70
+ defaultMessage: 'This customer profile will be available for testing across multiple communication channels.',
71
+ },
11
72
  testAndPreviewHeader: {
12
73
  id: `${scope}.testAndPreviewHeader`,
13
74
  defaultMessage: 'Preview and Test',
14
75
  },
76
+
15
77
  customerSearchTitle: {
16
78
  id: `${scope}.customerSearchTitle`,
17
79
  defaultMessage: 'Customer',
@@ -32,6 +94,34 @@ export default defineMessages({
32
94
  id: `${scope}.showJSON`,
33
95
  defaultMessage: 'Show JSON',
34
96
  },
97
+ addTestCustomer: {
98
+ id: `${scope}.addTestCustomer`,
99
+ defaultMessage: 'Add as Test Customer',
100
+ },
101
+ addTestCustomerWithValue: {
102
+ id: `${scope}.addTestCustomerWithValue`,
103
+ defaultMessage: 'Add {searchValue} as Test Customer',
104
+ },
105
+ memberLookupError: {
106
+ id: `${scope}.memberLookupError`,
107
+ defaultMessage: 'Unable to look up customer. Please try again.',
108
+ },
109
+ newTestCustomerAddedSuccess: {
110
+ id: `${scope}.newTestCustomerAddedSuccess`,
111
+ defaultMessage: 'New test customer added successfully!',
112
+ },
113
+ errorTitle: {
114
+ id: `${scope}.errorTitle`,
115
+ defaultMessage: 'Error',
116
+ },
117
+ failedToAddTestCustomer: {
118
+ id: `${scope}.failedToAddTestCustomer`,
119
+ defaultMessage: 'Failed to add test customer',
120
+ },
121
+ errorAddingTestCustomer: {
122
+ id: `${scope}.errorAddingTestCustomer`,
123
+ defaultMessage: 'An error occurred while adding test customer',
124
+ },
35
125
  discardCustomValues: {
36
126
  id: `${scope}.discardCustomValues`,
37
127
  defaultMessage: 'Discard custom values',
@@ -120,6 +210,10 @@ export default defineMessages({
120
210
  id: `${scope}.testCustomersPlaceholder`,
121
211
  defaultMessage: 'Search and select a group or individual test customers',
122
212
  },
213
+ noMatchingOptions: {
214
+ id: `${scope}.noMatchingOptions`,
215
+ defaultMessage: 'No matching options',
216
+ },
123
217
  updatingPreview: {
124
218
  id: `${scope}.updatingPreview`,
125
219
  defaultMessage: 'Updating preview with the latest changes',
@@ -20,6 +20,7 @@ import {
20
20
  GET_TEST_CUSTOMERS_REQUESTED,
21
21
  GET_TEST_CUSTOMERS_SUCCESS,
22
22
  GET_TEST_CUSTOMERS_FAILURE,
23
+ ADD_TEST_CUSTOMER,
23
24
  GET_TEST_GROUPS_REQUESTED,
24
25
  GET_TEST_GROUPS_SUCCESS,
25
26
  GET_TEST_GROUPS_FAILURE,
@@ -216,6 +217,15 @@ const previewAndTestReducer = (state = initialState, action) => {
216
217
  return state.set('isFetchingTestCustomers', false)
217
218
  .set('fetchTestCustomersError', action.payload.error);
218
219
 
220
+ case ADD_TEST_CUSTOMER: {
221
+ const raw = state.get('testCustomers');
222
+ const list = Array.isArray(raw) ? raw : (raw && raw.toArray ? raw.toArray().map((i) => (i && i.toJS ? i.toJS() : i)) : []);
223
+ const customer = action.payload.customer;
224
+ const newId = customer.userId || customer.customerId;
225
+ if (list.some((c) => (c.userId || c.customerId) === newId)) return state;
226
+ return state.set('testCustomers', list.concat([customer]));
227
+ }
228
+
219
229
  // Test Groups
220
230
  case GET_TEST_GROUPS_REQUESTED:
221
231
  return state.set('isFetchingTestGroups', true)
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Tests for AddTestCustomerButton Component
3
+ *
4
+ * The parent (index.js) only renders this button when channel is EMAIL/SMS and value is valid.
5
+ * This component always renders the button when mounted; visibility is the parent's responsibility.
6
+ */
7
+
8
+ import React from 'react';
9
+ import { render, screen, fireEvent } from '@testing-library/react';
10
+ import { IntlProvider } from 'react-intl';
11
+ import AddTestCustomerButton from '../AddTestCustomer';
12
+
13
+ const mockMessages = {
14
+ 'app.v2Components.TestAndPreviewSlidebox.addTestCustomerWithValue': 'Add {searchValue} as Test Customer',
15
+ };
16
+
17
+ const TestWrapper = ({ children }) => (
18
+ <IntlProvider locale="en" messages={mockMessages}>
19
+ {children}
20
+ </IntlProvider>
21
+ );
22
+
23
+ describe('AddTestCustomerButton', () => {
24
+ const defaultProps = {
25
+ searchValue: 'user@example.com',
26
+ handleAddTestCustomer: jest.fn(),
27
+ };
28
+
29
+ beforeEach(() => {
30
+ jest.clearAllMocks();
31
+ });
32
+
33
+ it('should render button with searchValue in message', () => {
34
+ render(
35
+ <TestWrapper>
36
+ <AddTestCustomerButton {...defaultProps} />
37
+ </TestWrapper>
38
+ );
39
+ const button = screen.getByRole('button', { name: /add.*test customer/i });
40
+ expect(button).toBeTruthy();
41
+ });
42
+
43
+ it('should show searchValue in button text', () => {
44
+ render(
45
+ <TestWrapper>
46
+ <AddTestCustomerButton {...defaultProps} searchValue="user@example.com" />
47
+ </TestWrapper>
48
+ );
49
+ expect(screen.getByRole('button', { name: /user@example.com/i })).toBeTruthy();
50
+ });
51
+
52
+ it('should call handleAddTestCustomer when button is clicked', () => {
53
+ const handleAddTestCustomer = jest.fn();
54
+ render(
55
+ <TestWrapper>
56
+ <AddTestCustomerButton
57
+ searchValue="user@example.com"
58
+ handleAddTestCustomer={handleAddTestCustomer}
59
+ />
60
+ </TestWrapper>
61
+ );
62
+ const button = screen.getByRole('button', { name: /add.*test customer/i });
63
+ fireEvent.click(button);
64
+ expect(handleAddTestCustomer).toHaveBeenCalledTimes(1);
65
+ });
66
+ });