@capillarytech/creatives-library 9.0.35 → 9.0.36

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 (51) hide show
  1. package/app.js +175 -10
  2. package/entry.js +1 -67
  3. package/mfe-exposed-components.js +4 -2
  4. package/package.json +2 -2
  5. package/services/api.js +1 -9
  6. package/styles/containers/layout/_layoutPage.scss +6 -8
  7. package/utils/gtmTrackers/gtmEvents/creativeDetails.js +1 -2
  8. package/utils/rcsPayloadUtils.js +2 -5
  9. package/utils/tests/rcsPayloadUtils.test.js +2 -54
  10. package/v2Components/CapActionButton/constants.js +0 -1
  11. package/v2Components/CapActionButton/index.js +9 -77
  12. package/v2Components/CapActionButton/index.scss +0 -13
  13. package/v2Components/CapActionButton/messages.js +0 -13
  14. package/v2Components/CapActionButton/tests/index.test.js +1 -32
  15. package/v2Components/CommonTestAndPreview/index.js +15 -44
  16. package/v2Components/CommonTestAndPreview/tests/index.test.js +0 -78
  17. package/v2Components/CommonTestAndPreview/utils.js +0 -34
  18. package/v2Components/NavigationBar/index.js +7 -9
  19. package/v2Components/NavigationBar/tests/index.test.js +19 -39
  20. package/v2Components/SmsFallback/index.js +0 -6
  21. package/v2Containers/Cap/constants.js +0 -1
  22. package/v2Containers/Cap/index.js +27 -57
  23. package/v2Containers/CreativesContainer/index.js +8 -11
  24. package/v2Containers/CreativesContainer/tests/index.test.js +0 -53
  25. package/v2Containers/Rcs/carouselUtils.js +73 -46
  26. package/v2Containers/Rcs/components/CarouselCard.js +9 -11
  27. package/v2Containers/Rcs/components/CarouselCardButtons.js +0 -20
  28. package/v2Containers/Rcs/constants.js +7 -2
  29. package/v2Containers/Rcs/index.js +205 -226
  30. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +47 -60
  31. package/v2Containers/Rcs/tests/CarouselCard.test.js +2 -1
  32. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +0 -286
  33. package/v2Containers/Rcs/tests/carouselUtils.test.js +32 -28
  34. package/v2Containers/Rcs/tests/index.test.js +238 -151
  35. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +0 -110
  36. package/v2Containers/Rcs/tests/utils.test.js +17 -36
  37. package/v2Containers/Rcs/utils.js +55 -28
  38. package/v2Containers/SmsTrai/Edit/index.js +24 -35
  39. package/v2Containers/Templates/_templates.scss +1 -1
  40. package/v2Containers/Templates/index.js +0 -1
  41. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +0 -5
  42. package/v2Containers/TemplatesV2/TemplatesV2.style.js +3 -8
  43. package/v2Containers/TemplatesV2/index.js +9 -22
  44. package/AppRoot.js +0 -124
  45. package/app-config.js +0 -61
  46. package/bootstrap.js +0 -185
  47. package/utils/getDataLayer.js +0 -15
  48. package/utils/mfeDetect.js +0 -1
  49. package/utils/mfeFirstPaintReady.js +0 -29
  50. package/utils/mfeHistory.js +0 -62
  51. package/v2Components/NavigationBar/mfeModuleHeader.config.js +0 -16
@@ -475,7 +475,7 @@ describe('Creatives rcs test/>', () => {
475
475
  });
476
476
 
477
477
  describe('RCS carousel label resolution in non-full mode', () => {
478
- it('keeps each carousel card\'s same-named variable independently scoped in both inline preview and Test&Preview slidebox', async () => {
478
+ it('keeps every carousel variable slot independent, even when they share a literal tag name, in both inline preview and Test&Preview slidebox', async () => {
479
479
  const templateData = {
480
480
  name: 'CarouselVarsTemplate',
481
481
  versions: {
@@ -564,53 +564,275 @@ describe('RCS carousel label resolution in non-full mode', () => {
564
564
  return id.includes('{{name}}_') || key.includes('{{name}}_');
565
565
  });
566
566
 
567
+ // Card 0 has two `{{name}}` occurrences — title's, then description's, in DOM order — and
568
+ // each is now its own independent slot despite sharing the literal name.
567
569
  const card0NameAreas = nameVarAreaForCard(0);
568
- expect(card0NameAreas.length).toBeGreaterThanOrEqual(1);
570
+ expect(card0NameAreas.length).toBe(2);
569
571
 
570
572
  act(() => {
571
- const slotId = card0NameAreas.at(0).prop('id') || card0NameAreas.at(0).key();
572
- card0NameAreas.at(0).props().onChange({ target: { id: slotId, value: 'madhavi' } });
573
+ const slotId = nameVarAreas.at(0).prop('id') || nameVarAreas.at(0).key();
574
+ nameVarAreas.at(0).props().onChange({ target: { id: slotId, value: 'madhavi' } });
573
575
  });
574
576
  wrapper.update();
575
577
 
576
- // Inline preview should resolve card 0 only card 1's own `{{name}}` slot is untouched.
578
+ // Only the title resolvesdescription's own `{{name}}` slot is untouched (independent slot).
577
579
  const updatedInlinePreview = wrapper.find('UnifiedPreview').filterWhere((n) => n.prop('content')?.carouselData).at(0);
578
580
  const inlineCards = updatedInlinePreview.props().content.carouselData;
579
581
  expect(inlineCards[0].title).toBe('Hi madhavi');
580
- expect(inlineCards[0].bodyText).toBe('Welcome madhavi');
582
+ expect(inlineCards[0].bodyText).toBe('Welcome {{name}}');
581
583
  expect(inlineCards[1].title).toBe('Offer for {{name}}');
582
584
  expect(inlineCards[1].bodyText).toBe('Dear {{name}}');
583
585
 
584
- // Slidebox content should mirror the same per-card scoping (same underlying mapping)
586
+ // Filling card 0's description slot separately resolves it without disturbing the title.
587
+ act(() => {
588
+ const descSlotId = nameVarAreaForCard(0).at(1).prop('id') || nameVarAreaForCard(0).at(1).key();
589
+ nameVarAreaForCard(0).at(1).props().onChange({ target: { id: descSlotId, value: 'lakshmi' } });
590
+ });
591
+ wrapper.update();
592
+
593
+ const afterDescInlinePreview = wrapper.find('UnifiedPreview').filterWhere((n) => n.prop('content')?.carouselData).at(0);
594
+ const afterDescInlineCards = afterDescInlinePreview.props().content.carouselData;
595
+ expect(afterDescInlineCards[0].title).toBe('Hi madhavi');
596
+ expect(afterDescInlineCards[0].bodyText).toBe('Welcome lakshmi');
597
+
598
+ // Slidebox content should mirror the same independent-slot resolution.
585
599
  const slidebox = wrapper.find('TestAndPreviewSlidebox').at(0);
586
600
  expect(slidebox.exists()).toBe(true);
587
601
  const slideboxContent = slidebox.props().content;
588
602
  expect(slideboxContent?.carouselData?.length).toBeGreaterThanOrEqual(2);
589
603
  expect(slideboxContent.carouselData[0].title).toBe('Hi madhavi');
590
- expect(slideboxContent.carouselData[0].bodyText).toBe('Welcome madhavi');
604
+ expect(slideboxContent.carouselData[0].bodyText).toBe('Welcome lakshmi');
591
605
  expect(slideboxContent.carouselData[1].title).toBe('Offer for {{name}}');
592
606
  expect(slideboxContent.carouselData[1].bodyText).toBe('Dear {{name}}');
593
607
 
594
- // Setting card 1's own `{{name}}` slot independently resolves card 1 without disturbing card 0.
608
+ // Setting card 1's own `{{name}}` slots independently resolves card 1 without disturbing card 0.
595
609
  // Inactive tab panes aren't mounted until switched to, so activate card 1's tab first.
596
610
  act(() => {
597
611
  wrapper.find('CapTab').prop('onChange')('1');
598
612
  });
599
613
  wrapper.update();
600
614
  const card1NameAreas = nameVarAreaForCard(1);
601
- expect(card1NameAreas.length).toBeGreaterThanOrEqual(1);
615
+ expect(card1NameAreas.length).toBe(2);
602
616
  act(() => {
603
617
  const slotId = card1NameAreas.at(0).prop('id') || card1NameAreas.at(0).key();
604
618
  card1NameAreas.at(0).props().onChange({ target: { id: slotId, value: 'raghav' } });
605
619
  });
606
620
  wrapper.update();
621
+ act(() => {
622
+ const descSlotId = nameVarAreaForCard(1).at(1).prop('id') || nameVarAreaForCard(1).at(1).key();
623
+ nameVarAreaForCard(1).at(1).props().onChange({ target: { id: descSlotId, value: 'suresh' } });
624
+ });
625
+ wrapper.update();
607
626
 
608
627
  const finalInlinePreview = wrapper.find('UnifiedPreview').filterWhere((n) => n.prop('content')?.carouselData).at(0);
609
628
  const finalInlineCards = finalInlinePreview.props().content.carouselData;
610
629
  expect(finalInlineCards[0].title).toBe('Hi madhavi');
611
- expect(finalInlineCards[0].bodyText).toBe('Welcome madhavi');
630
+ expect(finalInlineCards[0].bodyText).toBe('Welcome lakshmi');
612
631
  expect(finalInlineCards[1].title).toBe('Offer for raghav');
613
- expect(finalInlineCards[1].bodyText).toBe('Dear raghav');
632
+ expect(finalInlineCards[1].bodyText).toBe('Dear suresh');
633
+ });
634
+
635
+ it('picking a tag from "Add Labels" for card 2\'s description (a bare semantic token with no numeric backing) shows the value live, without touching card 2\'s title slot', async () => {
636
+ const templateData = {
637
+ name: 'CarouselTagPickTemplate',
638
+ versions: {
639
+ base: {
640
+ content: {
641
+ RCS: {
642
+ rcsContent: {
643
+ cardType: 'CAROUSEL',
644
+ cardSettings: { cardOrientation: 'VERTICAL', cardWidth: 'SMALL' },
645
+ cardContent: [
646
+ {
647
+ title: 'Hi {{name}}',
648
+ description: 'Welcome {{name}}',
649
+ mediaType: 'IMAGE',
650
+ media: { mediaUrl: 'https://cdn.example.com/c1.png', height: 'MEDIUM' },
651
+ cardVarMapped: {},
652
+ suggestions: [],
653
+ },
654
+ {
655
+ title: 'sdvsdvs {{first_name_capital}}',
656
+ description: 'dssdfv {{adv}}',
657
+ mediaType: 'IMAGE',
658
+ media: { mediaUrl: 'https://cdn.example.com/c2.png', height: 'MEDIUM' },
659
+ cardVarMapped: {},
660
+ suggestions: [],
661
+ },
662
+ ],
663
+ contentType: 'RICHCARD',
664
+ },
665
+ },
666
+ },
667
+ },
668
+ },
669
+ type: 'RCS',
670
+ };
671
+
672
+ const wrapper = mountWithIntl(
673
+ <Provider store={store}>
674
+ <Rcs
675
+ actions={{
676
+ uploadRcsAsset,
677
+ clearRcsMediaAsset,
678
+ createTemplate,
679
+ clearCreateResponse,
680
+ getTemplateDetails,
681
+ editTemplate,
682
+ clearEditResponse,
683
+ }}
684
+ globalActions={{ fetchSchemaForEntity }}
685
+ onCreateComplete={onCreateComplete}
686
+ handleClose={handleClose}
687
+ intl={{ formatMessage }}
688
+ location={{ pathname: '/rcs/edit', query: { type: false, module: 'default' }, search: '' }}
689
+ params={params}
690
+ templateData={templateData}
691
+ rcsData={{}}
692
+ isFullMode={false}
693
+ isEditFlow={false}
694
+ loadingTags={false}
695
+ metaEntities={[]}
696
+ isDltEnabled={false}
697
+ smsRegister={'DLT'}
698
+ />
699
+ </Provider>,
700
+ );
701
+
702
+ // Flush hydration useEffects
703
+ await act(async () => {
704
+ await Promise.resolve();
705
+ });
706
+ wrapper.update();
707
+
708
+ // Card 2 is inactive until switched to.
709
+ act(() => {
710
+ wrapper.find('CapTab').prop('onChange')('1');
711
+ });
712
+ wrapper.update();
713
+
714
+ const card2 = wrapper.find('CarouselCard').filterWhere((n) => n.prop('cardIndex') === 1);
715
+
716
+ const descVarArea = card2
717
+ .find('CapInputTextArea')
718
+ .filterWhere((n) => String(n.prop('id') || '').includes('{{adv}}_'));
719
+ expect(descVarArea.length).toBeGreaterThanOrEqual(1);
720
+
721
+ // Focus card 2's description var slot, exactly like a user clicking into that box.
722
+ act(() => {
723
+ const id = descVarArea.at(0).prop('id');
724
+ descVarArea.at(0).props().onFocus({ target: { id } });
725
+ });
726
+ wrapper.update();
727
+
728
+ // Card 2's description TagList (the second .tag-mock under this card) picks "adv".
729
+ const card2AfterFocus = wrapper.find('CarouselCard').filterWhere((n) => n.prop('cardIndex') === 1);
730
+ const descTagList = card2AfterFocus.find('.tag-mock').at(1);
731
+ act(() => {
732
+ descTagList.props().onTagSelect('adv');
733
+ });
734
+ wrapper.update();
735
+
736
+ // Live (no save/reload) — the description's own box should now show the picked tag as its value.
737
+ const card2Final = wrapper.find('CarouselCard').filterWhere((n) => n.prop('cardIndex') === 1);
738
+ const descVarAreaAfterPick = card2Final
739
+ .find('CapInputTextArea')
740
+ .filterWhere((n) => String(n.prop('id') || '').includes('{{adv}}_'));
741
+ expect(descVarAreaAfterPick.at(0).prop('value')).toBe('{{adv}}');
742
+
743
+ // Title's own slot must be untouched by the description's tag pick.
744
+ const titleVarAreaAfterPick = card2Final
745
+ .find('CapInputTextArea')
746
+ .filterWhere((n) => String(n.prop('id') || '').includes('{{first_name_capital}}_'));
747
+ expect(titleVarAreaAfterPick.at(0).prop('value')).toBe('');
748
+ });
749
+
750
+ it('reopening a carousel after a prior save shows card 2\'s description value (persisted under its global positional key), not empty', async () => {
751
+ // Shape of a template that was already saved once: card 2 (index 1) picked "adv" for its
752
+ // description slot in an earlier session. Card 0 has 2 var tokens (title + desc), so card 2's
753
+ // title is global position 3 and its description is global position 4 — key "4".
754
+ const templateData = {
755
+ name: 'CarouselReopenTemplate',
756
+ versions: {
757
+ base: {
758
+ content: {
759
+ RCS: {
760
+ rcsContent: {
761
+ cardType: 'CAROUSEL',
762
+ cardSettings: { cardOrientation: 'VERTICAL', cardWidth: 'SMALL' },
763
+ cardContent: [
764
+ {
765
+ title: 'Hi {{name}}',
766
+ description: 'Welcome {{name}}',
767
+ mediaType: 'IMAGE',
768
+ media: { mediaUrl: 'https://cdn.example.com/c1.png', height: 'MEDIUM' },
769
+ cardVarMapped: {},
770
+ suggestions: [],
771
+ },
772
+ {
773
+ title: 'sdvsdvs {{first_name_capital}}',
774
+ description: 'dssdfv {{adv}}',
775
+ mediaType: 'IMAGE',
776
+ media: { mediaUrl: 'https://cdn.example.com/c2.png', height: 'MEDIUM' },
777
+ cardVarMapped: { '4': '{{adv}}' },
778
+ suggestions: [],
779
+ },
780
+ ],
781
+ contentType: 'RICHCARD',
782
+ },
783
+ },
784
+ },
785
+ },
786
+ },
787
+ type: 'RCS',
788
+ };
789
+
790
+ const wrapper = mountWithIntl(
791
+ <Provider store={store}>
792
+ <Rcs
793
+ actions={{
794
+ uploadRcsAsset,
795
+ clearRcsMediaAsset,
796
+ createTemplate,
797
+ clearCreateResponse,
798
+ getTemplateDetails,
799
+ editTemplate,
800
+ clearEditResponse,
801
+ }}
802
+ globalActions={{ fetchSchemaForEntity }}
803
+ onCreateComplete={onCreateComplete}
804
+ handleClose={handleClose}
805
+ intl={{ formatMessage }}
806
+ location={{ pathname: '/rcs/edit', query: { type: false, module: 'default' }, search: '' }}
807
+ params={params}
808
+ templateData={templateData}
809
+ rcsData={{}}
810
+ isFullMode={false}
811
+ isEditFlow={false}
812
+ loadingTags={false}
813
+ metaEntities={[]}
814
+ isDltEnabled={false}
815
+ smsRegister={'DLT'}
816
+ />
817
+ </Provider>,
818
+ );
819
+
820
+ await act(async () => {
821
+ await Promise.resolve();
822
+ });
823
+ wrapper.update();
824
+
825
+ act(() => {
826
+ wrapper.find('CapTab').prop('onChange')('1');
827
+ });
828
+ wrapper.update();
829
+
830
+ const card2 = wrapper.find('CarouselCard').filterWhere((n) => n.prop('cardIndex') === 1);
831
+ const descVarArea = card2
832
+ .find('CapInputTextArea')
833
+ .filterWhere((n) => String(n.prop('id') || '').includes('{{adv}}_'));
834
+ expect(descVarArea.length).toBeGreaterThanOrEqual(1);
835
+ expect(descVarArea.at(0).prop('value')).toBe('{{adv}}');
614
836
  });
615
837
  });
616
838
 
@@ -967,130 +1189,6 @@ describe('RCS createPayload', () => {
967
1189
  expect(Object.prototype.hasOwnProperty.call(card, 'cardVarMapped')).toBe(true);
968
1190
  });
969
1191
 
970
- it('disables Done in non-full mode when a CTA button URL still holds the unresolved {{1}} placeholder', () => {
971
- const templateData = {
972
- name: 'PromoCard',
973
- type: 'RCS',
974
- versions: {
975
- base: {
976
- content: {
977
- RCS: {
978
- rcsContent: {
979
- cardType: 'STANDALONE',
980
- cardSettings: { cardOrientation: 'VERTICAL' },
981
- cardContent: [
982
- {
983
- title: 'Title',
984
- description: 'Desc',
985
- mediaType: 'VIDEO',
986
- media: {
987
- mediaUrl: 'https://cdn.example.com/video.mp4',
988
- thumbnailUrl: 'https://cdn.example.com/video-thumb.jpg',
989
- },
990
- suggestions: [
991
- { type: 'CTA', text: 'Visit here', url: 'https://example.com{{1}}' },
992
- ],
993
- },
994
- ],
995
- contentType: 'RICHCARD',
996
- },
997
- },
998
- },
999
- },
1000
- },
1001
- };
1002
-
1003
- const wrapper = mountWithIntl(
1004
- <Provider store={store}>
1005
- <Rcs
1006
- actions={{
1007
- clearCreateResponse: jest.fn(),
1008
- getTemplateDetails: jest.fn(),
1009
- uploadRcsAsset: jest.fn(),
1010
- clearRcsMediaAsset: jest.fn(),
1011
- editTemplate: jest.fn(),
1012
- clearEditResponse: jest.fn(),
1013
- }}
1014
- globalActions={{ fetchSchemaForEntity }}
1015
- onCreateComplete={onCreateComplete}
1016
- handleClose={handleClose}
1017
- intl={{ formatMessage }}
1018
- location={{ pathname: '/rcs/create', query: { type: false, module: 'default' }, search: '' }}
1019
- params={params}
1020
- templateData={templateData}
1021
- rcsData={{}}
1022
- isFullMode={false}
1023
- isEditFlow={false}
1024
- loadingTags={false}
1025
- metaEntities={[]}
1026
- isDltEnabled={false}
1027
- smsRegister={'DLT'}
1028
- getFormData={jest.fn()}
1029
- />
1030
- </Provider>,
1031
- );
1032
-
1033
- const doneBtn = wrapper.find('CapButton.rcs-done-btn').at(0);
1034
- expect(doneBtn.exists()).toBe(true);
1035
- expect(doneBtn.prop('disabled')).toBe(true);
1036
- });
1037
-
1038
- it('disables Send for Approval in full-mode creation when a CTA button URL still holds the unresolved {{1}} placeholder', () => {
1039
- const wrapper = mountWithIntl(
1040
- <Provider store={store}>
1041
- <Rcs
1042
- actions={{
1043
- clearCreateResponse: jest.fn(),
1044
- getTemplateDetails: jest.fn(),
1045
- uploadRcsAsset: jest.fn(),
1046
- clearRcsMediaAsset: jest.fn(),
1047
- editTemplate: jest.fn(),
1048
- clearEditResponse: jest.fn(),
1049
- }}
1050
- globalActions={{ fetchSchemaForEntity }}
1051
- onCreateComplete={onCreateComplete}
1052
- handleClose={handleClose}
1053
- intl={{ formatMessage }}
1054
- location={{ pathname: '/rcs/create', query: { type: false, module: 'default' }, search: '' }}
1055
- params={params}
1056
- templateData={{}}
1057
- rcsData={{}}
1058
- isFullMode
1059
- isEditFlow={false}
1060
- loadingTags={false}
1061
- metaEntities={[]}
1062
- isDltEnabled={false}
1063
- smsRegister={'DLT'}
1064
- getFormData={jest.fn()}
1065
- />
1066
- </Provider>,
1067
- );
1068
-
1069
- act(() => {
1070
- wrapper.find('#rcs_template_name_input').at(0).simulate('change', { target: { value: 'PromoCard' } });
1071
- });
1072
- wrapper.update();
1073
-
1074
- act(() => {
1075
- wrapper.find('#rcs_template_message_textarea').at(0).simulate('change', { target: { value: 'Hello there' } });
1076
- });
1077
- wrapper.update();
1078
-
1079
- const actionButton = wrapper.find('.cap-action-button-mock').at(0);
1080
- expect(actionButton.exists()).toBe(true);
1081
- act(() => {
1082
- actionButton.prop('updateButtonChange')(
1083
- { type: 'CTA', text: 'Visit here', url: 'https://example.com{{1}}' },
1084
- 0,
1085
- );
1086
- });
1087
- wrapper.update();
1088
-
1089
- const doneBtn = wrapper.find('CapButton.rcs-done-btn').at(0);
1090
- expect(doneBtn.exists()).toBe(true);
1091
- expect(doneBtn.prop('disabled')).toBe(true);
1092
- });
1093
-
1094
1192
  it('should sync duplicate placeholders via cardVarMapped in non-full mode (typing once updates all occurrences)', () => {
1095
1193
  const templateData = {
1096
1194
  name: 'DupVarsTemplate',
@@ -1188,12 +1286,7 @@ describe('RCS createPayload', () => {
1188
1286
  cardContent: [
1189
1287
  {
1190
1288
  title: 'Hello {{user_name}} and {{user_name}}',
1191
- // Distinct from the title's var name: VarSegmentMessageEditor ids are
1192
- // `${token}_${localSegmentIndex}`, not scoped per-field, so reusing
1193
- // {{user_name}} here would collide with the title's own id and get
1194
- // picked up by the `{{user_name}}_` filter below once the tag-select
1195
- // renames the title's first occurrence.
1196
- description: 'Hi {{other_var}}',
1289
+ description: 'Hi {{user_name}}',
1197
1290
  mediaType: 'IMAGE',
1198
1291
  media: { mediaUrl: '', thumbnailUrl: '' },
1199
1292
  cardVarMapped: {},
@@ -1259,18 +1352,12 @@ describe('RCS createPayload', () => {
1259
1352
  });
1260
1353
  wrapper.update();
1261
1354
 
1262
- // The focused occurrence's own token is replaced with the picked tag, while the untouched
1263
- // duplicate keeps its original {{user_name}} token but syncs to the same resolved value.
1264
- const renamedVarArea = wrapper.find('CapInputTextArea').filterWhere((n) => {
1265
- const id = n.prop('id') || '';
1266
- return id.includes('{{first_name}}_');
1267
- });
1268
- const duplicateVarArea = wrapper.find('CapInputTextArea').filterWhere((n) => {
1355
+ const updatedTitleVarAreas = wrapper.find('CapInputTextArea').filterWhere((n) => {
1269
1356
  const id = n.prop('id') || '';
1270
1357
  return id.includes('{{user_name}}_');
1271
1358
  });
1272
- expect(renamedVarArea.at(0).prop('value')).toBe('{{first_name}}');
1273
- expect(duplicateVarArea.at(0).prop('value')).toBe('{{first_name}}');
1359
+ expect(updatedTitleVarAreas.at(0).prop('value')).toBe('{{first_name}}');
1360
+ expect(updatedTitleVarAreas.at(1).prop('value')).toBe('{{first_name}}');
1274
1361
  });
1275
1362
 
1276
1363
  it('should keep two tags + freetext inside the variable textarea in non-full mode edit (not merged into static text)', () => {
@@ -364,14 +364,6 @@ describe('rcsLibraryHydrationUtils', () => {
364
364
  expect(out).toEqual({ 1: 'x', user_name: 'y' });
365
365
  });
366
366
 
367
- it('keeps function-style tag keys with parentheses (voucher/offer/badge tags)', () => {
368
- const input = {
369
- 'voucher(1190738)': 'SAVE20',
370
- 'badges_enroll_no_expiry(123)': 'Gold',
371
- };
372
- expect(pickRcsCardVarMappedEntries(input)).toEqual(input);
373
- });
374
-
375
367
  it('returns {} for an empty object', () => {
376
368
  expect(pickRcsCardVarMappedEntries({})).toEqual({});
377
369
  });
@@ -447,7 +439,6 @@ describe('rcsLibraryHydrationUtils', () => {
447
439
  template: 'Hello',
448
440
  templateName: 'T1',
449
441
  registeredSenderIds: ['S1'],
450
- templateVariableMapping: { '{#a#}_0': { data: 'x', count: 1 } },
451
442
  },
452
443
  },
453
444
  });
@@ -506,7 +497,6 @@ describe('rcsLibraryHydrationUtils', () => {
506
497
  expect(out.smsFallBackContent.templateConfigs).toEqual({
507
498
  templateName: 'ConfigName',
508
499
  registeredSenderIds: ['H1'],
509
- templateVariableMapping: { a: { data: '1', count: 1 } },
510
500
  });
511
501
  expect(out.smsFallBackContent.rcsSmsFallbackVarMapped).toEqual({ a: '1' });
512
502
  });
@@ -547,25 +537,6 @@ describe('rcsLibraryHydrationUtils', () => {
547
537
  });
548
538
  });
549
539
 
550
- it('non-full mode: DLT campaign with registeredSenderIds carries the var map into templateConfigs.templateVariableMapping (so it survives normalizeRcsMessageContentForApi stripping rcsSmsFallbackVarMapped)', () => {
551
- enableTraiDlt();
552
- const out = buildSmsFallBackContentForPayload({
553
- smsFallbackData: {
554
- message: 'Hi {#var#} your OTP is {#var#}.Capillary Tech.',
555
- templateName: 'TEST_FALLBACK',
556
- registeredSenderIds: ['CPLRYT'],
557
- rcsSmsFallbackVarMapped: { '{#var#}_1': 'test1', '{#var#}_3': 'test2' },
558
- },
559
- templateData: {},
560
- isFullMode: false,
561
- smsRegister: 'DLT',
562
- });
563
- expect(out.smsFallBackContent.templateConfigs.templateVariableMapping).toEqual({
564
- '{#var#}_1': { data: 'test1', count: 1 },
565
- '{#var#}_3': { data: 'test2', count: 2 },
566
- });
567
- });
568
-
569
540
  it('non-full mode: DLT campaign but no derivable templateConfigs omits templateConfigs', () => {
570
541
  enableTraiDlt();
571
542
  const out = buildSmsFallBackContentForPayload({
@@ -685,45 +656,6 @@ describe('rcsLibraryHydrationUtils', () => {
685
656
  expect(warnSpy).toHaveBeenCalled();
686
657
  });
687
658
 
688
- it('falls back to templateConfigs.templateName when root-level name fields are absent', () => {
689
- const out = resolveSmsFallbackHydrationFromDetails({
690
- smsFallBackContent: {
691
- templateConfigs: { templateName: 'ConfigTemplateName' },
692
- },
693
- });
694
- expect(out.nextSmsState.templateName).toBe('ConfigTemplateName');
695
- });
696
-
697
- it('falls back to templateConfigs.name when no other name field is present anywhere', () => {
698
- const out = resolveSmsFallbackHydrationFromDetails({
699
- smsFallBackContent: {
700
- templateConfigs: { name: 'ConfigName' },
701
- },
702
- });
703
- expect(out.nextSmsState.templateName).toBe('ConfigName');
704
- });
705
-
706
- it('falls back to the root-level templateName when templateConfigs has no name field', () => {
707
- const out = resolveSmsFallbackHydrationFromDetails({
708
- smsFallBackContent: {
709
- templateName: 'RootName',
710
- templateConfigs: { registeredSenderIds: ['123'] },
711
- },
712
- });
713
- expect(out.nextSmsState.templateName).toBe('RootName');
714
- });
715
-
716
- it('prefers the root templateName over a conflicting nested templateConfigs.name', () => {
717
- const out = resolveSmsFallbackHydrationFromDetails({
718
- smsFallBackContent: {
719
- templateName: 'RootName',
720
- smsTemplateName: '',
721
- templateConfigs: { name: 'NestedName', header: ['H1'] },
722
- },
723
- });
724
- expect(out.nextSmsState.templateName).toBe('RootName');
725
- });
726
-
727
659
  it('is meaningful from var map alone, with no template name or message text', () => {
728
660
  const out = resolveSmsFallbackHydrationFromDetails({
729
661
  smsFallBackContent: { rcsSmsFallbackVarMapped: { '{#a#}_0': 'v' } },
@@ -773,48 +705,6 @@ describe('rcsLibraryHydrationUtils', () => {
773
705
  ).toBe('Direct Template');
774
706
  });
775
707
 
776
- it('prefers templateConfigs.template (raw, {#var#} intact) over smsContent/message for DLT', () => {
777
- const out = resolveSmsFallbackHydrationFromDetails({
778
- smsFallBackContent: {
779
- smsContent: 'hey user this is a test',
780
- message: 'hey user this is a test',
781
- templateConfigs: {
782
- templateName: 'RCSDLT2',
783
- template: 'hey{#var#}test',
784
- templateVariableMapping: {
785
- '{#var#}_1': { data: ' user this is a ', count: 1 },
786
- },
787
- },
788
- },
789
- });
790
- expect(out.nextSmsState.content).toBe('hey{#var#}test');
791
- expect(out.nextSmsState.templateContent).toBe('hey{#var#}test');
792
- expect(out.nextSmsState.rcsSmsFallbackVarMapped).toEqual({ '{#var#}_1': ' user this is a ' });
793
- });
794
-
795
- it('merges templateVariableMapping-derived slots with an explicit rcsSmsFallbackVarMapped (explicit wins)', () => {
796
- const out = resolveSmsFallbackHydrationFromDetails({
797
- smsFallBackContent: {
798
- smsContent: 'hey user this is a test',
799
- rcsSmsFallbackVarMapped: { '{#var#}_1': 'explicit override' },
800
- templateConfigs: {
801
- template: 'hey{#var#}test',
802
- templateVariableMapping: {
803
- '{#var#}_1': { data: ' user this is a ', count: 1 },
804
- },
805
- },
806
- },
807
- });
808
- expect(out.nextSmsState.rcsSmsFallbackVarMapped).toEqual({ '{#var#}_1': 'explicit override' });
809
- });
810
-
811
- it('falls back to smsContent when templateConfigs.template is absent (non-DLT)', () => {
812
- const out = resolveSmsFallbackHydrationFromDetails({
813
- smsFallBackContent: { smsContent: 'Hi {{first_name}}, your OTP is 1234' },
814
- });
815
- expect(out.nextSmsState.content).toBe('Hi {{first_name}}, your OTP is 1234');
816
- });
817
-
818
708
  it('joins an updated-sms-editor array into the fallback message', () => {
819
709
  const out = resolveSmsFallbackHydrationFromDetails({
820
710
  smsFallBackContent: {
@@ -669,6 +669,23 @@ describe('RCS utils', () => {
669
669
  expect(out[0].description).toBe('This is Bird with colorful feathers {{voucher(1190738)}}');
670
670
  });
671
671
 
672
+ it('substitutes plain text typed into a numeric personalization slot (not a tag pick)', () => {
673
+ const out = mapRcsCardContentForConsumerWithResolvedTags(
674
+ [
675
+ {
676
+ title: 'Hello {{1}}',
677
+ description: 'Visit us at {{2}} today',
678
+ mediaType: 'IMAGE',
679
+ cardVarMapped: { 1: 'World', 2: 'Downtown Store' },
680
+ },
681
+ ],
682
+ {},
683
+ false,
684
+ );
685
+ expect(out[0].title).toBe('Hello World');
686
+ expect(out[0].description).toBe('Visit us at Downtown Store today');
687
+ });
688
+
672
689
  it('BUG REPRO: 2-card carousel — card 1 description resolves to card 0\'s value instead of its own tag pick', () => {
673
690
  // Mirrors CreativesContainer/index.js ~1448-1461: rootRcsCardVarMapped is derived ONLY from
674
691
  // cardContentFromSubmit[0] (the FIRST card's own cardVarMapped) and reused for every card.
@@ -764,42 +781,6 @@ describe('RCS utils', () => {
764
781
  expect(rcsTitle).toBe('');
765
782
  expect(rcsDesc).toBe('Visit Store for {{loyalty_points}} discount');
766
783
  });
767
-
768
- it('carousel card 1+: cardSlotOffset param resolves against its own global slots, not card 0\'s', () => {
769
- // Full, correctly-populated carousel-wide map: slot 1 = card 0 title (user_id_b64), slot 2 =
770
- // card 0 desc (voucher(1190738)), slot 3 = card 1 title (user_id_b64), slot 4 = card 1 desc
771
- // (first_name).
772
- const fullCarouselWideMap = {
773
- 1: '{{user_id_b64}}',
774
- 2: '{{voucher(1190738)}}',
775
- 3: '{{user_id_b64}}',
776
- 4: '{{first_name}}',
777
- };
778
- // Card 1's own title/description, called with cardSlotOffset=2 (card 0 contributed 2 slots) —
779
- // the same way mapRcsCardContentForConsumerWithResolvedTags now calls it per card.
780
- const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
781
- 'This is a park {{3}}',
782
- 'This park has a cat and a bird {{4}}',
783
- fullCarouselWideMap,
784
- true,
785
- false,
786
- false,
787
- 2,
788
- );
789
- expect(rcsTitle).toBe('This is a park {{user_id_b64}}');
790
- expect(rcsDesc).toBe('This park has a cat and a bird {{first_name}}');
791
- });
792
-
793
- it('omitting cardSlotOffset defaults to 0 (single-card / standalone callers unaffected)', () => {
794
- const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
795
- 'Hi {{1}}',
796
- 'Pts {{2}}',
797
- { 1: '{{first_name}}', 2: '{{loyalty_points}}' },
798
- true,
799
- );
800
- expect(rcsTitle).toBe('Hi {{first_name}}');
801
- expect(rcsDesc).toBe('Pts {{loyalty_points}}');
802
- });
803
784
  });
804
785
 
805
786
  describe('sanitizeCardVarMappedValue', () => {