@capillarytech/creatives-library 8.0.316 → 8.0.317-alpha.0

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 (172) hide show
  1. package/constants/unified.js +15 -0
  2. package/package.json +1 -1
  3. package/services/api.js +6 -0
  4. package/services/tests/api.test.js +7 -0
  5. package/utils/common.js +6 -1
  6. package/utils/templateVarUtils.js +172 -0
  7. package/utils/tests/tagValidations.test.js +34 -0
  8. package/utils/tests/templateVarUtils.test.js +160 -0
  9. package/v2Components/CapTagList/index.js +25 -22
  10. package/v2Components/CapTagList/style.scss +48 -0
  11. package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +63 -0
  12. package/v2Components/CapTagListWithInput/index.js +4 -0
  13. package/v2Components/CapWhatsappCTA/index.js +2 -0
  14. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  15. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  16. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  21. package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
  22. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  23. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  24. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
  25. package/v2Components/CommonTestAndPreview/constants.js +38 -0
  26. package/v2Components/CommonTestAndPreview/index.js +693 -155
  27. package/v2Components/CommonTestAndPreview/messages.js +41 -3
  28. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  29. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  30. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +352 -0
  31. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
  32. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  33. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  34. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
  35. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  36. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  37. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  38. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  39. package/v2Components/FormBuilder/index.js +14 -1
  40. package/v2Components/HtmlEditor/HTMLEditor.js +6 -1
  41. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  42. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +927 -2
  43. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +3 -0
  44. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  45. package/v2Components/SmsFallback/constants.js +73 -0
  46. package/v2Components/SmsFallback/index.js +956 -0
  47. package/v2Components/SmsFallback/index.scss +265 -0
  48. package/v2Components/SmsFallback/messages.js +78 -0
  49. package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  50. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  51. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  52. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  53. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  54. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  55. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  56. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  57. package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
  58. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  59. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  60. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  61. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  62. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  63. package/v2Containers/BeeEditor/index.js +3 -0
  64. package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
  65. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
  66. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
  67. package/v2Containers/CommunicationFlow/constants.js +200 -0
  68. package/v2Containers/CommunicationFlow/index.js +102 -0
  69. package/v2Containers/CommunicationFlow/messages.js +346 -0
  70. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
  71. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
  72. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
  73. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
  74. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
  75. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
  76. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
  77. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
  78. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
  79. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
  80. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
  81. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
  82. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
  83. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
  84. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
  85. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
  86. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
  87. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
  88. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
  89. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
  90. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
  91. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
  92. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
  93. package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
  94. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  95. package/v2Containers/CreativesContainer/SlideBoxContent.js +64 -5
  96. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  97. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  98. package/v2Containers/CreativesContainer/constants.js +12 -0
  99. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  100. package/v2Containers/CreativesContainer/index.js +289 -93
  101. package/v2Containers/CreativesContainer/index.scss +51 -1
  102. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  103. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +104 -0
  104. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
  105. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  106. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
  107. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
  108. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  109. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  110. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  111. package/v2Containers/Email/index.js +1 -0
  112. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +7 -1
  113. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +3 -0
  114. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -2
  115. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +16 -1
  116. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +3 -0
  117. package/v2Containers/EmailWrapper/index.js +4 -0
  118. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +1 -0
  119. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +9 -0
  120. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +19 -0
  121. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +3 -0
  122. package/v2Containers/InAppWrapper/index.js +3 -0
  123. package/v2Containers/MobilePush/Create/index.js +2 -0
  124. package/v2Containers/MobilePush/Edit/index.js +2 -0
  125. package/v2Containers/MobilepushWrapper/index.js +3 -1
  126. package/v2Containers/Rcs/constants.js +32 -1
  127. package/v2Containers/Rcs/index.js +951 -873
  128. package/v2Containers/Rcs/index.scss +85 -6
  129. package/v2Containers/Rcs/messages.js +10 -1
  130. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  131. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
  132. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  133. package/v2Containers/Rcs/tests/index.test.js +41 -38
  134. package/v2Containers/Rcs/tests/mockData.js +38 -0
  135. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  136. package/v2Containers/Rcs/tests/utils.test.js +379 -1
  137. package/v2Containers/Rcs/utils.js +358 -10
  138. package/v2Containers/Sms/Create/index.js +83 -36
  139. package/v2Containers/Sms/Edit/index.js +2 -0
  140. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  141. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  142. package/v2Containers/SmsTrai/Create/index.js +9 -4
  143. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  144. package/v2Containers/SmsTrai/Edit/index.js +611 -128
  145. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  146. package/v2Containers/SmsTrai/Edit/messages.js +9 -4
  147. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
  148. package/v2Containers/SmsWrapper/index.js +39 -8
  149. package/v2Containers/TagList/index.js +47 -2
  150. package/v2Containers/TagList/messages.js +4 -0
  151. package/v2Containers/TagList/tests/TagList.test.js +122 -20
  152. package/v2Containers/TagList/tests/mockdata.js +17 -0
  153. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  154. package/v2Containers/Templates/_templates.scss +61 -2
  155. package/v2Containers/Templates/actions.js +11 -0
  156. package/v2Containers/Templates/constants.js +2 -0
  157. package/v2Containers/Templates/index.js +90 -40
  158. package/v2Containers/Templates/sagas.js +57 -12
  159. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  160. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  161. package/v2Containers/Templates/tests/sagas.test.js +193 -12
  162. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  163. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  164. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  165. package/v2Containers/TemplatesV2/index.js +86 -23
  166. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  167. package/v2Containers/Viber/index.js +5 -0
  168. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +0 -2
  169. package/v2Containers/WebPush/Create/index.js +9 -1
  170. package/v2Containers/Whatsapp/index.js +8 -20
  171. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +598 -34
  172. package/v2Containers/Zalo/index.js +2 -0
@@ -301,11 +301,6 @@ 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
- />
309
304
  <div
310
305
  className="CapLabel-n7zsf5-0 ekUKMg rcs-listing-content desc"
311
306
  type="label19"
@@ -15,6 +15,14 @@ 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
+
18
26
  jest.mock('../../../v2Containers/TagList/index.js', () => ({
19
27
  __esModule: true,
20
28
  default: (props) => (
@@ -142,7 +150,7 @@ const renderHelper = (args) => {
142
150
  isEditFlow={args.isEditFlow || false}
143
151
  loadingTags={false}
144
152
  metaEntities={[]}
145
- getDefaultTags
153
+ {...(args.omitGetDefaultTags ? {} : { getDefaultTags: true })}
146
154
  isDltEnabled={args.isDltEnabled || false}
147
155
  smsRegister={'DLT'}
148
156
  />
@@ -445,8 +453,8 @@ describe('Creatives rcs test/>', () => {
445
453
  });
446
454
 
447
455
  it('should call fetchSchemaForEntity when TagList context changes (non-full mode)', () => {
448
- // Re-render in non-full mode so TagList is visible
449
- renderHelper({ isFullMode: false });
456
+ // omitGetDefaultTags: when getDefaultTags is true it overwrites query.context and blocks context switches
457
+ renderHelper({ isFullMode: false, omitGetDefaultTags: true });
450
458
  const tagList = renderedComponent.find('.tag-mock').at(0);
451
459
  expect(tagList.exists()).toBe(true);
452
460
  const before = fetchSchemaForEntity.mock.calls.length;
@@ -454,7 +462,7 @@ describe('Creatives rcs test/>', () => {
454
462
  tagList.prop('onContextChange')('ALL');
455
463
  });
456
464
  const after = fetchSchemaForEntity.mock.calls.length;
457
- expect(after).toBe(before + 1);
465
+ expect(after).toBeGreaterThan(before);
458
466
  const lastArg = fetchSchemaForEntity.mock.calls[after - 1][0];
459
467
  expect(lastArg).toEqual(expect.objectContaining({ layout: 'SMS', type: 'TAG', context: 'default' }));
460
468
  });
@@ -630,15 +638,16 @@ describe('RCS createPayload', () => {
630
638
  expect(payloadArg.versions.base.content.RCS.rcsContent.accessToken).toBe('secret-token');
631
639
  expect(payloadArg.versions.base.content.RCS.rcsContent.hostName).toBe('rcs.host.example.com');
632
640
  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).
633
643
  expect(payloadArg.versions.base.content.RCS.rcsContent.cardSettings).toEqual(
634
- expect.objectContaining({ cardOrientation: 'HORIZONTAL', mediaAlignment: 'LEFT', cardWidth: 'SMALL' }),
635
- );
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: '' }),
644
+ expect.objectContaining({ cardOrientation: 'VERTICAL', cardWidth: 'SMALL' }),
640
645
  );
641
- });
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);
642
651
 
643
652
  it('should include empty account metadata when no account is selected', async () => {
644
653
  const createRcsTemplate = jest.fn();
@@ -704,7 +713,7 @@ describe('RCS createPayload', () => {
704
713
  expect(rcsContent.accountName).toBe('');
705
714
  });
706
715
 
707
- it('should attach DLT template configs when enabled', async () => {
716
+ it('does not add smsFallBackContent until SmsFallback data is set (isDltEnabled is not wired on Rcs)', async () => {
708
717
  const createRcsTemplate = jest.fn();
709
718
  const getFormData = jest.fn();
710
719
  const { createPayloadFullMode } = mockData;
@@ -757,16 +766,7 @@ describe('RCS createPayload', () => {
757
766
  createRcsTemplate.mock.calls[0]?.[0] ||
758
767
  getFormData.mock.calls[0]?.[0]?.value;
759
768
  expect(payloadArg).toBeDefined();
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
- );
769
+ expect(payloadArg.versions.base.content.RCS.smsFallBackContent).toBeUndefined();
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: 'SMALL' }),
821
+ expect.objectContaining({ cardOrientation: 'VERTICAL', cardWidth: 'MEDIUM' }),
822
822
  );
823
823
  expect(card.mediaType).toBe('VIDEO');
824
824
  expect(card.media).toEqual(
@@ -843,7 +843,9 @@ 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
- mediaType: 'NONE',
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' },
847
849
  cardVarMapped: {}, // empty on load
848
850
  suggestions: [],
849
851
  },
@@ -911,6 +913,8 @@ describe('RCS createPayload', () => {
911
913
  });
912
914
 
913
915
  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).
914
918
  const templateData = {
915
919
  name: 'DupVarsTemplate2',
916
920
  versions: {
@@ -922,9 +926,11 @@ describe('RCS createPayload', () => {
922
926
  cardSettings: { cardOrientation: 'VERTICAL', cardWidth: 'SMALL' },
923
927
  cardContent: [
924
928
  {
925
- title: 'Hello {{user_name}} and {{user_name}}',
926
- description: 'Hi {{user_name}}',
927
- mediaType: 'NONE',
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' },
928
934
  cardVarMapped: {},
929
935
  suggestions: [],
930
936
  },
@@ -972,12 +978,12 @@ describe('RCS createPayload', () => {
972
978
  const id = n.prop('id') || '';
973
979
  return id.includes('{{user_name}}_');
974
980
  });
975
- expect(titleVarAreas.length).toBeGreaterThanOrEqual(2);
981
+ expect(titleVarAreas.length).toBeGreaterThanOrEqual(1);
976
982
 
977
- // Focus the first variable textarea so TagList knows where to insert
983
+ // VarSegmentMessageEditor calls onFocus(id) with the slot id string (not a DOM event)
978
984
  act(() => {
979
985
  const id = titleVarAreas.at(0).prop('id');
980
- titleVarAreas.at(0).props().onFocus({ target: { id } });
986
+ titleVarAreas.at(0).props().onFocus(id);
981
987
  });
982
988
  wrapper.update();
983
989
 
@@ -993,7 +999,6 @@ describe('RCS createPayload', () => {
993
999
  return id.includes('{{user_name}}_');
994
1000
  });
995
1001
  expect(updatedTitleVarAreas.at(0).prop('value')).toBe('{{first_name}}');
996
- expect(updatedTitleVarAreas.at(1).prop('value')).toBe('{{first_name}}');
997
1002
  });
998
1003
 
999
1004
  it('should keep two tags + freetext inside the variable textarea in non-full mode edit (not merged into static text)', () => {
@@ -1061,13 +1066,11 @@ describe('RCS createPayload', () => {
1061
1066
 
1062
1067
  wrapper.update();
1063
1068
 
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');
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);
1071
1074
 
1072
1075
  // Ensure freetext does NOT leak into static text blocks.
1073
1076
  const staticAreas = wrapper.find('TextArea').filterWhere((n) => !!n.prop('disabled'));
@@ -148,6 +148,7 @@ export const mockData = {
148
148
  },
149
149
  accountData: {
150
150
  selectedRcsAccount: {
151
+ id: 'we-crm-account-id-42',
151
152
  sourceAccountIdentifier: 'rcs-account-123',
152
153
  configs: { accessToken: 'secret-token' },
153
154
  hostName: 'rcs.host.example.com',
@@ -285,4 +286,41 @@ export const mockData = {
285
286
  },
286
287
  },
287
288
  },
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
+ },
288
326
  };
@@ -0,0 +1,251 @@
1
+ import {
2
+ getUnmappedDesc,
3
+ hasRcsVarTokens,
4
+ normalizeLibraryLoadedTitleDesc,
5
+ mergeRcsSmsFallBackContentFromDetails,
6
+ mergeRcsSmsFallbackVarMapLayers,
7
+ pickFirstSmsFallbackTemplateString,
8
+ syncCardVarMappedSemanticsFromSlots,
9
+ hasMeaningfulSmsFallbackShape,
10
+ getLibrarySmsFallbackApiBaselineFromTemplateData,
11
+ } from '../rcsLibraryHydrationUtils';
12
+ import { rcsVarRegex } from '../constants';
13
+
14
+ describe('rcsLibraryHydrationUtils', () => {
15
+ describe('mergeRcsSmsFallBackContentFromDetails', () => {
16
+ it('returns empty object for missing details', () => {
17
+ expect(mergeRcsSmsFallBackContentFromDetails(null)).toEqual({});
18
+ expect(mergeRcsSmsFallBackContentFromDetails(undefined)).toEqual({});
19
+ });
20
+
21
+ it('merges nested RCS smsFallBackContent with root mirror; nested wins on overlap', () => {
22
+ const details = {
23
+ versions: {
24
+ base: {
25
+ content: {
26
+ RCS: {
27
+ smsFallBackContent: {
28
+ smsTemplateName: 'OldName',
29
+ message: 'from nested',
30
+ smsContent: 'nested body',
31
+ },
32
+ },
33
+ },
34
+ },
35
+ },
36
+ smsFallBackContent: {
37
+ message: 'from root',
38
+ },
39
+ };
40
+ const out = mergeRcsSmsFallBackContentFromDetails(details);
41
+ expect(out.message).toBe('from nested');
42
+ expect(out.smsContent).toBe('nested body');
43
+ expect(out.smsTemplateName).toBe('OldName');
44
+ });
45
+
46
+ it('uses root when nested path is absent', () => {
47
+ const details = {
48
+ smsFallBackContent: { message: 'only root' },
49
+ };
50
+ expect(mergeRcsSmsFallBackContentFromDetails(details)).toEqual({ message: 'only root' });
51
+ });
52
+ });
53
+
54
+ describe('mergeRcsSmsFallbackVarMapLayers', () => {
55
+ it('merges API map with local so empty local object does not wipe API slots', () => {
56
+ const api = { rcsSmsFallbackVarMapped: { '{#a#}_0': 'x' } };
57
+ const local = { rcsSmsFallbackVarMapped: {} };
58
+ expect(mergeRcsSmsFallbackVarMapLayers(api, local)).toEqual({ '{#a#}_0': 'x' });
59
+ });
60
+
61
+ it('local slot values win on same key', () => {
62
+ const api = { rcsSmsFallbackVarMapped: { '{#a#}_0': 'old' } };
63
+ const local = { rcsSmsFallbackVarMapped: { '{#a#}_0': 'new' } };
64
+ expect(mergeRcsSmsFallbackVarMapLayers(api, local)).toEqual({ '{#a#}_0': 'new' });
65
+ });
66
+
67
+ it('reads kebab-case var map from a layer when camel is absent', () => {
68
+ const api = { 'rcs-sms-fallback-var-mapped': { '{#b#}_1': 'y' } };
69
+ expect(mergeRcsSmsFallbackVarMapLayers(api, {})).toEqual({ '{#b#}_1': 'y' });
70
+ });
71
+ });
72
+
73
+ describe('hasMeaningfulSmsFallbackShape', () => {
74
+ it('is false for empty / missing', () => {
75
+ expect(hasMeaningfulSmsFallbackShape(null)).toBe(false);
76
+ expect(hasMeaningfulSmsFallbackShape({})).toBe(false);
77
+ });
78
+
79
+ it('is true when template body or name or var map exists', () => {
80
+ expect(hasMeaningfulSmsFallbackShape({ message: 'hi' })).toBe(true);
81
+ expect(hasMeaningfulSmsFallbackShape({ smsTemplateName: 'T' })).toBe(true);
82
+ expect(hasMeaningfulSmsFallbackShape({ rcsSmsFallbackVarMapped: { '{#a#}_0': 'x' } })).toBe(true);
83
+ });
84
+ });
85
+
86
+ describe('getLibrarySmsFallbackApiBaselineFromTemplateData', () => {
87
+ it('merges root and nested; nested wins on overlap', () => {
88
+ const templateData = {
89
+ smsFallBackContent: { message: 'root' },
90
+ versions: {
91
+ base: {
92
+ content: {
93
+ RCS: {
94
+ smsFallBackContent: { message: 'nested', smsTemplateName: 'N' },
95
+ },
96
+ },
97
+ },
98
+ },
99
+ };
100
+ const out = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
101
+ expect(out.message).toBe('nested');
102
+ expect(out.smsTemplateName).toBe('N');
103
+ });
104
+ });
105
+
106
+ describe('pickFirstSmsFallbackTemplateString', () => {
107
+ it('prefers smsTemplateContent over resolved message so DLT tokens stay in the string', () => {
108
+ const sms = {
109
+ message: 'Hello user',
110
+ smsTemplateContent: 'Hello {#name#}',
111
+ };
112
+ expect(pickFirstSmsFallbackTemplateString(sms)).toBe('Hello {#name#}');
113
+ });
114
+
115
+ it('skips whitespace-only templateContent and uses next field', () => {
116
+ expect(
117
+ pickFirstSmsFallbackTemplateString({
118
+ templateContent: ' ',
119
+ message: 'x',
120
+ }),
121
+ ).toBe('x');
122
+ });
123
+ });
124
+
125
+ describe('getUnmappedDesc', () => {
126
+ it('returns empty string for falsy str', () => {
127
+ expect(getUnmappedDesc('', { a: '1' })).toBe('');
128
+ expect(getUnmappedDesc(null, { a: '1' })).toBe('');
129
+ });
130
+
131
+ it('returns str when mapping is empty or missing', () => {
132
+ expect(getUnmappedDesc('hello', null)).toBe('hello');
133
+ expect(getUnmappedDesc('hello', {})).toBe('hello');
134
+ });
135
+
136
+ it('replaces literal values with {{key}} placeholders', () => {
137
+ expect(getUnmappedDesc('Hi Alice', { user_name: 'Alice' })).toBe('Hi {{user_name}}');
138
+ });
139
+
140
+ it('skips mapping entries with empty or whitespace-only values', () => {
141
+ expect(getUnmappedDesc('x', { a: '', b: ' ' })).toBe('x');
142
+ });
143
+
144
+ it('treats already-braced raw values without duplicating braced needle', () => {
145
+ expect(getUnmappedDesc('v {{user_id}}', { user_id: '{{user_id}}' })).toContain('{{user_id}}');
146
+ });
147
+
148
+ it('adds braced needle when resolved value is not already in {{…}} form', () => {
149
+ const out = getUnmappedDesc('Hello Bob', { nick: 'Bob' });
150
+ expect(out).toBe('Hello {{nick}}');
151
+ });
152
+
153
+ it('ignores undefined mapping values', () => {
154
+ expect(getUnmappedDesc('x', { a: undefined })).toBe('x');
155
+ });
156
+
157
+ it('matches longer needles first to avoid partial replaces', () => {
158
+ const out = getUnmappedDesc('ab', { x: 'a', y: 'ab' });
159
+ expect(out).toBe('{{y}}');
160
+ });
161
+
162
+ it('escapes regex metacharacters in resolved values', () => {
163
+ expect(getUnmappedDesc('price 9.99', { p: '9.99' })).toBe('price {{p}}');
164
+ });
165
+ });
166
+
167
+ describe('hasRcsVarTokens', () => {
168
+ it('is false for empty or non-matching strings', () => {
169
+ expect(hasRcsVarTokens('', rcsVarRegex)).toBe(false);
170
+ expect(hasRcsVarTokens('plain', rcsVarRegex)).toBe(false);
171
+ });
172
+
173
+ it('is true when string contains {{word}} tokens', () => {
174
+ expect(hasRcsVarTokens('{{1}} hi', rcsVarRegex)).toBe(true);
175
+ expect(hasRcsVarTokens('{{user_id}}', rcsVarRegex)).toBe(true);
176
+ });
177
+ });
178
+
179
+ describe('normalizeLibraryLoadedTitleDesc', () => {
180
+ const cardVarMappedAfterHydration = { user_id: '123' };
181
+
182
+ it('returns originals in full mode', () => {
183
+ const out = normalizeLibraryLoadedTitleDesc({
184
+ loadedTitle: 'Hello 123',
185
+ loadedDesc: 'Desc',
186
+ isFullMode: true,
187
+ cardVarMappedAfterHydration,
188
+ rcsVarRegex,
189
+ });
190
+ expect(out).toEqual({ normalizedTitle: 'Hello 123', normalizedDesc: 'Desc' });
191
+ });
192
+
193
+ it('unmaps when library mode, map present, and no mustache tokens', () => {
194
+ const out = normalizeLibraryLoadedTitleDesc({
195
+ loadedTitle: 'Hello 123',
196
+ loadedDesc: 'World 123',
197
+ isFullMode: false,
198
+ cardVarMappedAfterHydration,
199
+ rcsVarRegex,
200
+ });
201
+ expect(out.normalizedTitle).toBe('Hello {{user_id}}');
202
+ expect(out.normalizedDesc).toBe('World {{user_id}}');
203
+ });
204
+
205
+ it('keeps {{numeric}} slots when tokens already exist', () => {
206
+ const out = normalizeLibraryLoadedTitleDesc({
207
+ loadedTitle: 'Hi {{1}}',
208
+ loadedDesc: 'Body 123',
209
+ isFullMode: false,
210
+ cardVarMappedAfterHydration,
211
+ rcsVarRegex,
212
+ });
213
+ expect(out.normalizedTitle).toBe('Hi {{1}}');
214
+ expect(out.normalizedDesc).toBe('Body {{user_id}}');
215
+ });
216
+
217
+ it('does not unmap when cardVarMappedAfterHydration is empty', () => {
218
+ const out = normalizeLibraryLoadedTitleDesc({
219
+ loadedTitle: 'Hello',
220
+ loadedDesc: 'D',
221
+ isFullMode: false,
222
+ cardVarMappedAfterHydration: {},
223
+ rcsVarRegex,
224
+ });
225
+ expect(out).toEqual({ normalizedTitle: 'Hello', normalizedDesc: 'D' });
226
+ });
227
+ });
228
+
229
+ describe('syncCardVarMappedSemanticsFromSlots', () => {
230
+ it('copies numeric slot values onto empty semantic keys (campaign round-trip)', () => {
231
+ const out = syncCardVarMappedSemanticsFromSlots(
232
+ { 1: '[FirstName]', user_name: '', 2: 'x', other: '' },
233
+ 'Hi {{user_name}}',
234
+ 'More {{other}}',
235
+ rcsVarRegex,
236
+ );
237
+ expect(out.user_name).toBe('[FirstName]');
238
+ expect(out.other).toBe('x');
239
+ });
240
+
241
+ it('does not overwrite non-empty semantic keys', () => {
242
+ const out = syncCardVarMappedSemanticsFromSlots(
243
+ { 1: 'from-slot', user_name: 'kept' },
244
+ 'Hello {{user_name}}',
245
+ '',
246
+ rcsVarRegex,
247
+ );
248
+ expect(out.user_name).toBe('kept');
249
+ });
250
+ });
251
+ });