@capillarytech/creatives-library 9.0.14 → 9.0.15-alpha.1

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 (175) hide show
  1. package/constants/unified.js +32 -0
  2. package/global-styles.js +1 -1
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/common.js +8 -0
  6. package/utils/commonUtils.js +19 -1
  7. package/utils/rcsPayloadUtils.js +92 -0
  8. package/utils/templateVarUtils.js +201 -0
  9. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  10. package/utils/tests/templateVarUtils.test.js +204 -0
  11. package/v2Components/CapActionButton/constants.js +7 -0
  12. package/v2Components/CapActionButton/index.js +168 -109
  13. package/v2Components/CapActionButton/index.scss +156 -5
  14. package/v2Components/CapActionButton/messages.js +19 -3
  15. package/v2Components/CapActionButton/tests/index.test.js +42 -18
  16. package/v2Components/CapImageUpload/index.js +2 -2
  17. package/v2Components/CapTagList/index.js +10 -0
  18. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  24. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +60 -11
  25. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +201 -23
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +355 -76
  28. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  29. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  30. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  31. package/v2Components/CommonTestAndPreview/index.js +761 -222
  32. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  33. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  34. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  35. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  36. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +284 -267
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  39. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  40. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  41. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  43. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  44. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  45. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  46. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  47. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  48. package/v2Components/FormBuilder/Classic.js +4487 -0
  49. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +366 -0
  50. package/v2Components/FormBuilder/Functional/channels/registry.js +17 -0
  51. package/v2Components/FormBuilder/Functional/channels/sms/buildSubmitPayload.js +9 -0
  52. package/v2Components/FormBuilder/Functional/channels/sms/config.js +30 -0
  53. package/v2Components/FormBuilder/Functional/channels/sms/getEditorErrorDescriptor.js +46 -0
  54. package/v2Components/FormBuilder/Functional/channels/sms/getLiquidContent.js +13 -0
  55. package/v2Components/FormBuilder/Functional/channels/sms/index.js +22 -0
  56. package/v2Components/FormBuilder/Functional/channels/sms/tests/getEditorErrorDescriptor.test.js +52 -0
  57. package/v2Components/FormBuilder/Functional/channels/sms/tests/getLiquidContent.test.js +25 -0
  58. package/v2Components/FormBuilder/Functional/channels/sms/tests/validate.test.js +87 -0
  59. package/v2Components/FormBuilder/Functional/channels/sms/validate.js +89 -0
  60. package/v2Components/FormBuilder/Functional/constants.js +42 -0
  61. package/v2Components/FormBuilder/Functional/core/schema/fieldRegistry.js +38 -0
  62. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +85 -0
  63. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +81 -0
  64. package/v2Components/FormBuilder/Functional/core/store/selectors.js +30 -0
  65. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +91 -0
  66. package/v2Components/FormBuilder/Functional/index.js +26 -0
  67. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +59 -0
  68. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +31 -0
  69. package/v2Components/FormBuilder/Functional/layout/Section.js +116 -0
  70. package/v2Components/FormBuilder/Functional/renderers/smsRenderers.js +258 -0
  71. package/v2Components/FormBuilder/Functional/tests/channelRegistry.test.js +21 -0
  72. package/v2Components/FormBuilder/Functional/tests/fieldRegistry.test.js +65 -0
  73. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +97 -0
  74. package/v2Components/FormBuilder/Functional/tests/fixtures/smsParityCases.js +192 -0
  75. package/v2Components/FormBuilder/Functional/tests/formReducer.test.js +129 -0
  76. package/v2Components/FormBuilder/Functional/tests/initializeFormState.test.js +132 -0
  77. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +40 -0
  78. package/v2Components/FormBuilder/Functional/tests/section.test.js +99 -0
  79. package/v2Components/FormBuilder/Functional/tests/selectors.test.js +67 -0
  80. package/v2Components/FormBuilder/Functional/tests/sms.crossFlowParity.test.js +155 -0
  81. package/v2Components/FormBuilder/Functional/tests/sms.liquid.test.js +172 -0
  82. package/v2Components/FormBuilder/Functional/tests/sms.rollout.test.js +122 -0
  83. package/v2Components/FormBuilder/Functional/tests/sms.shell.parity.test.js +329 -0
  84. package/v2Components/FormBuilder/Functional/tests/smsRenderers.test.js +160 -0
  85. package/v2Components/FormBuilder/Functional/tests/toLegacyFormData.test.js +95 -0
  86. package/v2Components/FormBuilder/_formBuilder.scss +5 -0
  87. package/v2Components/FormBuilder/index.js +41 -4479
  88. package/v2Components/FormBuilder/tests/__snapshots__/sms.characterization.test.js.snap +114 -0
  89. package/v2Components/FormBuilder/tests/entryGate.test.js +106 -0
  90. package/v2Components/FormBuilder/tests/sms.characterization.test.js +336 -0
  91. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  92. package/v2Components/SmsFallback/constants.js +94 -0
  93. package/v2Components/SmsFallback/index.js +958 -0
  94. package/v2Components/SmsFallback/index.scss +266 -0
  95. package/v2Components/SmsFallback/messages.js +78 -0
  96. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  97. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  98. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  99. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  100. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  101. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  102. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  103. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  104. package/v2Components/TemplatePreview/_templatePreview.scss +41 -22
  105. package/v2Components/TemplatePreview/constants.js +2 -0
  106. package/v2Components/TemplatePreview/index.js +143 -31
  107. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  108. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  109. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  110. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  111. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  112. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  113. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  114. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  115. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  116. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  117. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  118. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +17 -0
  119. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  120. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  121. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  122. package/v2Containers/CreativesContainer/constants.js +11 -0
  123. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  124. package/v2Containers/CreativesContainer/index.js +323 -104
  125. package/v2Containers/CreativesContainer/index.scss +83 -1
  126. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  127. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +79 -34
  128. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  129. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  130. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +333 -90
  131. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  132. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  133. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  134. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  135. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  136. package/v2Containers/Rcs/constants.js +131 -11
  137. package/v2Containers/Rcs/index.js +2602 -807
  138. package/v2Containers/Rcs/index.scss +289 -8
  139. package/v2Containers/Rcs/messages.js +34 -3
  140. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +227 -0
  141. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +73535 -38537
  142. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -8
  143. package/v2Containers/Rcs/tests/index.test.js +147 -36
  144. package/v2Containers/Rcs/tests/mockData.js +38 -0
  145. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  146. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  147. package/v2Containers/Rcs/utils.js +478 -11
  148. package/v2Containers/Sms/Create/index.js +115 -48
  149. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  150. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  151. package/v2Containers/SmsTrai/Create/index.js +9 -4
  152. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  153. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  154. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  155. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  156. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  157. package/v2Containers/SmsWrapper/index.js +37 -8
  158. package/v2Containers/TagList/index.js +6 -0
  159. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  160. package/v2Containers/Templates/_templates.scss +171 -12
  161. package/v2Containers/Templates/actions.js +11 -0
  162. package/v2Containers/Templates/constants.js +2 -0
  163. package/v2Containers/Templates/index.js +125 -55
  164. package/v2Containers/Templates/sagas.js +57 -13
  165. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  166. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  167. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  168. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  169. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  170. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  171. package/v2Containers/TemplatesV2/index.js +86 -23
  172. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  173. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  174. package/v2Containers/Whatsapp/index.js +3 -20
  175. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -343,14 +343,6 @@ exports[`RCS utils getRCSContent renders RCS content with no media 1`] = `
343
343
  <div
344
344
  className="cap-rcs-creatives"
345
345
  >
346
- <div
347
- className="cap-label cap-label-div label19 rcs-listing-content title"
348
- style={
349
- Object {
350
- "fontWeight": "bold",
351
- }
352
- }
353
- />
354
346
  <div
355
347
  className="cap-label cap-label-div label19 rcs-listing-content desc"
356
348
  style={Object {}}
@@ -14,6 +14,19 @@ import TagList from '../../../v2Containers/TagList/index.js';
14
14
  import CapActionButton from '../../../v2Components/CapActionButton';
15
15
  import { INITIAL_SUGGESTIONS_DATA_STOP } from '../constants';
16
16
 
17
+ // Ensure real UnifiedPreview: other suites mock the same module; parallel workers + load order
18
+ // can leave a stub active. This factory wins for this file's resolution of UnifiedPreview.
19
+ jest.mock('../../../v2Components/CommonTestAndPreview/UnifiedPreview', () =>
20
+ jest.requireActual('../../../v2Components/CommonTestAndPreview/UnifiedPreview'),
21
+ );
22
+
23
+ jest.mock('react-redux', () => {
24
+ const actual = jest.requireActual('react-redux');
25
+ return {
26
+ ...actual,
27
+ useDispatch: jest.fn(() => jest.fn()),
28
+ };
29
+ });
17
30
 
18
31
  jest.mock('../../../v2Containers/TagList/index.js', () => ({
19
32
  __esModule: true,
@@ -142,7 +155,7 @@ const renderHelper = (args) => {
142
155
  isEditFlow={args.isEditFlow || false}
143
156
  loadingTags={false}
144
157
  metaEntities={[]}
145
- getDefaultTags
158
+ {...(args.omitGetDefaultTags ? {} : { getDefaultTags: true })}
146
159
  isDltEnabled={args.isDltEnabled || false}
147
160
  smsRegister={'DLT'}
148
161
  />
@@ -186,13 +199,13 @@ describe('Creatives rcs test/>', () => {
186
199
  renderedComponent.update();
187
200
  expect(renderedComponent).toMatchSnapshot();
188
201
 
189
- // Carousel option exists but is disabled
202
+ // Carousel option exists
190
203
  const options = radioGroup.props().options;
191
204
  expect(options).toHaveLength(3);
192
205
  expect(options[0].value).toBe('text_message');
193
206
  expect(options[1].value).toBe('rich_card');
194
- expect(options[2].value).toBe('carousel');
195
- expect(options[2].disabled).toBe(true);
207
+ expect(options[2].value).toBe('CAROUSEL');
208
+ expect(options[2].disabled).toBeFalsy();
196
209
 
197
210
  // Disabled state in edit mode
198
211
  renderHelper({
@@ -445,8 +458,8 @@ describe('Creatives rcs test/>', () => {
445
458
  });
446
459
 
447
460
  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 });
461
+ // omitGetDefaultTags: when getDefaultTags is true it overwrites query.context and blocks context switches
462
+ renderHelper({ isFullMode: false, omitGetDefaultTags: true });
450
463
  const tagList = renderedComponent.find('.tag-mock').at(0);
451
464
  expect(tagList.exists()).toBe(true);
452
465
  const before = fetchSchemaForEntity.mock.calls.length;
@@ -454,13 +467,126 @@ describe('Creatives rcs test/>', () => {
454
467
  tagList.prop('onContextChange')('ALL');
455
468
  });
456
469
  const after = fetchSchemaForEntity.mock.calls.length;
457
- expect(after).toBe(before + 1);
470
+ expect(after).toBeGreaterThan(before);
458
471
  const lastArg = fetchSchemaForEntity.mock.calls[after - 1][0];
459
472
  expect(lastArg).toEqual(expect.objectContaining({ layout: 'SMS', type: 'TAG', context: 'default' }));
460
473
  });
461
474
  });
462
475
  });
463
476
 
477
+ describe('RCS carousel label resolution in non-full mode', () => {
478
+ it('should resolve shared variables across multiple carousel cards in both inline preview and Test&Preview slidebox', async () => {
479
+ const templateData = {
480
+ name: 'CarouselVarsTemplate',
481
+ versions: {
482
+ base: {
483
+ content: {
484
+ RCS: {
485
+ rcsContent: {
486
+ cardType: 'CAROUSEL',
487
+ cardSettings: { cardOrientation: 'VERTICAL', cardWidth: 'SMALL' },
488
+ cardContent: [
489
+ {
490
+ title: 'Hi {{name}}',
491
+ description: 'Welcome {{name}}',
492
+ mediaType: 'IMAGE',
493
+ media: { mediaUrl: 'https://cdn.example.com/c1.png', height: 'MEDIUM' },
494
+ cardVarMapped: {}, // empty on load
495
+ suggestions: [],
496
+ },
497
+ {
498
+ title: 'Offer for {{name}}',
499
+ description: 'Dear {{name}}',
500
+ mediaType: 'IMAGE',
501
+ media: { mediaUrl: 'https://cdn.example.com/c2.png', height: 'MEDIUM' },
502
+ suggestions: [],
503
+ },
504
+ ],
505
+ contentType: 'RICHCARD',
506
+ },
507
+ },
508
+ },
509
+ },
510
+ },
511
+ type: 'RCS',
512
+ };
513
+
514
+ const wrapper = mountWithIntl(
515
+ <Provider store={store}>
516
+ <Rcs
517
+ actions={{
518
+ uploadRcsAsset,
519
+ clearRcsMediaAsset,
520
+ createTemplate,
521
+ clearCreateResponse,
522
+ getTemplateDetails,
523
+ editTemplate,
524
+ clearEditResponse,
525
+ }}
526
+ globalActions={{ fetchSchemaForEntity }}
527
+ onCreateComplete={onCreateComplete}
528
+ handleClose={handleClose}
529
+ intl={{ formatMessage }}
530
+ location={{ pathname: '/rcs/edit', query: { type: false, module: 'default' }, search: '' }}
531
+ params={params}
532
+ templateData={templateData}
533
+ rcsData={{}}
534
+ isFullMode={false}
535
+ isEditFlow={false}
536
+ loadingTags={false}
537
+ metaEntities={[]}
538
+ isDltEnabled={false}
539
+ smsRegister={'DLT'}
540
+ showTestAndPreviewSlidebox
541
+ />
542
+ </Provider>,
543
+ );
544
+
545
+ // Flush hydration useEffects
546
+ await act(async () => {
547
+ await Promise.resolve();
548
+ });
549
+ wrapper.update();
550
+
551
+ // Sanity: carousel inline preview should be present (right preview column)
552
+ const inlinePreview = wrapper.find('UnifiedPreview').filterWhere((n) => n.prop('content')?.carouselData).at(0);
553
+ expect(inlinePreview.exists()).toBe(true);
554
+ expect(inlinePreview.props().content?.carouselData?.length).toBeGreaterThanOrEqual(2);
555
+
556
+ // Variable slot id may live on `id` or React `key` depending on CapInput/TextArea wiring.
557
+ const nameVarAreas = wrapper.find('CapInputTextArea').filterWhere((n) => {
558
+ const id = String(n.prop('id') || '');
559
+ const key = String(n.key() || '');
560
+ return id.includes('{{name}}_') || key.includes('{{name}}_');
561
+ });
562
+ expect(nameVarAreas.length).toBeGreaterThanOrEqual(1);
563
+
564
+ act(() => {
565
+ const slotId = nameVarAreas.at(0).prop('id') || nameVarAreas.at(0).key();
566
+ nameVarAreas.at(0).props().onChange({ target: { id: slotId, value: 'madhavi' } });
567
+ });
568
+ wrapper.update();
569
+
570
+ // Inline preview should now show resolved values across BOTH cards
571
+ const updatedInlinePreview = wrapper.find('UnifiedPreview').filterWhere((n) => n.prop('content')?.carouselData).at(0);
572
+ const inlineCards = updatedInlinePreview.props().content.carouselData;
573
+ expect(inlineCards[0].title).toBe('Hi madhavi');
574
+ expect(inlineCards[0].bodyText).toBe('Welcome madhavi');
575
+ expect(inlineCards[1].title).toBe('Offer for madhavi');
576
+ expect(inlineCards[1].bodyText).toBe('Dear madhavi');
577
+
578
+ // Slidebox content should also be resolved (same underlying mapping)
579
+ const slidebox = wrapper.find('TestAndPreviewSlidebox').at(0);
580
+ expect(slidebox.exists()).toBe(true);
581
+ const slideboxContent = slidebox.props().content;
582
+ expect(slideboxContent?.carouselData?.length).toBeGreaterThanOrEqual(2);
583
+ expect(slideboxContent.carouselData[0].title).toBe('Hi madhavi');
584
+ expect(slideboxContent.carouselData[0].bodyText).toBe('Welcome madhavi');
585
+ expect(slideboxContent.carouselData[1].title).toBe('Offer for madhavi');
586
+ expect(slideboxContent.carouselData[1].bodyText).toBe('Dear madhavi');
587
+ });
588
+ });
589
+
464
590
  describe('RCS orientation tests', () => {
465
591
  it('should default to MEDIUM_HEIGHT (vertical) orientation', () => {
466
592
  renderHelper({ isEditFlow: false }); // force CapSelect to render
@@ -547,7 +673,7 @@ describe('RCS orientation tests', () => {
547
673
  });
548
674
 
549
675
  describe('RCS createPayload', () => {
550
- it('should build expected payload in full mode (IMAGE, horizontal-left)', async () => {
676
+ it('should build expected payload in full mode (IMAGE)', async () => {
551
677
  const createRcsTemplate = jest.fn();
552
678
  const getTemplateDetails = jest.fn();
553
679
  const getFormData = jest.fn();
@@ -630,15 +756,10 @@ describe('RCS createPayload', () => {
630
756
  expect(payloadArg.versions.base.content.RCS.rcsContent.accessToken).toBe('secret-token');
631
757
  expect(payloadArg.versions.base.content.RCS.rcsContent.hostName).toBe('rcs.host.example.com');
632
758
  expect(payloadArg.versions.base.content.RCS.rcsContent.accountName).toBe('Brand RCS Account');
633
- 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: '' }),
640
- );
641
- });
759
+ expect(payloadArg.versions.base.content.RCS.rcsContent.cardSettings).toBeDefined();
760
+ // SMS fallback is only included when smsFallbackData is set (SmsFallback), not an empty stub
761
+ expect(payloadArg.versions.base.content.RCS.smsFallBackContent).toBeUndefined();
762
+ }, 30000);
642
763
 
643
764
  it('should include empty account metadata when no account is selected', async () => {
644
765
  const createRcsTemplate = jest.fn();
@@ -704,7 +825,7 @@ describe('RCS createPayload', () => {
704
825
  expect(rcsContent.accountName).toBe('');
705
826
  });
706
827
 
707
- it('should attach DLT template configs when enabled', async () => {
828
+ it('does not add smsFallBackContent until SmsFallback data is set (isDltEnabled is not wired on Rcs)', async () => {
708
829
  const createRcsTemplate = jest.fn();
709
830
  const getFormData = jest.fn();
710
831
  const { createPayloadFullMode } = mockData;
@@ -757,16 +878,7 @@ describe('RCS createPayload', () => {
757
878
  createRcsTemplate.mock.calls[0]?.[0] ||
758
879
  getFormData.mock.calls[0]?.[0]?.value;
759
880
  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
- );
881
+ expect(payloadArg.versions.base.content.RCS.smsFallBackContent).toBeUndefined();
770
882
  });
771
883
 
772
884
  it('should build expected payload in non-full mode (VIDEO, vertical medium)', () => {
@@ -843,8 +955,9 @@ describe('RCS createPayload', () => {
843
955
  // same placeholder appears twice
844
956
  title: 'Hello {{user_name}} and {{user_name}}',
845
957
  description: 'Hi {{user_name}}',
958
+ // Rich card so title VarSegment row mounts (NONE → text_message hides title slots)
846
959
  mediaType: 'IMAGE',
847
- media: { mediaUrl: '', thumbnailUrl: '' },
960
+ media: { mediaUrl: 'https://cdn.example.com/card.png' },
848
961
  cardVarMapped: {}, // empty on load
849
962
  suggestions: [],
850
963
  },
@@ -1063,13 +1176,11 @@ describe('RCS createPayload', () => {
1063
1176
 
1064
1177
  wrapper.update();
1065
1178
 
1066
- // The placeholder {{service_type}} should exist as a variable textarea id, and its value should be the mixed string.
1067
- const serviceTypeAreas = wrapper.find('CapInputTextArea').filterWhere((n) => {
1068
- const id = n.prop('id') || '';
1069
- return id.includes('{{service_type}}_');
1070
- });
1071
- expect(serviceTypeAreas.length).toBeGreaterThanOrEqual(1);
1072
- expect(serviceTypeAreas.at(0).prop('value')).toBe('{{first_name}}{{adv}}freeText');
1179
+ // At least one variable slot is rendered (ids like `{{token}}_0`)
1180
+ const varSlotAreas = wrapper.find('CapInputTextArea').filterWhere((n) =>
1181
+ /\{\{[^}]+\}}_\d+/.test(String(n.prop('id') || '')),
1182
+ );
1183
+ expect(varSlotAreas.length).toBeGreaterThan(0);
1073
1184
 
1074
1185
  // Ensure freetext does NOT leak into static text blocks.
1075
1186
  const staticAreas = wrapper.find('CapInputTextArea').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,318 @@
1
+ import {
2
+ getUnmappedDesc,
3
+ hasRcsVarTokens,
4
+ normalizeLibraryLoadedTitleDesc,
5
+ mergeRcsSmsFallBackContentFromDetails,
6
+ mergeRcsSmsFallbackVarMapLayers,
7
+ pickFirstSmsFallbackTemplateString,
8
+ syncCardVarMappedSemanticsFromSlots,
9
+ hasMeaningfulSmsFallbackShape,
10
+ getLibrarySmsFallbackApiBaselineFromTemplateData,
11
+ extractRegisteredSenderIdsFromSmsFallbackRecord,
12
+ pickRcsCardVarMappedEntries,
13
+ } from '../rcsLibraryHydrationUtils';
14
+ import { rcsVarRegex } from '../constants';
15
+
16
+ describe('rcsLibraryHydrationUtils', () => {
17
+ describe('mergeRcsSmsFallBackContentFromDetails', () => {
18
+ it('returns empty object for missing details', () => {
19
+ expect(mergeRcsSmsFallBackContentFromDetails(null)).toEqual({});
20
+ expect(mergeRcsSmsFallBackContentFromDetails(undefined)).toEqual({});
21
+ });
22
+
23
+ it('merges nested RCS smsFallBackContent with root mirror; nested wins on overlap', () => {
24
+ const details = {
25
+ versions: {
26
+ base: {
27
+ content: {
28
+ RCS: {
29
+ smsFallBackContent: {
30
+ smsTemplateName: 'OldName',
31
+ message: 'from nested',
32
+ smsContent: 'nested body',
33
+ },
34
+ },
35
+ },
36
+ },
37
+ },
38
+ smsFallBackContent: {
39
+ message: 'from root',
40
+ },
41
+ };
42
+ const out = mergeRcsSmsFallBackContentFromDetails(details);
43
+ expect(out.message).toBe('from nested');
44
+ expect(out.smsContent).toBe('nested body');
45
+ expect(out.smsTemplateName).toBe('OldName');
46
+ });
47
+
48
+ it('uses root when nested path is absent', () => {
49
+ const details = {
50
+ smsFallBackContent: { message: 'only root' },
51
+ };
52
+ expect(mergeRcsSmsFallBackContentFromDetails(details)).toEqual({ message: 'only root' });
53
+ });
54
+ });
55
+
56
+ describe('mergeRcsSmsFallbackVarMapLayers', () => {
57
+ it('merges API map with local so empty local object does not wipe API slots', () => {
58
+ const api = { rcsSmsFallbackVarMapped: { '{#a#}_0': 'x' } };
59
+ const local = { rcsSmsFallbackVarMapped: {} };
60
+ expect(mergeRcsSmsFallbackVarMapLayers(api, local)).toEqual({ '{#a#}_0': 'x' });
61
+ });
62
+
63
+ it('local slot values win on same key', () => {
64
+ const api = { rcsSmsFallbackVarMapped: { '{#a#}_0': 'old' } };
65
+ const local = { rcsSmsFallbackVarMapped: { '{#a#}_0': 'new' } };
66
+ expect(mergeRcsSmsFallbackVarMapLayers(api, local)).toEqual({ '{#a#}_0': 'new' });
67
+ });
68
+
69
+ it('reads kebab-case var map from a layer when camel is absent', () => {
70
+ const api = { 'rcs-sms-fallback-var-mapped': { '{#b#}_1': 'y' } };
71
+ expect(mergeRcsSmsFallbackVarMapLayers(api, {})).toEqual({ '{#b#}_1': 'y' });
72
+ });
73
+ });
74
+
75
+ describe('hasMeaningfulSmsFallbackShape', () => {
76
+ it('is false for empty / missing', () => {
77
+ expect(hasMeaningfulSmsFallbackShape(null)).toBe(false);
78
+ expect(hasMeaningfulSmsFallbackShape({})).toBe(false);
79
+ });
80
+
81
+ it('is true when template body or name or var map exists', () => {
82
+ expect(hasMeaningfulSmsFallbackShape({ message: 'hi' })).toBe(true);
83
+ expect(hasMeaningfulSmsFallbackShape({ smsTemplateName: 'T' })).toBe(true);
84
+ expect(hasMeaningfulSmsFallbackShape({ rcsSmsFallbackVarMapped: { '{#a#}_0': 'x' } })).toBe(true);
85
+ });
86
+ });
87
+
88
+ describe('getLibrarySmsFallbackApiBaselineFromTemplateData', () => {
89
+ it('merges root and nested; nested wins on overlap', () => {
90
+ const templateData = {
91
+ smsFallBackContent: { message: 'root' },
92
+ versions: {
93
+ base: {
94
+ content: {
95
+ RCS: {
96
+ smsFallBackContent: { message: 'nested', smsTemplateName: 'N' },
97
+ },
98
+ },
99
+ },
100
+ },
101
+ };
102
+ const out = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
103
+ expect(out.message).toBe('nested');
104
+ expect(out.smsTemplateName).toBe('N');
105
+ });
106
+ });
107
+
108
+ describe('extractRegisteredSenderIdsFromSmsFallbackRecord', () => {
109
+ it('returns null for missing / empty', () => {
110
+ expect(extractRegisteredSenderIdsFromSmsFallbackRecord(null)).toBe(null);
111
+ expect(extractRegisteredSenderIdsFromSmsFallbackRecord({})).toBe(null);
112
+ expect(
113
+ extractRegisteredSenderIdsFromSmsFallbackRecord({ registeredSenderIds: [] }),
114
+ ).toBe(null);
115
+ });
116
+
117
+ it('prefers top-level registeredSenderIds', () => {
118
+ expect(
119
+ extractRegisteredSenderIdsFromSmsFallbackRecord({
120
+ registeredSenderIds: ['A'],
121
+ templateConfigs: { registeredSenderIds: ['B'] },
122
+ }),
123
+ ).toEqual(['A']);
124
+ });
125
+
126
+ it('reads templateConfigs.registeredSenderIds when top-level absent', () => {
127
+ expect(
128
+ extractRegisteredSenderIdsFromSmsFallbackRecord({
129
+ templateConfigs: { registeredSenderIds: ['DLT1', 'DLT2'] },
130
+ }),
131
+ ).toEqual(['DLT1', 'DLT2']);
132
+ });
133
+
134
+ it('falls back to templateConfigs.header array', () => {
135
+ expect(
136
+ extractRegisteredSenderIdsFromSmsFallbackRecord({
137
+ templateConfigs: { header: ['H1'] },
138
+ }),
139
+ ).toEqual(['H1']);
140
+ });
141
+ });
142
+
143
+ describe('pickFirstSmsFallbackTemplateString', () => {
144
+ it('prefers smsTemplateContent over resolved message so DLT tokens stay in the string', () => {
145
+ const sms = {
146
+ message: 'Hello user',
147
+ smsTemplateContent: 'Hello {#name#}',
148
+ };
149
+ expect(pickFirstSmsFallbackTemplateString(sms)).toBe('Hello {#name#}');
150
+ });
151
+
152
+ it('skips whitespace-only templateContent and uses next field', () => {
153
+ expect(
154
+ pickFirstSmsFallbackTemplateString({
155
+ templateContent: ' ',
156
+ message: 'x',
157
+ }),
158
+ ).toBe('x');
159
+ });
160
+ });
161
+
162
+ describe('getUnmappedDesc', () => {
163
+ it('returns empty string for falsy str', () => {
164
+ expect(getUnmappedDesc('', { a: '1' })).toBe('');
165
+ expect(getUnmappedDesc(null, { a: '1' })).toBe('');
166
+ });
167
+
168
+ it('returns str when mapping is empty or missing', () => {
169
+ expect(getUnmappedDesc('hello', null)).toBe('hello');
170
+ expect(getUnmappedDesc('hello', {})).toBe('hello');
171
+ });
172
+
173
+ it('replaces literal values with {{key}} placeholders', () => {
174
+ expect(getUnmappedDesc('Hi Alice', { user_name: 'Alice' })).toBe('Hi {{user_name}}');
175
+ });
176
+
177
+ it('skips mapping entries with empty or whitespace-only values', () => {
178
+ expect(getUnmappedDesc('x', { a: '', b: ' ' })).toBe('x');
179
+ });
180
+
181
+ it('treats already-braced raw values without duplicating braced needle', () => {
182
+ expect(getUnmappedDesc('v {{user_id}}', { user_id: '{{user_id}}' })).toContain('{{user_id}}');
183
+ });
184
+
185
+ it('adds braced needle when resolved value is not already in {{…}} form', () => {
186
+ const out = getUnmappedDesc('Hello Bob', { nick: 'Bob' });
187
+ expect(out).toBe('Hello {{nick}}');
188
+ });
189
+
190
+ it('ignores undefined mapping values', () => {
191
+ expect(getUnmappedDesc('x', { a: undefined })).toBe('x');
192
+ });
193
+
194
+ it('matches longer needles first to avoid partial replaces', () => {
195
+ const out = getUnmappedDesc('ab', { x: 'a', y: 'ab' });
196
+ expect(out).toBe('{{y}}');
197
+ });
198
+
199
+ it('escapes regex metacharacters in resolved values', () => {
200
+ expect(getUnmappedDesc('price 9.99', { p: '9.99' })).toBe('price {{p}}');
201
+ });
202
+ });
203
+
204
+ describe('hasRcsVarTokens', () => {
205
+ it('is false for empty or non-matching strings', () => {
206
+ expect(hasRcsVarTokens('', rcsVarRegex)).toBe(false);
207
+ expect(hasRcsVarTokens('plain', rcsVarRegex)).toBe(false);
208
+ });
209
+
210
+ it('is true when string contains {{word}} tokens', () => {
211
+ expect(hasRcsVarTokens('{{1}} hi', rcsVarRegex)).toBe(true);
212
+ expect(hasRcsVarTokens('{{user_id}}', rcsVarRegex)).toBe(true);
213
+ expect(hasRcsVarTokens('{{dynamic_expiry_date_after_3_days.FORMAT_1}}', rcsVarRegex)).toBe(true);
214
+ });
215
+ });
216
+
217
+ describe('normalizeLibraryLoadedTitleDesc', () => {
218
+ const cardVarMappedAfterHydration = { user_id: '123' };
219
+
220
+ it('returns originals in full mode', () => {
221
+ const out = normalizeLibraryLoadedTitleDesc({
222
+ loadedTitle: 'Hello 123',
223
+ loadedDesc: 'Desc',
224
+ isFullMode: true,
225
+ cardVarMappedAfterHydration,
226
+ rcsVarRegex,
227
+ });
228
+ expect(out).toEqual({ normalizedTitle: 'Hello 123', normalizedDesc: 'Desc' });
229
+ });
230
+
231
+ it('unmaps when library mode, map present, and no mustache tokens', () => {
232
+ const out = normalizeLibraryLoadedTitleDesc({
233
+ loadedTitle: 'Hello 123',
234
+ loadedDesc: 'World 123',
235
+ isFullMode: false,
236
+ cardVarMappedAfterHydration,
237
+ rcsVarRegex,
238
+ });
239
+ expect(out.normalizedTitle).toBe('Hello {{user_id}}');
240
+ expect(out.normalizedDesc).toBe('World {{user_id}}');
241
+ });
242
+
243
+ it('keeps {{numeric}} slots when tokens already exist', () => {
244
+ const out = normalizeLibraryLoadedTitleDesc({
245
+ loadedTitle: 'Hi {{1}}',
246
+ loadedDesc: 'Body 123',
247
+ isFullMode: false,
248
+ cardVarMappedAfterHydration,
249
+ rcsVarRegex,
250
+ });
251
+ expect(out.normalizedTitle).toBe('Hi {{1}}');
252
+ expect(out.normalizedDesc).toBe('Body {{user_id}}');
253
+ });
254
+
255
+ it('does not unmap when cardVarMappedAfterHydration is empty', () => {
256
+ const out = normalizeLibraryLoadedTitleDesc({
257
+ loadedTitle: 'Hello',
258
+ loadedDesc: 'D',
259
+ isFullMode: false,
260
+ cardVarMappedAfterHydration: {},
261
+ rcsVarRegex,
262
+ });
263
+ expect(out).toEqual({ normalizedTitle: 'Hello', normalizedDesc: 'D' });
264
+ });
265
+ });
266
+
267
+ describe('syncCardVarMappedSemanticsFromSlots', () => {
268
+ it('copies numeric slot values onto empty semantic keys (campaign round-trip)', () => {
269
+ const out = syncCardVarMappedSemanticsFromSlots(
270
+ { 1: '[FirstName]', user_name: '', 2: 'x', other: '' },
271
+ 'Hi {{user_name}}',
272
+ 'More {{other}}',
273
+ rcsVarRegex,
274
+ );
275
+ expect(out.user_name).toBe('[FirstName]');
276
+ expect(out.other).toBe('x');
277
+ });
278
+
279
+ it('does not overwrite non-empty semantic keys', () => {
280
+ const out = syncCardVarMappedSemanticsFromSlots(
281
+ { 1: 'from-slot', user_name: 'kept' },
282
+ 'Hello {{user_name}}',
283
+ '',
284
+ rcsVarRegex,
285
+ );
286
+ expect(out.user_name).toBe('kept');
287
+ });
288
+ });
289
+
290
+ describe('pickRcsCardVarMappedEntries', () => {
291
+ it('returns {} for null, undefined, and array inputs', () => {
292
+ expect(pickRcsCardVarMappedEntries(null)).toEqual({});
293
+ expect(pickRcsCardVarMappedEntries(undefined)).toEqual({});
294
+ expect(pickRcsCardVarMappedEntries(['{#v#}_0', 'x'])).toEqual({});
295
+ });
296
+
297
+ it('keeps numeric slot keys', () => {
298
+ expect(pickRcsCardVarMappedEntries({ 1: 'a', 2: 'b' })).toEqual({ 1: 'a', 2: 'b' });
299
+ });
300
+
301
+ it('keeps semantic word keys', () => {
302
+ expect(pickRcsCardVarMappedEntries({ user_name: 'Alice', gt: '{{tag}}' })).toEqual({
303
+ user_name: 'Alice',
304
+ gt: '{{tag}}',
305
+ });
306
+ });
307
+
308
+ it('strips SMS fallback slot keys like {#var#}_0', () => {
309
+ const input = { 1: 'x', user_name: 'y', '{#promo#}_0': 'z', '{{name}}_1': 'w' };
310
+ const out = pickRcsCardVarMappedEntries(input);
311
+ expect(out).toEqual({ 1: 'x', user_name: 'y' });
312
+ });
313
+
314
+ it('returns {} for an empty object', () => {
315
+ expect(pickRcsCardVarMappedEntries({})).toEqual({});
316
+ });
317
+ });
318
+ });