@capillarytech/creatives-library 9.0.29-alpha.1 → 9.0.29-alpha.2
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/package.json +1 -1
- 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 -76
- package/v2Components/CapActionButton/index.scss +0 -13
- package/v2Components/CapActionButton/messages.js +0 -13
- package/v2Components/CapActionButton/tests/index.test.js +1 -32
- package/v2Containers/Rcs/carouselUtils.js +79 -47
- 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 +151 -56
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +43 -5
- package/v2Containers/Rcs/tests/CarouselCard.test.js +2 -1
- package/v2Containers/Rcs/tests/carouselUtils.test.js +15 -15
- package/v2Containers/Rcs/tests/index.test.js +265 -84
- package/v2Containers/Rcs/tests/utils.test.js +80 -0
- package/v2Containers/Rcs/utils.js +63 -6
|
@@ -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('
|
|
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: {
|
|
@@ -553,13 +553,21 @@ describe('RCS carousel label resolution in non-full mode', () => {
|
|
|
553
553
|
expect(inlinePreview.exists()).toBe(true);
|
|
554
554
|
expect(inlinePreview.props().content?.carouselData?.length).toBeGreaterThanOrEqual(2);
|
|
555
555
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
556
|
+
const nameVarAreaForCard = (cardIndex) =>
|
|
557
|
+
wrapper
|
|
558
|
+
.find('CarouselCard')
|
|
559
|
+
.filterWhere((n) => n.prop('cardIndex') === cardIndex)
|
|
560
|
+
.find('CapInputTextArea')
|
|
561
|
+
.filterWhere((n) => {
|
|
562
|
+
const id = String(n.prop('id') || '');
|
|
563
|
+
const key = String(n.key() || '');
|
|
564
|
+
return id.includes('{{name}}_') || key.includes('{{name}}_');
|
|
565
|
+
});
|
|
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.
|
|
569
|
+
const card0NameAreas = nameVarAreaForCard(0);
|
|
570
|
+
expect(card0NameAreas.length).toBe(2);
|
|
563
571
|
|
|
564
572
|
act(() => {
|
|
565
573
|
const slotId = nameVarAreas.at(0).prop('id') || nameVarAreas.at(0).key();
|
|
@@ -567,23 +575,264 @@ describe('RCS carousel label resolution in non-full mode', () => {
|
|
|
567
575
|
});
|
|
568
576
|
wrapper.update();
|
|
569
577
|
|
|
570
|
-
//
|
|
578
|
+
// Only the title resolves — description's own `{{name}}` slot is untouched (independent slot).
|
|
571
579
|
const updatedInlinePreview = wrapper.find('UnifiedPreview').filterWhere((n) => n.prop('content')?.carouselData).at(0);
|
|
572
580
|
const inlineCards = updatedInlinePreview.props().content.carouselData;
|
|
573
581
|
expect(inlineCards[0].title).toBe('Hi madhavi');
|
|
574
|
-
expect(inlineCards[0].bodyText).toBe('Welcome
|
|
575
|
-
expect(inlineCards[1].title).toBe('Offer for
|
|
576
|
-
expect(inlineCards[1].bodyText).toBe('Dear
|
|
582
|
+
expect(inlineCards[0].bodyText).toBe('Welcome {{name}}');
|
|
583
|
+
expect(inlineCards[1].title).toBe('Offer for {{name}}');
|
|
584
|
+
expect(inlineCards[1].bodyText).toBe('Dear {{name}}');
|
|
585
|
+
|
|
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');
|
|
577
597
|
|
|
578
|
-
// Slidebox content should
|
|
598
|
+
// Slidebox content should mirror the same independent-slot resolution.
|
|
579
599
|
const slidebox = wrapper.find('TestAndPreviewSlidebox').at(0);
|
|
580
600
|
expect(slidebox.exists()).toBe(true);
|
|
581
601
|
const slideboxContent = slidebox.props().content;
|
|
582
602
|
expect(slideboxContent?.carouselData?.length).toBeGreaterThanOrEqual(2);
|
|
583
603
|
expect(slideboxContent.carouselData[0].title).toBe('Hi madhavi');
|
|
584
|
-
expect(slideboxContent.carouselData[0].bodyText).toBe('Welcome
|
|
585
|
-
expect(slideboxContent.carouselData[1].title).toBe('Offer for
|
|
586
|
-
expect(slideboxContent.carouselData[1].bodyText).toBe('Dear
|
|
604
|
+
expect(slideboxContent.carouselData[0].bodyText).toBe('Welcome lakshmi');
|
|
605
|
+
expect(slideboxContent.carouselData[1].title).toBe('Offer for {{name}}');
|
|
606
|
+
expect(slideboxContent.carouselData[1].bodyText).toBe('Dear {{name}}');
|
|
607
|
+
|
|
608
|
+
// Setting card 1's own `{{name}}` slots independently resolves card 1 without disturbing card 0.
|
|
609
|
+
// Inactive tab panes aren't mounted until switched to, so activate card 1's tab first.
|
|
610
|
+
act(() => {
|
|
611
|
+
wrapper.find('CapTab').prop('onChange')('1');
|
|
612
|
+
});
|
|
613
|
+
wrapper.update();
|
|
614
|
+
const card1NameAreas = nameVarAreaForCard(1);
|
|
615
|
+
expect(card1NameAreas.length).toBe(2);
|
|
616
|
+
act(() => {
|
|
617
|
+
const slotId = card1NameAreas.at(0).prop('id') || card1NameAreas.at(0).key();
|
|
618
|
+
card1NameAreas.at(0).props().onChange({ target: { id: slotId, value: 'raghav' } });
|
|
619
|
+
});
|
|
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();
|
|
626
|
+
|
|
627
|
+
const finalInlinePreview = wrapper.find('UnifiedPreview').filterWhere((n) => n.prop('content')?.carouselData).at(0);
|
|
628
|
+
const finalInlineCards = finalInlinePreview.props().content.carouselData;
|
|
629
|
+
expect(finalInlineCards[0].title).toBe('Hi madhavi');
|
|
630
|
+
expect(finalInlineCards[0].bodyText).toBe('Welcome lakshmi');
|
|
631
|
+
expect(finalInlineCards[1].title).toBe('Offer for 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}}');
|
|
587
836
|
});
|
|
588
837
|
});
|
|
589
838
|
|
|
@@ -940,74 +1189,6 @@ describe('RCS createPayload', () => {
|
|
|
940
1189
|
expect(Object.prototype.hasOwnProperty.call(card, 'cardVarMapped')).toBe(true);
|
|
941
1190
|
});
|
|
942
1191
|
|
|
943
|
-
it('disables Done in non-full mode when a CTA button URL still holds the unresolved {{1}} placeholder', () => {
|
|
944
|
-
const templateData = {
|
|
945
|
-
name: 'PromoCard',
|
|
946
|
-
type: 'RCS',
|
|
947
|
-
versions: {
|
|
948
|
-
base: {
|
|
949
|
-
content: {
|
|
950
|
-
RCS: {
|
|
951
|
-
rcsContent: {
|
|
952
|
-
cardType: 'STANDALONE',
|
|
953
|
-
cardSettings: { cardOrientation: 'VERTICAL' },
|
|
954
|
-
cardContent: [
|
|
955
|
-
{
|
|
956
|
-
title: 'Title',
|
|
957
|
-
description: 'Desc',
|
|
958
|
-
mediaType: 'VIDEO',
|
|
959
|
-
media: {
|
|
960
|
-
mediaUrl: 'https://cdn.example.com/video.mp4',
|
|
961
|
-
thumbnailUrl: 'https://cdn.example.com/video-thumb.jpg',
|
|
962
|
-
},
|
|
963
|
-
suggestions: [
|
|
964
|
-
{ type: 'CTA', text: 'Visit here', url: 'https://example.com{{1}}' },
|
|
965
|
-
],
|
|
966
|
-
},
|
|
967
|
-
],
|
|
968
|
-
contentType: 'RICHCARD',
|
|
969
|
-
},
|
|
970
|
-
},
|
|
971
|
-
},
|
|
972
|
-
},
|
|
973
|
-
},
|
|
974
|
-
};
|
|
975
|
-
|
|
976
|
-
const wrapper = mountWithIntl(
|
|
977
|
-
<Provider store={store}>
|
|
978
|
-
<Rcs
|
|
979
|
-
actions={{
|
|
980
|
-
clearCreateResponse: jest.fn(),
|
|
981
|
-
getTemplateDetails: jest.fn(),
|
|
982
|
-
uploadRcsAsset: jest.fn(),
|
|
983
|
-
clearRcsMediaAsset: jest.fn(),
|
|
984
|
-
editTemplate: jest.fn(),
|
|
985
|
-
clearEditResponse: jest.fn(),
|
|
986
|
-
}}
|
|
987
|
-
globalActions={{ fetchSchemaForEntity }}
|
|
988
|
-
onCreateComplete={onCreateComplete}
|
|
989
|
-
handleClose={handleClose}
|
|
990
|
-
intl={{ formatMessage }}
|
|
991
|
-
location={{ pathname: '/rcs/create', query: { type: false, module: 'default' }, search: '' }}
|
|
992
|
-
params={params}
|
|
993
|
-
templateData={templateData}
|
|
994
|
-
rcsData={{}}
|
|
995
|
-
isFullMode={false}
|
|
996
|
-
isEditFlow={false}
|
|
997
|
-
loadingTags={false}
|
|
998
|
-
metaEntities={[]}
|
|
999
|
-
isDltEnabled={false}
|
|
1000
|
-
smsRegister={'DLT'}
|
|
1001
|
-
getFormData={jest.fn()}
|
|
1002
|
-
/>
|
|
1003
|
-
</Provider>,
|
|
1004
|
-
);
|
|
1005
|
-
|
|
1006
|
-
const doneBtn = wrapper.find('CapButton.rcs-done-btn').at(0);
|
|
1007
|
-
expect(doneBtn.exists()).toBe(true);
|
|
1008
|
-
expect(doneBtn.prop('disabled')).toBe(true);
|
|
1009
|
-
});
|
|
1010
|
-
|
|
1011
1192
|
it('should sync duplicate placeholders via cardVarMapped in non-full mode (typing once updates all occurrences)', () => {
|
|
1012
1193
|
const templateData = {
|
|
1013
1194
|
name: 'DupVarsTemplate',
|
|
@@ -648,6 +648,86 @@ describe('RCS utils', () => {
|
|
|
648
648
|
const out = mapRcsCardContentForConsumerWithResolvedTags([null, 'string', 42], {}, false);
|
|
649
649
|
expect(out).toEqual([null, 'string', 42]);
|
|
650
650
|
});
|
|
651
|
+
|
|
652
|
+
it('preserves a voucher/offer tag value (parenthesized key) in the emitted cardVarMapped, and keeps the raw tag in the description', () => {
|
|
653
|
+
const out = mapRcsCardContentForConsumerWithResolvedTags(
|
|
654
|
+
[
|
|
655
|
+
{
|
|
656
|
+
title: '',
|
|
657
|
+
description: 'This is Bird with colorful feathers {{voucher(1190738)}}',
|
|
658
|
+
mediaType: 'NONE',
|
|
659
|
+
cardVarMapped: { 'voucher(1190738)': 'SAVE20' },
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
{},
|
|
663
|
+
false,
|
|
664
|
+
);
|
|
665
|
+
expect(out[0].cardVarMapped).toEqual({ 'voucher(1190738)': 'SAVE20' });
|
|
666
|
+
// The typed value is a literal preview string, not another mustache tag — the raw
|
|
667
|
+
// {{voucher(...)}} token must survive so the editor can re-render an editable slot
|
|
668
|
+
// on reopen, and so the backend can resolve a real per-recipient code at send time.
|
|
669
|
+
expect(out[0].description).toBe('This is Bird with colorful feathers {{voucher(1190738)}}');
|
|
670
|
+
});
|
|
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
|
+
|
|
689
|
+
it('BUG REPRO: 2-card carousel — card 1 description resolves to card 0\'s value instead of its own tag pick', () => {
|
|
690
|
+
// Mirrors CreativesContainer/index.js ~1448-1461: rootRcsCardVarMapped is derived ONLY from
|
|
691
|
+
// cardContentFromSubmit[0] (the FIRST card's own cardVarMapped) and reused for every card.
|
|
692
|
+
// Card 0: title slot 1 -> user_id_b64, desc slot 2 -> voucher(1190738).
|
|
693
|
+
// Card 1: title slot 3 -> user_id_b64 (same semantic name as card 0), desc slot 4 -> first_name
|
|
694
|
+
// (the user's actual pick for card 1's own description slot, via "Add labels" while focused on card 1).
|
|
695
|
+
const card0 = {
|
|
696
|
+
title: 'This is a bird {{1}}',
|
|
697
|
+
description: 'This is Bird with colorful feathers {{2}}',
|
|
698
|
+
mediaType: 'IMAGE',
|
|
699
|
+
cardVarMapped: { 1: '{{user_id_b64}}', 2: '{{voucher(1190738)}}' },
|
|
700
|
+
};
|
|
701
|
+
const card1 = {
|
|
702
|
+
title: 'This is a park {{3}}',
|
|
703
|
+
description: 'This park has a cat and a bird {{4}}',
|
|
704
|
+
mediaType: 'IMAGE',
|
|
705
|
+
cardVarMapped: { 3: '{{user_id_b64}}', 4: '{{first_name}}' },
|
|
706
|
+
};
|
|
707
|
+
// rootRcsCardVarMapped, as CreativesContainer actually builds it: card 0's own map only.
|
|
708
|
+
const rootRcsCardVarMappedFromFirstCardOnly = { 1: '{{user_id_b64}}', 2: '{{voucher(1190738)}}' };
|
|
709
|
+
|
|
710
|
+
const out = mapRcsCardContentForConsumerWithResolvedTags(
|
|
711
|
+
[card0, card1],
|
|
712
|
+
rootRcsCardVarMappedFromFirstCardOnly,
|
|
713
|
+
true,
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
// Card 0 resolves correctly.
|
|
717
|
+
expect(out[0].title).toBe('This is a bird {{user_id_b64}}');
|
|
718
|
+
expect(out[0].description).toBe('This is Bird with colorful feathers {{voucher(1190738)}}');
|
|
719
|
+
|
|
720
|
+
// Card 1's title happens to resolve "correctly" only by coincidence (both root slot "1" and
|
|
721
|
+
// card 1's own slot "3" hold the same semantic value, user_id_b64).
|
|
722
|
+
expect(out[1].title).toBe('This is a park {{user_id_b64}}');
|
|
723
|
+
|
|
724
|
+
// Card 1's description SHOULD resolve using its own cardVarMapped slot 4 ({{first_name}}),
|
|
725
|
+
// not card 0's leaked slot 2 ({{voucher(1190738)}}). This is the reported bug: it currently
|
|
726
|
+
// resolves to card 0's value because resolveRcsCardPreviewStrings restarts slot numbering at
|
|
727
|
+
// 0 for every card (utils.js ~345-351) while the merged map still carries card 0's leftover
|
|
728
|
+
// global-numbered keys (via the root map bug in CreativesContainer).
|
|
729
|
+
expect(out[1].description).toBe('This park has a cat and a bird {{first_name}}');
|
|
730
|
+
});
|
|
651
731
|
});
|
|
652
732
|
|
|
653
733
|
describe('isRcsTextOnlyCardMediaType', () => {
|
|
@@ -196,6 +196,50 @@ export function coalesceCardVarMappedToTemplate(
|
|
|
196
196
|
templateVarTokens.forEach((token, slotIndexZeroBased) => {
|
|
197
197
|
const semanticVarName = getVarNameFromToken(token);
|
|
198
198
|
if (!semanticVarName) return;
|
|
199
|
+
|
|
200
|
+
if (RCS_NUMERIC_VAR_NAME_REGEX.test(semanticVarName)) {
|
|
201
|
+
// Carousel numeric placeholder ({{3}}, {{4}}, …): the slot key IS the token digit itself —
|
|
202
|
+
// numbers are allocated globally across the whole carousel (getNextCarouselVarToken), not
|
|
203
|
+
// re-numbered per card, so the position-based `slotIndexZeroBased + 1` key below would target
|
|
204
|
+
// the wrong slot for every card after the first (e.g. card 2's first token is digit "3", not "1").
|
|
205
|
+
const numericSlotKey = semanticVarName;
|
|
206
|
+
const trimmedSlotValue = String(lookupSourceMap[numericSlotKey] ?? '').trim();
|
|
207
|
+
coalescedMap[numericSlotKey] = trimmedSlotValue;
|
|
208
|
+
// The slot's value may itself be a mustache-wrapped tag reference (e.g. "3" -> "{{last_name}}")
|
|
209
|
+
// — mirror it onto the bare semantic key too so VarSegment editors keyed by tag name also
|
|
210
|
+
// prepopulate (same rule as syncCardVarMappedSemanticsFromSlots).
|
|
211
|
+
const mustacheInnerMatch = trimmedSlotValue.match(/^\{\{([^}]+)\}\}$/);
|
|
212
|
+
const innerSemanticName = mustacheInnerMatch?.[1]?.trim();
|
|
213
|
+
if (innerSemanticName && innerSemanticName !== numericSlotKey) {
|
|
214
|
+
const existingInnerValue = String(coalescedMap[innerSemanticName] ?? '').trim();
|
|
215
|
+
if (!existingInnerValue) {
|
|
216
|
+
coalescedMap[innerSemanticName] = trimmedSlotValue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// A numeric slot may already self-reference this exact tag (e.g. "4" -> "{{last_name}}") when
|
|
223
|
+
// the template's own numeric placeholder has since been resolved to this tag in the text (see
|
|
224
|
+
// mapRcsCardContentForConsumerWithResolvedTags). That numeric key is the source of truth — don't
|
|
225
|
+
// fall through to the position-based legacy lookup below, which targets an unrelated (or
|
|
226
|
+
// nonexistent) slot and would clobber the real value with an empty string.
|
|
227
|
+
const mustacheSelfReference = `{{${semanticVarName}}}`;
|
|
228
|
+
const existingNumericKeyForSemantic = Object.keys(lookupSourceMap).find((key) => (
|
|
229
|
+
RCS_NUMERIC_VAR_NAME_REGEX.test(key) && lookupSourceMap[key] === mustacheSelfReference
|
|
230
|
+
));
|
|
231
|
+
if (existingNumericKeyForSemantic) {
|
|
232
|
+
const trimmedSlotValue = String(lookupSourceMap[existingNumericKeyForSemantic] ?? '').trim();
|
|
233
|
+
coalescedMap[existingNumericKeyForSemantic] = trimmedSlotValue;
|
|
234
|
+
if (!seenSemanticVarNames.has(semanticVarName)) {
|
|
235
|
+
seenSemanticVarNames.add(semanticVarName);
|
|
236
|
+
coalescedMap[semanticVarName] = trimmedSlotValue;
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Legacy: template embeds the semantic token directly ({{user_name}}); older payloads stored
|
|
242
|
+
// the resolved value under a position-based numeric key ("1", "2", …) by order of appearance.
|
|
199
243
|
const numericSlotKey = String(slotIndexZeroBased + 1);
|
|
200
244
|
const isRepeatOfSemanticName = seenSemanticVarNames.has(semanticVarName);
|
|
201
245
|
const skipSharedSemanticLookup =
|
|
@@ -206,12 +250,6 @@ export function coalesceCardVarMappedToTemplate(
|
|
|
206
250
|
valueFromSource = lookupSourceMap[semanticVarName];
|
|
207
251
|
}
|
|
208
252
|
}
|
|
209
|
-
if (valueFromSource === undefined || valueFromSource === null) {
|
|
210
|
-
valueFromSource = lookupSourceMap[String(slotIndexZeroBased + 1)];
|
|
211
|
-
}
|
|
212
|
-
if (valueFromSource === undefined || valueFromSource === null) {
|
|
213
|
-
valueFromSource = lookupSourceMap[slotIndexZeroBased + 1];
|
|
214
|
-
}
|
|
215
253
|
const trimmedSlotValue = valueFromSource == null ? '' : String(valueFromSource).trim();
|
|
216
254
|
coalescedMap[numericSlotKey] = trimmedSlotValue;
|
|
217
255
|
if (!seenSemanticVarNames.has(semanticVarName)) {
|
|
@@ -287,6 +325,15 @@ export function isRcsTextOnlyCardMediaType(mediaType) {
|
|
|
287
325
|
* Mirrors `resolveTemplateWithMap` in the Rcs editor: title vars use global slots 0..n-1, then description.
|
|
288
326
|
* For text-only cards (`textOnlyCard`), ignore persisted `title` and resolve description from slot 0 — matches
|
|
289
327
|
* the editor where only the message body is shown.
|
|
328
|
+
*
|
|
329
|
+
* @param {boolean} [preserveLiteralValueTokens=false] When true, a slot value that is NOT itself a
|
|
330
|
+
* `{{...}}`-wrapped tag (e.g. a literal preview string like "SAVE20" typed for a voucher/offer tag)
|
|
331
|
+
*
|
|
332
|
+
* @param {number} [cardSlotOffset=0] Starting global slot index for this card's title. Carousel cards
|
|
333
|
+
* beyond index 0 must pass the running total of every prior card's var count — numeric `{{N}}` tokens
|
|
334
|
+
* are numbered globally across the whole carousel, not per card (see `getNextCarouselVarToken` in
|
|
335
|
+
* carouselUtils.js). Without this, every card resolves as if it were card 0, colliding with an earlier
|
|
336
|
+
* card's numeric slot key in the merged map.
|
|
290
337
|
*/
|
|
291
338
|
export function resolveRcsCardPreviewStrings(
|
|
292
339
|
title,
|
|
@@ -321,6 +368,15 @@ export function resolveRcsCardPreviewStrings(
|
|
|
321
368
|
omitSemantic,
|
|
322
369
|
);
|
|
323
370
|
if (slotValue == null || String(slotValue).trim() === '') return elem;
|
|
371
|
+
const trimmedSlotValue = String(slotValue).trim();
|
|
372
|
+
const isPlainNumericSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(key);
|
|
373
|
+
if (
|
|
374
|
+
preserveLiteralValueTokens
|
|
375
|
+
&& !isPlainNumericSlot
|
|
376
|
+
&& !RCS_MUSTACHE_WRAPPED_REGEX.test(trimmedSlotValue)
|
|
377
|
+
) {
|
|
378
|
+
return elem;
|
|
379
|
+
}
|
|
324
380
|
return String(slotValue);
|
|
325
381
|
}
|
|
326
382
|
return elem;
|
|
@@ -373,6 +429,7 @@ export function mapRcsCardContentForConsumerWithResolvedTags(
|
|
|
373
429
|
textOnly,
|
|
374
430
|
);
|
|
375
431
|
const { cardVarMapped: _drop, ...cardRest } = card;
|
|
432
|
+
|
|
376
433
|
return {
|
|
377
434
|
...cardRest,
|
|
378
435
|
title: rcsTitle,
|