@capillarytech/creatives-library 8.0.329 → 8.0.330

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 (122) hide show
  1. package/constants/unified.js +0 -14
  2. package/package.json +1 -1
  3. package/services/api.js +0 -17
  4. package/services/tests/api.test.js +0 -85
  5. package/utils/commonUtils.js +0 -10
  6. package/utils/tests/commonUtil.test.js +0 -169
  7. package/v2Components/CapTagList/index.js +0 -10
  8. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +49 -70
  9. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +2 -8
  10. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +21 -207
  11. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +0 -16
  12. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +10 -85
  13. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +0 -30
  14. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +11 -79
  15. package/v2Components/CommonTestAndPreview/SendTestMessage.js +53 -87
  16. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +1 -20
  17. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +4 -133
  18. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +34 -145
  19. package/v2Components/CommonTestAndPreview/actions.js +0 -10
  20. package/v2Components/CommonTestAndPreview/constants.js +1 -53
  21. package/v2Components/CommonTestAndPreview/index.js +168 -1006
  22. package/v2Components/CommonTestAndPreview/messages.js +3 -147
  23. package/v2Components/CommonTestAndPreview/reducer.js +0 -10
  24. package/v2Components/CommonTestAndPreview/sagas.js +6 -15
  25. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +286 -328
  26. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +65 -231
  27. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +5 -118
  28. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +0 -341
  29. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +24 -65
  30. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +1 -199
  31. package/v2Components/CommonTestAndPreview/tests/constants.test.js +1 -31
  32. package/v2Components/CommonTestAndPreview/tests/index.test.js +4 -168
  33. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +0 -71
  34. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  35. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +0 -17
  36. package/v2Components/FormBuilder/index.js +1 -7
  37. package/v2Components/TestAndPreviewSlidebox/index.js +1 -8
  38. package/v2Components/TestAndPreviewSlidebox/sagas.js +4 -11
  39. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +1 -3
  40. package/v2Containers/CreativesContainer/SlideBoxContent.js +4 -36
  41. package/v2Containers/CreativesContainer/SlideBoxFooter.js +1 -10
  42. package/v2Containers/CreativesContainer/SlideBoxHeader.js +4 -29
  43. package/v2Containers/CreativesContainer/constants.js +0 -9
  44. package/v2Containers/CreativesContainer/index.js +93 -286
  45. package/v2Containers/CreativesContainer/index.scss +1 -51
  46. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +34 -78
  47. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +16 -79
  48. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +0 -8
  49. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +98 -357
  50. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +10 -20
  51. package/v2Containers/CreativesContainer/tests/index.test.js +9 -71
  52. package/v2Containers/Rcs/constants.js +1 -34
  53. package/v2Containers/Rcs/index.js +884 -999
  54. package/v2Containers/Rcs/index.scss +6 -85
  55. package/v2Containers/Rcs/messages.js +1 -10
  56. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +2453 -41456
  57. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +5 -0
  58. package/v2Containers/Rcs/tests/index.test.js +38 -41
  59. package/v2Containers/Rcs/tests/mockData.js +0 -38
  60. package/v2Containers/Rcs/tests/utils.test.js +1 -379
  61. package/v2Containers/Rcs/utils.js +10 -358
  62. package/v2Containers/Sms/Create/index.js +38 -100
  63. package/v2Containers/SmsTrai/Create/index.js +4 -9
  64. package/v2Containers/SmsTrai/Edit/constants.js +0 -2
  65. package/v2Containers/SmsTrai/Edit/index.js +128 -609
  66. package/v2Containers/SmsTrai/Edit/messages.js +4 -9
  67. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +2600 -4586
  68. package/v2Containers/SmsWrapper/index.js +8 -37
  69. package/v2Containers/TagList/index.js +0 -6
  70. package/v2Containers/Templates/_templates.scss +2 -63
  71. package/v2Containers/Templates/actions.js +0 -11
  72. package/v2Containers/Templates/constants.js +0 -2
  73. package/v2Containers/Templates/index.js +40 -90
  74. package/v2Containers/Templates/sagas.js +12 -57
  75. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1079 -1043
  76. package/v2Containers/Templates/tests/sagas.test.js +123 -193
  77. package/v2Containers/TemplatesV2/TemplatesV2.style.js +1 -72
  78. package/v2Containers/TemplatesV2/index.js +23 -86
  79. package/v2Containers/Whatsapp/index.js +20 -3
  80. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +4872 -5790
  81. package/utils/templateVarUtils.js +0 -172
  82. package/utils/tests/templateVarUtils.test.js +0 -160
  83. package/v2Components/CommonTestAndPreview/AddTestCustomer.js +0 -42
  84. package/v2Components/CommonTestAndPreview/CustomerCreationModal.js +0 -155
  85. package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +0 -93
  86. package/v2Components/CommonTestAndPreview/previewApiUtils.js +0 -59
  87. package/v2Components/CommonTestAndPreview/tests/AddTestCustomer.test.js +0 -66
  88. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +0 -648
  89. package/v2Components/CommonTestAndPreview/tests/CustomerCreationModal.test.js +0 -174
  90. package/v2Components/CommonTestAndPreview/tests/ExistingCustomerModal.test.js +0 -114
  91. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +0 -67
  92. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +0 -87
  93. package/v2Components/SmsFallback/constants.js +0 -73
  94. package/v2Components/SmsFallback/index.js +0 -955
  95. package/v2Components/SmsFallback/index.scss +0 -265
  96. package/v2Components/SmsFallback/messages.js +0 -78
  97. package/v2Components/SmsFallback/smsFallbackUtils.js +0 -107
  98. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +0 -50
  99. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +0 -147
  100. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +0 -304
  101. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +0 -197
  102. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +0 -261
  103. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +0 -422
  104. package/v2Components/SmsFallback/useLocalTemplateList.js +0 -92
  105. package/v2Components/VarSegmentMessageEditor/constants.js +0 -2
  106. package/v2Components/VarSegmentMessageEditor/index.js +0 -125
  107. package/v2Components/VarSegmentMessageEditor/index.scss +0 -46
  108. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +0 -43
  109. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +0 -67
  110. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +0 -90
  111. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +0 -258
  112. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +0 -125
  113. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +0 -205
  114. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +0 -251
  115. package/v2Containers/Sms/smsFormDataHelpers.js +0 -67
  116. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +0 -253
  117. package/v2Containers/SmsTrai/Edit/index.scss +0 -121
  118. package/v2Containers/Templates/TemplatesActionBar.js +0 -101
  119. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +0 -120
  120. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +0 -180
  121. package/v2Containers/Templates/utils/smsTemplatesListApi.js +0 -79
  122. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +0 -131
@@ -301,6 +301,11 @@ exports[`RCS utils getRCSContent renders RCS content with no media 1`] = `
301
301
  <div
302
302
  className="cap-rcs-creatives"
303
303
  >
304
+ <div
305
+ className="CapLabel-n7zsf5-0 gtGqsG rcs-listing-content title"
306
+ fontWeight="bold"
307
+ type="label19"
308
+ />
304
309
  <div
305
310
  className="CapLabel-n7zsf5-0 ekUKMg rcs-listing-content desc"
306
311
  type="label19"
@@ -15,14 +15,6 @@ import CapActionButton from '../../../v2Components/CapActionButton';
15
15
  import { INITIAL_SUGGESTIONS_DATA_STOP } from '../constants';
16
16
 
17
17
 
18
- jest.mock('react-redux', () => {
19
- const actual = jest.requireActual('react-redux');
20
- return {
21
- ...actual,
22
- useDispatch: jest.fn(() => jest.fn()),
23
- };
24
- });
25
-
26
18
  jest.mock('../../../v2Containers/TagList/index.js', () => ({
27
19
  __esModule: true,
28
20
  default: (props) => (
@@ -150,7 +142,7 @@ const renderHelper = (args) => {
150
142
  isEditFlow={args.isEditFlow || false}
151
143
  loadingTags={false}
152
144
  metaEntities={[]}
153
- {...(args.omitGetDefaultTags ? {} : { getDefaultTags: true })}
145
+ getDefaultTags
154
146
  isDltEnabled={args.isDltEnabled || false}
155
147
  smsRegister={'DLT'}
156
148
  />
@@ -453,8 +445,8 @@ describe('Creatives rcs test/>', () => {
453
445
  });
454
446
 
455
447
  it('should call fetchSchemaForEntity when TagList context changes (non-full mode)', () => {
456
- // omitGetDefaultTags: when getDefaultTags is true it overwrites query.context and blocks context switches
457
- renderHelper({ isFullMode: false, omitGetDefaultTags: true });
448
+ // Re-render in non-full mode so TagList is visible
449
+ renderHelper({ isFullMode: false });
458
450
  const tagList = renderedComponent.find('.tag-mock').at(0);
459
451
  expect(tagList.exists()).toBe(true);
460
452
  const before = fetchSchemaForEntity.mock.calls.length;
@@ -462,7 +454,7 @@ describe('Creatives rcs test/>', () => {
462
454
  tagList.prop('onContextChange')('ALL');
463
455
  });
464
456
  const after = fetchSchemaForEntity.mock.calls.length;
465
- expect(after).toBeGreaterThan(before);
457
+ expect(after).toBe(before + 1);
466
458
  const lastArg = fetchSchemaForEntity.mock.calls[after - 1][0];
467
459
  expect(lastArg).toEqual(expect.objectContaining({ layout: 'SMS', type: 'TAG', context: 'default' }));
468
460
  });
@@ -638,16 +630,15 @@ describe('RCS createPayload', () => {
638
630
  expect(payloadArg.versions.base.content.RCS.rcsContent.accessToken).toBe('secret-token');
639
631
  expect(payloadArg.versions.base.content.RCS.rcsContent.hostName).toBe('rcs.host.example.com');
640
632
  expect(payloadArg.versions.base.content.RCS.rcsContent.accountName).toBe('Brand RCS Account');
641
- // templateType effect (text_message → rich_card) can reset selectedDimension after hydration on first mount;
642
- // payload cardSettings follow `selectedDimension` state (see Rcs createPayload).
643
633
  expect(payloadArg.versions.base.content.RCS.rcsContent.cardSettings).toEqual(
644
- expect.objectContaining({ cardOrientation: 'VERTICAL', cardWidth: 'SMALL' }),
634
+ expect.objectContaining({ cardOrientation: 'HORIZONTAL', mediaAlignment: 'LEFT', cardWidth: 'SMALL' }),
645
635
  );
646
- // First-mount templateType effect vs hydration can leave templateMediaType as NONE; payload follows state.
647
- expect(card.mediaType).toBe('NONE');
648
- // SMS fallback is only included when smsFallbackData is set (SmsFallback), not an empty stub
649
- expect(payloadArg.versions.base.content.RCS.smsFallBackContent).toBeUndefined();
650
- }, 30000);
636
+ expect(card.mediaType).toBe('IMAGE');
637
+ expect(card.media).toEqual(expect.objectContaining({ mediaUrl: 'https://cdn.example.com/img.png' }));
638
+ expect(payloadArg.versions.base.content.RCS.smsFallBackContent).toEqual(
639
+ expect.objectContaining({ message: '' }),
640
+ );
641
+ });
651
642
 
652
643
  it('should include empty account metadata when no account is selected', async () => {
653
644
  const createRcsTemplate = jest.fn();
@@ -713,7 +704,7 @@ describe('RCS createPayload', () => {
713
704
  expect(rcsContent.accountName).toBe('');
714
705
  });
715
706
 
716
- it('does not add smsFallBackContent until SmsFallback data is set (isDltEnabled is not wired on Rcs)', async () => {
707
+ it('should attach DLT template configs when enabled', async () => {
717
708
  const createRcsTemplate = jest.fn();
718
709
  const getFormData = jest.fn();
719
710
  const { createPayloadFullMode } = mockData;
@@ -766,7 +757,16 @@ describe('RCS createPayload', () => {
766
757
  createRcsTemplate.mock.calls[0]?.[0] ||
767
758
  getFormData.mock.calls[0]?.[0]?.value;
768
759
  expect(payloadArg).toBeDefined();
769
- expect(payloadArg.versions.base.content.RCS.smsFallBackContent).toBeUndefined();
760
+ expect(payloadArg.versions.base.content.RCS.smsFallBackContent).toEqual(
761
+ expect.objectContaining({
762
+ templateConfigs: expect.objectContaining({
763
+ templateId: '',
764
+ templateName: '',
765
+ template: '',
766
+ registeredSenderIds: [],
767
+ }),
768
+ }),
769
+ );
770
770
  });
771
771
 
772
772
  it('should build expected payload in non-full mode (VIDEO, vertical medium)', () => {
@@ -818,7 +818,7 @@ describe('RCS createPayload', () => {
818
818
  expect(payloadArg.type).toBe('RCS');
819
819
  expect(payloadArg.versions.base.content.RCS.rcsContent.contentType).toBe('RICHCARD');
820
820
  expect(payloadArg.versions.base.content.RCS.rcsContent.cardSettings).toEqual(
821
- expect.objectContaining({ cardOrientation: 'VERTICAL', cardWidth: 'MEDIUM' }),
821
+ expect.objectContaining({ cardOrientation: 'VERTICAL', cardWidth: 'SMALL' }),
822
822
  );
823
823
  expect(card.mediaType).toBe('VIDEO');
824
824
  expect(card.media).toEqual(
@@ -843,9 +843,7 @@ describe('RCS createPayload', () => {
843
843
  // same placeholder appears twice
844
844
  title: 'Hello {{user_name}} and {{user_name}}',
845
845
  description: 'Hi {{user_name}}',
846
- // Rich card so title VarSegment row mounts (NONE → text_message hides title slots)
847
- mediaType: 'IMAGE',
848
- media: { mediaUrl: 'https://cdn.example.com/card.png' },
846
+ mediaType: 'NONE',
849
847
  cardVarMapped: {}, // empty on load
850
848
  suggestions: [],
851
849
  },
@@ -913,8 +911,6 @@ describe('RCS createPayload', () => {
913
911
  });
914
912
 
915
913
  it('should insert TagList label into focused placeholder and sync duplicates in non-full mode', () => {
916
- // One title slot so numeric slot key + semantic user_name stay aligned (duplicate {{user_name}} would
917
- // leave other slots on empty "2","3",… keys from hydration).
918
914
  const templateData = {
919
915
  name: 'DupVarsTemplate2',
920
916
  versions: {
@@ -926,11 +922,9 @@ describe('RCS createPayload', () => {
926
922
  cardSettings: { cardOrientation: 'VERTICAL', cardWidth: 'SMALL' },
927
923
  cardContent: [
928
924
  {
929
- title: 'Hello {{user_name}}',
930
- description: 'Hi',
931
- // Rich card so title VarSegment mounts (NONE → text_message hides title row)
932
- mediaType: 'IMAGE',
933
- media: { mediaUrl: 'https://cdn.example.com/card.png' },
925
+ title: 'Hello {{user_name}} and {{user_name}}',
926
+ description: 'Hi {{user_name}}',
927
+ mediaType: 'NONE',
934
928
  cardVarMapped: {},
935
929
  suggestions: [],
936
930
  },
@@ -978,12 +972,12 @@ describe('RCS createPayload', () => {
978
972
  const id = n.prop('id') || '';
979
973
  return id.includes('{{user_name}}_');
980
974
  });
981
- expect(titleVarAreas.length).toBeGreaterThanOrEqual(1);
975
+ expect(titleVarAreas.length).toBeGreaterThanOrEqual(2);
982
976
 
983
- // VarSegmentMessageEditor calls onFocus(id) with the slot id string (not a DOM event)
977
+ // Focus the first variable textarea so TagList knows where to insert
984
978
  act(() => {
985
979
  const id = titleVarAreas.at(0).prop('id');
986
- titleVarAreas.at(0).props().onFocus(id);
980
+ titleVarAreas.at(0).props().onFocus({ target: { id } });
987
981
  });
988
982
  wrapper.update();
989
983
 
@@ -999,6 +993,7 @@ describe('RCS createPayload', () => {
999
993
  return id.includes('{{user_name}}_');
1000
994
  });
1001
995
  expect(updatedTitleVarAreas.at(0).prop('value')).toBe('{{first_name}}');
996
+ expect(updatedTitleVarAreas.at(1).prop('value')).toBe('{{first_name}}');
1002
997
  });
1003
998
 
1004
999
  it('should keep two tags + freetext inside the variable textarea in non-full mode edit (not merged into static text)', () => {
@@ -1066,11 +1061,13 @@ describe('RCS createPayload', () => {
1066
1061
 
1067
1062
  wrapper.update();
1068
1063
 
1069
- // At least one variable slot is rendered (ids like `{{token}}_0`)
1070
- const varSlotAreas = wrapper.find('TextArea').filterWhere((n) =>
1071
- /\{\{[^}]+\}}_\d+/.test(String(n.prop('id') || '')),
1072
- );
1073
- expect(varSlotAreas.length).toBeGreaterThan(0);
1064
+ // The placeholder {{service_type}} should exist as a variable textarea id, and its value should be the mixed string.
1065
+ const serviceTypeAreas = wrapper.find('TextArea').filterWhere((n) => {
1066
+ const id = n.prop('id') || '';
1067
+ return id.includes('{{service_type}}_');
1068
+ });
1069
+ expect(serviceTypeAreas.length).toBeGreaterThanOrEqual(1);
1070
+ expect(serviceTypeAreas.at(0).prop('value')).toBe('{{first_name}}{{adv}}freeText');
1074
1071
 
1075
1072
  // Ensure freetext does NOT leak into static text blocks.
1076
1073
  const staticAreas = wrapper.find('TextArea').filterWhere((n) => !!n.prop('disabled'));
@@ -148,7 +148,6 @@ export const mockData = {
148
148
  },
149
149
  accountData: {
150
150
  selectedRcsAccount: {
151
- id: 'we-crm-account-id-42',
152
151
  sourceAccountIdentifier: 'rcs-account-123',
153
152
  configs: { accessToken: 'secret-token' },
154
153
  hostName: 'rcs.host.example.com',
@@ -286,41 +285,4 @@ export const mockData = {
286
285
  },
287
286
  },
288
287
  },
289
- // RCS template with SMS fallback for edit-flow tests
290
- rcsTemplateWithSmsFallback: {
291
- templateDetails: {
292
- _id: 'rcs_with_fallback_1',
293
- name: 'RCSWithFallback',
294
- type: 'RCS',
295
- versions: {
296
- base: {
297
- content: {
298
- RCS: {
299
- rcsContent: {
300
- cardSettings: { cardOrientation: 'VERTICAL' },
301
- cardContent: [
302
- {
303
- description: 'RCS description',
304
- mediaType: 'TEXT',
305
- suggestions: [],
306
- },
307
- ],
308
- contentType: 'text_message',
309
- },
310
- smsFallBackContent: {
311
- smsTemplateId: 'sms_fb_001',
312
- smsTemplateName: 'Fallback Template',
313
- smsContent: 'SMS fallback message',
314
- message: 'SMS fallback message',
315
- smsTemplateContent: 'SMS fallback message',
316
- smsVariables: [],
317
- smsVarMapped: {},
318
- smsUnicodeValidity: true,
319
- },
320
- },
321
- },
322
- },
323
- },
324
- },
325
- },
326
288
  };
@@ -1,21 +1,7 @@
1
1
  import React from 'react';
2
2
  import renderer from 'react-test-renderer';
3
3
  import { render, screen } from '../../../utils/test-utils';
4
- import {
5
- getRCSContent,
6
- getRcsStatusType,
7
- getTemplateStatusType,
8
- normalizeCardVarMapped,
9
- coalesceCardVarMappedToTemplate,
10
- resolveCardVarMappedSlotValue,
11
- isRcsTextOnlyCardMediaType,
12
- mapRcsCardContentForConsumerWithResolvedTags,
13
- resolveRcsCardPreviewStrings,
14
- sanitizeCardVarMappedValue,
15
- areAllRcsSmsFallbackVarSlotsFilled,
16
- buildRcsNumericMustachePlaceholderRegex,
17
- } from '../utils';
18
- import { rcsVarRegex } from '../constants';
4
+ import { getRCSContent, getRcsStatusType, getTemplateStatusType } from '../utils';
19
5
  import { RCS, RCS_BUTTON_TYPES, STATUS_OPTIONS, RCS_STATUSES } from '../constants';
20
6
  import { mockData } from './mockData';
21
7
 
@@ -73,25 +59,6 @@ describe('RCS utils - renderRcsSuggestionsPreview', () => {
73
59
  expect(labels.length).toBe(1);
74
60
  expect(labels[0].textContent).toContain('Call');
75
61
  });
76
-
77
- it('renders only divider for unknown suggestion type', () => {
78
- const template = JSON.parse(JSON.stringify(templateBase));
79
- template.versions.base.content[RCS].rcsContent.cardContent[0].suggestions = [
80
- { type: RCS_BUTTON_TYPES.NONE, text: 'Ignored' },
81
- ];
82
- render(getRCSContent(template));
83
- expect(document.querySelectorAll('.rcs-cta-preview').length).toBe(0);
84
- expect(document.querySelectorAll('.whatsapp-divider').length).toBeGreaterThan(0);
85
- });
86
-
87
- it('prefers thumbnailUrl over mediaUrl for preview image', () => {
88
- const template = JSON.parse(JSON.stringify(templateBase));
89
- const card = template.versions.base.content[RCS].rcsContent.cardContent[0];
90
- card.media = { thumbnailUrl: 'thumb.jpg', mediaUrl: 'full.jpg' };
91
- render(getRCSContent(template));
92
- const img = document.querySelector('.rcs-listing-image');
93
- expect(img?.getAttribute('src')).toBe('thumb.jpg');
94
- });
95
62
  });
96
63
 
97
64
  describe('RCS utils', () => {
@@ -169,349 +136,4 @@ describe('RCS utils', () => {
169
136
  expect(getTemplateStatusType('some_unknown_status')).toBe('warning');
170
137
  });
171
138
  });
172
-
173
- describe('normalizeCardVarMapped', () => {
174
- it('maps numeric key + placeholder value to tag name', () => {
175
- expect(normalizeCardVarMapped({ 1: '{{user_id_b64}}' })).toEqual({ user_id_b64: '' });
176
- });
177
-
178
- it('maps numeric key + literal value when orderedTagNames is provided', () => {
179
- expect(
180
- normalizeCardVarMapped({ 1: 'hello' }, ['user_id_b64']),
181
- ).toEqual({ user_id_b64: 'hello' });
182
- });
183
-
184
- it('keeps numeric key when no orderedTagNames for literal value', () => {
185
- expect(normalizeCardVarMapped({ 1: 'hello' })).toEqual({ 1: 'hello' });
186
- });
187
-
188
- it('keeps semantic key + different tag token (TagList value)', () => {
189
- expect(normalizeCardVarMapped({ myKey: '{{tag}}' })).toEqual({ myKey: '{{tag}}' });
190
- });
191
-
192
- it('maps numeric slot + chosen tag onto template token when ordered names provided', () => {
193
- expect(
194
- normalizeCardVarMapped({ 1: '{{FirstName}}' }, ['user_name']),
195
- ).toEqual({ user_name: '{{FirstName}}' });
196
- });
197
-
198
- it('returns {} for null, undefined, or non-object raw', () => {
199
- expect(normalizeCardVarMapped(null)).toEqual({});
200
- expect(normalizeCardVarMapped(undefined)).toEqual({});
201
- expect(normalizeCardVarMapped('not-object')).toEqual({});
202
- });
203
-
204
- it('maps numeric key to order slot only when index is within orderedTagNames', () => {
205
- expect(normalizeCardVarMapped({ 3: 'z' }, ['a', 'b'])).toEqual({ 3: 'z' });
206
- });
207
-
208
- it('does not overwrite a literal slot value with a duplicate semantic placeholder entry', () => {
209
- expect(
210
- normalizeCardVarMapped(
211
- { 1: 'hello', user_id_b64: '{{user_id_b64}}' },
212
- ['user_id_b64'],
213
- ),
214
- ).toEqual({ user_id_b64: 'hello' });
215
- });
216
- });
217
-
218
- describe('coalesceCardVarMappedToTemplate', () => {
219
- it('maps slot 1 to first token name from title', () => {
220
- expect(
221
- coalesceCardVarMappedToTemplate(
222
- { 1: 'abc' },
223
- 'Hi {{user_name}}',
224
- '',
225
- rcsVarRegex,
226
- ),
227
- ).toEqual({ 1: 'abc', user_name: 'abc' });
228
- });
229
-
230
- it('keeps {{1}} style token as key 1', () => {
231
- expect(
232
- coalesceCardVarMappedToTemplate(
233
- { 1: '' },
234
- 'Hi {{1}}',
235
- '',
236
- rcsVarRegex,
237
- ),
238
- ).toEqual({ 1: '' });
239
- });
240
-
241
- it('returns clone when no tokens', () => {
242
- const raw = { 1: 'x' };
243
- const out = coalesceCardVarMappedToTemplate(raw, 'no vars', '', rcsVarRegex);
244
- expect(out).toEqual({ 1: 'x' });
245
- expect(out).not.toBe(raw);
246
- });
247
-
248
- it('merges tokens from title and description', () => {
249
- const out = coalesceCardVarMappedToTemplate(
250
- { a: '1', b: '2' },
251
- 'T {{a}}',
252
- 'D {{b}}',
253
- rcsVarRegex,
254
- );
255
- expect(out).toEqual({ a: '1', b: '2', 1: '1', 2: '2' });
256
- });
257
-
258
- it('returns {} when there are no tokens and raw is null or not an object', () => {
259
- expect(coalesceCardVarMappedToTemplate(null, 'plain', '', rcsVarRegex)).toEqual({});
260
- expect(coalesceCardVarMappedToTemplate(undefined, 'plain', '', rcsVarRegex)).toEqual({});
261
- });
262
-
263
- it('fills token values from legacy numeric slots when named key is missing', () => {
264
- expect(
265
- coalesceCardVarMappedToTemplate(
266
- { 1: 'legacy-val' },
267
- 'Hello {{name}}',
268
- '',
269
- rcsVarRegex,
270
- ),
271
- ).toEqual({ 1: 'legacy-val', name: 'legacy-val' });
272
- });
273
-
274
- it('maps values when raw is non-null but missing named keys', () => {
275
- expect(
276
- coalesceCardVarMappedToTemplate(
277
- {},
278
- 'X {{a}}',
279
- '',
280
- rcsVarRegex,
281
- ),
282
- ).toEqual({ 1: '', a: '' });
283
- });
284
- });
285
-
286
- describe('resolveCardVarMappedSlotValue', () => {
287
- it('prefers per-slot numeric key when both semantic and numeric are present', () => {
288
- expect(
289
- resolveCardVarMappedSlotValue({ user_name: 'A', 1: 'B' }, 'user_name', 0),
290
- ).toBe('B');
291
- });
292
-
293
- it('falls back to slot 1 when name missing', () => {
294
- expect(resolveCardVarMappedSlotValue({ 1: 'legacy' }, 'user_name', 0)).toBe('legacy');
295
- });
296
-
297
- it('uses global slot index for second variable', () => {
298
- expect(resolveCardVarMappedSlotValue({ 2: 'second' }, 'b', 1)).toBe('second');
299
- });
300
-
301
- it('reads slot from string slot key when varName has no direct mapping', () => {
302
- expect(resolveCardVarMappedSlotValue({ 1: 'only-slot' }, 'missing', 0)).toBe('only-slot');
303
- });
304
-
305
- it('reads numeric object key for slot when string slot key is absent', () => {
306
- expect(resolveCardVarMappedSlotValue({ 2: 'n2' }, 'x', 1)).toBe('n2');
307
- });
308
-
309
- it('returns empty when named key is cleared, not numeric slot fallback', () => {
310
- expect(
311
- resolveCardVarMappedSlotValue({ user_name: '', 1: 'old value' }, 'user_name', 0),
312
- ).toBe('');
313
- });
314
-
315
- it('library mode: semantic empty does not hide non-empty numeric slot (campaign / journey payload)', () => {
316
- expect(
317
- resolveCardVarMappedSlotValue(
318
- { user_id_b64: '', 1: 'selected-from-library' },
319
- 'user_id_b64',
320
- 0,
321
- true,
322
- ),
323
- ).toBe('selected-from-library');
324
- });
325
-
326
- it('empty numeric slot falls back to semantic tag (preview after hydration leaves 1:"")', () => {
327
- expect(
328
- resolveCardVarMappedSlotValue(
329
- { promotion_points: '{{loyalty_points}}', 1: '' },
330
- 'promotion_points',
331
- 0,
332
- ),
333
- ).toBe('{{loyalty_points}}');
334
- });
335
- });
336
-
337
- describe('mapRcsCardContentForConsumerWithResolvedTags', () => {
338
- it('sets title and description to resolved tag strings on each card', () => {
339
- const out = mapRcsCardContentForConsumerWithResolvedTags(
340
- [
341
- {
342
- title: '',
343
- description: 'Visit {{gt}} discount',
344
- mediaType: 'NONE',
345
- cardVarMapped: { gt: '{{loyalty_points}}', 1: '{{loyalty_points}}' },
346
- },
347
- ],
348
- {},
349
- false,
350
- );
351
- expect(out[0].title).toBe('');
352
- expect(out[0].description).toBe('Visit {{loyalty_points}} discount');
353
- });
354
-
355
- it('merges root rcsCardVarMapped with nested cardVarMapped', () => {
356
- const out = mapRcsCardContentForConsumerWithResolvedTags(
357
- [
358
- {
359
- title: 'Hi {{first_name}}',
360
- description: 'Pts {{promotion_points}}',
361
- mediaType: 'IMAGE',
362
- cardVarMapped: {
363
- promotion_points: '{{loyalty_points}}',
364
- 2: '{{loyalty_points}}',
365
- },
366
- },
367
- ],
368
- {
369
- first_name: '{{customer_name}}',
370
- 1: '{{customer_name}}',
371
- },
372
- false,
373
- );
374
- expect(out[0].title).toBe('Hi {{customer_name}}');
375
- expect(out[0].description).toBe('Pts {{loyalty_points}}');
376
- });
377
- });
378
-
379
- describe('isRcsTextOnlyCardMediaType', () => {
380
- it('returns true for NONE and TEXT (text message card)', () => {
381
- expect(isRcsTextOnlyCardMediaType('NONE')).toBe(true);
382
- expect(isRcsTextOnlyCardMediaType('TEXT')).toBe(true);
383
- expect(isRcsTextOnlyCardMediaType('text')).toBe(true);
384
- });
385
- it('returns false for rich card media', () => {
386
- expect(isRcsTextOnlyCardMediaType('IMAGE')).toBe(false);
387
- expect(isRcsTextOnlyCardMediaType('VIDEO')).toBe(false);
388
- });
389
- });
390
-
391
- describe('resolveRcsCardPreviewStrings', () => {
392
- it('substitutes mapped tags for title and description (campaign preview parity)', () => {
393
- const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
394
- 'Hi {{first_name}}',
395
- 'Pts {{promotion_points}}',
396
- {
397
- first_name: '{{customer_name}}',
398
- promotion_points: '{{loyalty_points}}',
399
- 1: '{{customer_name}}',
400
- 2: '{{loyalty_points}}',
401
- },
402
- true,
403
- );
404
- expect(rcsTitle).toBe('Hi {{customer_name}}');
405
- expect(rcsDesc).toBe('Pts {{loyalty_points}}');
406
- });
407
-
408
- it('leaves placeholders when map has no value', () => {
409
- const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
410
- '{{a}}',
411
- '{{b}}',
412
- {},
413
- false,
414
- );
415
- expect(rcsTitle).toBe('{{a}}');
416
- expect(rcsDesc).toBe('{{b}}');
417
- });
418
-
419
- it('text-only card: ignores stale title and resolves description from global slot 0', () => {
420
- const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
421
- 'Stale {{old_tag}} title',
422
- 'Visit Store for {{gt}} discount',
423
- { gt: '{{loyalty_points}}', 1: '{{loyalty_points}}' },
424
- true,
425
- true,
426
- );
427
- expect(rcsTitle).toBe('');
428
- expect(rcsDesc).toBe('Visit Store for {{loyalty_points}} discount');
429
- });
430
- });
431
-
432
- describe('sanitizeCardVarMappedValue', () => {
433
- it('returns empty for null', () => {
434
- expect(sanitizeCardVarMappedValue(null)).toBe('');
435
- });
436
-
437
- it('strips numeric-only self-placeholder stored as value', () => {
438
- expect(sanitizeCardVarMappedValue('{{1}}')).toBe('');
439
- expect(sanitizeCardVarMappedValue('{{12}}')).toBe('');
440
- });
441
-
442
- it('keeps semantic mustache values from TagList', () => {
443
- expect(sanitizeCardVarMappedValue('{{FirstName}}')).toBe('{{FirstName}}');
444
- expect(sanitizeCardVarMappedValue('{{user_name}}')).toBe('{{user_name}}');
445
- });
446
-
447
- it('returns original string for non-placeholder values', () => {
448
- expect(sanitizeCardVarMappedValue(' literal ')).toBe(' literal ');
449
- });
450
- });
451
-
452
- describe('areAllRcsSmsFallbackVarSlotsFilled', () => {
453
- it('returns true when template empty', () => {
454
- expect(areAllRcsSmsFallbackVarSlotsFilled('', { '{{a}}_0': 'x' })).toBe(true);
455
- });
456
-
457
- it('returns false when a mustache slot is only whitespace (parity with RCS title/desc vars)', () => {
458
- expect(
459
- areAllRcsSmsFallbackVarSlotsFilled('Hello {{name}}', { '{{name}}_1': ' ' }),
460
- ).toBe(false);
461
- });
462
-
463
- it('returns false when a mustache slot is missing or empty string', () => {
464
- expect(areAllRcsSmsFallbackVarSlotsFilled('Hello {{name}}', {})).toBe(false);
465
- expect(areAllRcsSmsFallbackVarSlotsFilled('Hello {{name}}', { '{{name}}_1': '' })).toBe(
466
- false,
467
- );
468
- });
469
-
470
- it('returns false when a DLT {#var#} slot is empty', () => {
471
- expect(areAllRcsSmsFallbackVarSlotsFilled('Hi {#x#}', { '{#x#}_1': ' ' })).toBe(false);
472
- });
473
-
474
- it('requires mapping for {{optout}} token like any other variable', () => {
475
- expect(areAllRcsSmsFallbackVarSlotsFilled('test {{optout}}', {})).toBe(false);
476
- expect(areAllRcsSmsFallbackVarSlotsFilled('test {{ optout }}', {})).toBe(false);
477
- expect(areAllRcsSmsFallbackVarSlotsFilled('test {{optout}}', { '{{optout}}_1': '{{optout}}' })).toBe(true);
478
- expect(areAllRcsSmsFallbackVarSlotsFilled('test {{optout}}', { '{{optout}}_1': '' })).toBe(false);
479
- });
480
-
481
- it('returns true when template is missing or not a string', () => {
482
- expect(areAllRcsSmsFallbackVarSlotsFilled(null, {})).toBe(true);
483
- expect(areAllRcsSmsFallbackVarSlotsFilled(undefined, {})).toBe(true);
484
- expect(areAllRcsSmsFallbackVarSlotsFilled(123, {})).toBe(true);
485
- });
486
-
487
- it('returns true when all variable slots have non-whitespace values', () => {
488
- expect(
489
- areAllRcsSmsFallbackVarSlotsFilled('Hello {{name}}', { '{{name}}_1': 'Ann' }),
490
- ).toBe(true);
491
- });
492
-
493
- it('coerces non-string slot values with String() so DLT/hydration payloads still count as filled', () => {
494
- expect(
495
- areAllRcsSmsFallbackVarSlotsFilled('Hello {{name}}', { '{{name}}_1': 42 }),
496
- ).toBe(true);
497
- });
498
-
499
- it('accepts legacy 1-based ordinal keys when the only token is at segment index 0 (DLT / API parity)', () => {
500
- expect(areAllRcsSmsFallbackVarSlotsFilled('{#shop#}', { '{#shop#}_1': 'Mart' })).toBe(true);
501
- expect(areAllRcsSmsFallbackVarSlotsFilled('{#shop#}', { '{#shop#}_1': '' })).toBe(false);
502
- });
503
- });
504
-
505
- describe('buildRcsNumericMustachePlaceholderRegex', () => {
506
- it('escapes regex metacharacters in numeric name', () => {
507
- const re = buildRcsNumericMustachePlaceholderRegex('1.5');
508
- expect(re.test('{{1.5}}')).toBe(true);
509
- });
510
-
511
- it('matches simple numeric placeholder', () => {
512
- const re = buildRcsNumericMustachePlaceholderRegex('2');
513
- expect(re.test('{{2}}')).toBe(true);
514
- expect(re.test('{{3}}')).toBe(false);
515
- });
516
- });
517
139
  });