@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.
- package/app.js +175 -10
- package/entry.js +1 -67
- package/mfe-exposed-components.js +4 -2
- package/package.json +2 -2
- package/services/api.js +1 -9
- package/styles/containers/layout/_layoutPage.scss +6 -8
- package/utils/gtmTrackers/gtmEvents/creativeDetails.js +1 -2
- package/utils/rcsPayloadUtils.js +2 -5
- package/utils/tests/rcsPayloadUtils.test.js +2 -54
- package/v2Components/CapActionButton/constants.js +0 -1
- package/v2Components/CapActionButton/index.js +9 -77
- package/v2Components/CapActionButton/index.scss +0 -13
- package/v2Components/CapActionButton/messages.js +0 -13
- package/v2Components/CapActionButton/tests/index.test.js +1 -32
- package/v2Components/CommonTestAndPreview/index.js +15 -44
- package/v2Components/CommonTestAndPreview/tests/index.test.js +0 -78
- package/v2Components/CommonTestAndPreview/utils.js +0 -34
- package/v2Components/NavigationBar/index.js +7 -9
- package/v2Components/NavigationBar/tests/index.test.js +19 -39
- package/v2Components/SmsFallback/index.js +0 -6
- package/v2Containers/Cap/constants.js +0 -1
- package/v2Containers/Cap/index.js +27 -57
- package/v2Containers/CreativesContainer/index.js +8 -11
- package/v2Containers/CreativesContainer/tests/index.test.js +0 -53
- package/v2Containers/Rcs/carouselUtils.js +73 -46
- package/v2Containers/Rcs/components/CarouselCard.js +9 -11
- package/v2Containers/Rcs/components/CarouselCardButtons.js +0 -20
- package/v2Containers/Rcs/constants.js +7 -2
- package/v2Containers/Rcs/index.js +205 -226
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +47 -60
- package/v2Containers/Rcs/tests/CarouselCard.test.js +2 -1
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +0 -286
- package/v2Containers/Rcs/tests/carouselUtils.test.js +32 -28
- package/v2Containers/Rcs/tests/index.test.js +238 -151
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +0 -110
- package/v2Containers/Rcs/tests/utils.test.js +17 -36
- package/v2Containers/Rcs/utils.js +55 -28
- package/v2Containers/SmsTrai/Edit/index.js +24 -35
- package/v2Containers/Templates/_templates.scss +1 -1
- package/v2Containers/Templates/index.js +0 -1
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +0 -5
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +3 -8
- package/v2Containers/TemplatesV2/index.js +9 -22
- package/AppRoot.js +0 -124
- package/app-config.js +0 -61
- package/bootstrap.js +0 -185
- package/utils/getDataLayer.js +0 -15
- package/utils/mfeDetect.js +0 -1
- package/utils/mfeFirstPaintReady.js +0 -29
- package/utils/mfeHistory.js +0 -62
- 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
|
|
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).
|
|
570
|
+
expect(card0NameAreas.length).toBe(2);
|
|
569
571
|
|
|
570
572
|
act(() => {
|
|
571
|
-
const slotId =
|
|
572
|
-
|
|
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
|
-
//
|
|
578
|
+
// Only the title resolves — description'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
|
|
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
|
-
//
|
|
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
|
|
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}}`
|
|
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).
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1273
|
-
expect(
|
|
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', () => {
|