@capillarytech/creatives-library 9.0.34 → 9.0.35-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/v2Components/CommonTestAndPreview/index.js +12 -6
- package/v2Components/CommonTestAndPreview/tests/index.test.js +78 -0
- package/v2Components/CommonTestAndPreview/utils.js +34 -0
- package/v2Components/SmsFallback/index.js +6 -0
- package/v2Containers/CreativesContainer/index.js +15 -6
- package/v2Containers/CreativesContainer/tests/index.test.js +53 -0
- package/v2Containers/Rcs/carouselUtils.js +16 -17
- package/v2Containers/Rcs/components/CarouselCard.js +2 -2
- package/v2Containers/Rcs/constants.js +1 -1
- package/v2Containers/Rcs/index.js +200 -101
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +47 -6
- package/v2Containers/Rcs/tests/carouselUtils.test.js +22 -26
- package/v2Containers/Rcs/tests/index.test.js +58 -20
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +71 -0
- package/v2Containers/Rcs/tests/utils.test.js +99 -0
- package/v2Containers/Rcs/utils.js +34 -4
- package/v2Containers/SmsTrai/Edit/index.js +35 -24
|
@@ -554,15 +554,13 @@ describe('buildCarouselCardsForPreview', () => {
|
|
|
554
554
|
expect(
|
|
555
555
|
buildCarouselCardsForPreview(undefined, {
|
|
556
556
|
isFullMode: false,
|
|
557
|
-
|
|
558
|
-
resolveTemplateWithMap: jest.fn(),
|
|
557
|
+
resolveCarouselTemplateWithMap: jest.fn(),
|
|
559
558
|
}),
|
|
560
559
|
).toEqual([]);
|
|
561
560
|
expect(
|
|
562
561
|
buildCarouselCardsForPreview([], {
|
|
563
562
|
isFullMode: false,
|
|
564
|
-
|
|
565
|
-
resolveTemplateWithMap: jest.fn(),
|
|
563
|
+
resolveCarouselTemplateWithMap: jest.fn(),
|
|
566
564
|
}),
|
|
567
565
|
).toEqual([]);
|
|
568
566
|
});
|
|
@@ -571,47 +569,45 @@ describe('buildCarouselCardsForPreview', () => {
|
|
|
571
569
|
expect(
|
|
572
570
|
buildCarouselCardsForPreview(null, {
|
|
573
571
|
isFullMode: false,
|
|
574
|
-
|
|
575
|
-
resolveTemplateWithMap: jest.fn(),
|
|
572
|
+
resolveCarouselTemplateWithMap: jest.fn(),
|
|
576
573
|
}),
|
|
577
574
|
).toEqual([]);
|
|
578
575
|
});
|
|
579
576
|
|
|
580
|
-
it('passes title/description through unresolved in full mode without calling
|
|
581
|
-
const
|
|
577
|
+
it('passes title/description through unresolved in full mode without calling resolveCarouselTemplateWithMap', () => {
|
|
578
|
+
const resolveCarouselTemplateWithMap = jest.fn();
|
|
582
579
|
const out = buildCarouselCardsForPreview(
|
|
583
580
|
[{ title: 'Hi {{a}}', description: 'Bye {{b}}' }],
|
|
584
|
-
{ isFullMode: true,
|
|
581
|
+
{ isFullMode: true, resolveCarouselTemplateWithMap },
|
|
585
582
|
);
|
|
586
|
-
expect(
|
|
583
|
+
expect(resolveCarouselTemplateWithMap).not.toHaveBeenCalled();
|
|
587
584
|
expect(out[0].title).toBe('Hi {{a}}');
|
|
588
585
|
expect(out[0].bodyText).toBe('Bye {{b}}');
|
|
589
586
|
});
|
|
590
587
|
|
|
591
|
-
it('resolves title/description through
|
|
592
|
-
const
|
|
588
|
+
it('resolves title/description through resolveCarouselTemplateWithMap, scoped per card index', () => {
|
|
589
|
+
const resolveCarouselTemplateWithMap = jest.fn((str, cardIndex) => `R(${str}|${cardIndex})`);
|
|
593
590
|
const out = buildCarouselCardsForPreview(
|
|
594
591
|
[
|
|
595
592
|
{ title: 'Hi {{a}}', description: 'Bye {{b}} {{c}}' },
|
|
596
593
|
{ title: 'Solo {{d}}', description: 'none' },
|
|
597
594
|
],
|
|
598
|
-
{ isFullMode: false,
|
|
595
|
+
{ isFullMode: false, resolveCarouselTemplateWithMap },
|
|
599
596
|
);
|
|
600
|
-
expect(
|
|
601
|
-
expect(
|
|
602
|
-
expect(
|
|
603
|
-
expect(
|
|
597
|
+
expect(resolveCarouselTemplateWithMap).toHaveBeenNthCalledWith(1, 'Hi {{a}}', 0);
|
|
598
|
+
expect(resolveCarouselTemplateWithMap).toHaveBeenNthCalledWith(2, 'Bye {{b}} {{c}}', 0);
|
|
599
|
+
expect(resolveCarouselTemplateWithMap).toHaveBeenNthCalledWith(3, 'Solo {{d}}', 1);
|
|
600
|
+
expect(resolveCarouselTemplateWithMap).toHaveBeenNthCalledWith(4, 'none', 1);
|
|
604
601
|
expect(out[0].title).toBe('R(Hi {{a}}|0)');
|
|
605
|
-
expect(out[0].bodyText).toBe('R(Bye {{b}} {{c}}|
|
|
606
|
-
expect(out[1].title).toBe('R(Solo {{d}}|
|
|
607
|
-
expect(out[1].bodyText).toBe('R(none|
|
|
602
|
+
expect(out[0].bodyText).toBe('R(Bye {{b}} {{c}}|0)');
|
|
603
|
+
expect(out[1].title).toBe('R(Solo {{d}}|1)');
|
|
604
|
+
expect(out[1].bodyText).toBe('R(none|1)');
|
|
608
605
|
});
|
|
609
606
|
|
|
610
607
|
it('defaults missing card fields, including a default {} for a missing array item', () => {
|
|
611
608
|
const out = buildCarouselCardsForPreview([undefined], {
|
|
612
609
|
isFullMode: true,
|
|
613
|
-
|
|
614
|
-
resolveTemplateWithMap: jest.fn(),
|
|
610
|
+
resolveCarouselTemplateWithMap: jest.fn(),
|
|
615
611
|
});
|
|
616
612
|
expect(out[0]).toEqual({
|
|
617
613
|
mediaType: '',
|
|
@@ -635,7 +631,7 @@ describe('buildCarouselCardsForPreview', () => {
|
|
|
635
631
|
description: '',
|
|
636
632
|
},
|
|
637
633
|
],
|
|
638
|
-
{ isFullMode: true,
|
|
634
|
+
{ isFullMode: true, resolveCarouselTemplateWithMap: jest.fn() },
|
|
639
635
|
);
|
|
640
636
|
expect(out[0].mediaType).toBe('image');
|
|
641
637
|
expect(out[0].imageSrc).toBe('img.png');
|
|
@@ -651,7 +647,7 @@ describe('buildCarouselCardsForPreview', () => {
|
|
|
651
647
|
videoAsset: { videoSrc: 'v.mp4', videoThumbnail: 'other-thumb.png' },
|
|
652
648
|
},
|
|
653
649
|
],
|
|
654
|
-
{ isFullMode: true,
|
|
650
|
+
{ isFullMode: true, resolveCarouselTemplateWithMap: jest.fn() },
|
|
655
651
|
);
|
|
656
652
|
expect(out[0].videoPreviewImg).toBe('thumb.png');
|
|
657
653
|
expect(out[0].videoSrc).toBe('v.mp4');
|
|
@@ -665,7 +661,7 @@ describe('buildCarouselCardsForPreview', () => {
|
|
|
665
661
|
videoAsset: { videoSrc: 'v.mp4', videoThumbnail: 'other-thumb.png' },
|
|
666
662
|
},
|
|
667
663
|
],
|
|
668
|
-
{ isFullMode: true,
|
|
664
|
+
{ isFullMode: true, resolveCarouselTemplateWithMap: jest.fn() },
|
|
669
665
|
);
|
|
670
666
|
expect(out[0].videoPreviewImg).toBe('other-thumb.png');
|
|
671
667
|
});
|
|
@@ -673,7 +669,7 @@ describe('buildCarouselCardsForPreview', () => {
|
|
|
673
669
|
it('defaults videoPreviewImg and videoSrc to empty string when nothing is set', () => {
|
|
674
670
|
const out = buildCarouselCardsForPreview(
|
|
675
671
|
[{ mediaType: RCS_MEDIA_TYPES.IMAGE }],
|
|
676
|
-
{ isFullMode: true,
|
|
672
|
+
{ isFullMode: true, resolveCarouselTemplateWithMap: jest.fn() },
|
|
677
673
|
);
|
|
678
674
|
expect(out[0].videoPreviewImg).toBe('');
|
|
679
675
|
expect(out[0].videoSrc).toBe('');
|
|
@@ -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 each carousel card\'s same-named variable independently scoped in both inline preview and Test&Preview slidebox', async () => {
|
|
479
479
|
const templateData = {
|
|
480
480
|
name: 'CarouselVarsTemplate',
|
|
481
481
|
versions: {
|
|
@@ -553,37 +553,64 @@ 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
|
+
const card0NameAreas = nameVarAreaForCard(0);
|
|
568
|
+
expect(card0NameAreas.length).toBeGreaterThanOrEqual(1);
|
|
563
569
|
|
|
564
570
|
act(() => {
|
|
565
|
-
const slotId =
|
|
566
|
-
|
|
571
|
+
const slotId = card0NameAreas.at(0).prop('id') || card0NameAreas.at(0).key();
|
|
572
|
+
card0NameAreas.at(0).props().onChange({ target: { id: slotId, value: 'madhavi' } });
|
|
567
573
|
});
|
|
568
574
|
wrapper.update();
|
|
569
575
|
|
|
570
|
-
// Inline preview should
|
|
576
|
+
// Inline preview should resolve card 0 only — card 1's own `{{name}}` slot is untouched.
|
|
571
577
|
const updatedInlinePreview = wrapper.find('UnifiedPreview').filterWhere((n) => n.prop('content')?.carouselData).at(0);
|
|
572
578
|
const inlineCards = updatedInlinePreview.props().content.carouselData;
|
|
573
579
|
expect(inlineCards[0].title).toBe('Hi madhavi');
|
|
574
580
|
expect(inlineCards[0].bodyText).toBe('Welcome madhavi');
|
|
575
|
-
expect(inlineCards[1].title).toBe('Offer for
|
|
576
|
-
expect(inlineCards[1].bodyText).toBe('Dear
|
|
581
|
+
expect(inlineCards[1].title).toBe('Offer for {{name}}');
|
|
582
|
+
expect(inlineCards[1].bodyText).toBe('Dear {{name}}');
|
|
577
583
|
|
|
578
|
-
// Slidebox content should
|
|
584
|
+
// Slidebox content should mirror the same per-card scoping (same underlying mapping)
|
|
579
585
|
const slidebox = wrapper.find('TestAndPreviewSlidebox').at(0);
|
|
580
586
|
expect(slidebox.exists()).toBe(true);
|
|
581
587
|
const slideboxContent = slidebox.props().content;
|
|
582
588
|
expect(slideboxContent?.carouselData?.length).toBeGreaterThanOrEqual(2);
|
|
583
589
|
expect(slideboxContent.carouselData[0].title).toBe('Hi madhavi');
|
|
584
590
|
expect(slideboxContent.carouselData[0].bodyText).toBe('Welcome madhavi');
|
|
585
|
-
expect(slideboxContent.carouselData[1].title).toBe('Offer for
|
|
586
|
-
expect(slideboxContent.carouselData[1].bodyText).toBe('Dear
|
|
591
|
+
expect(slideboxContent.carouselData[1].title).toBe('Offer for {{name}}');
|
|
592
|
+
expect(slideboxContent.carouselData[1].bodyText).toBe('Dear {{name}}');
|
|
593
|
+
|
|
594
|
+
// Setting card 1's own `{{name}}` slot independently resolves card 1 without disturbing card 0.
|
|
595
|
+
// Inactive tab panes aren't mounted until switched to, so activate card 1's tab first.
|
|
596
|
+
act(() => {
|
|
597
|
+
wrapper.find('CapTab').prop('onChange')('1');
|
|
598
|
+
});
|
|
599
|
+
wrapper.update();
|
|
600
|
+
const card1NameAreas = nameVarAreaForCard(1);
|
|
601
|
+
expect(card1NameAreas.length).toBeGreaterThanOrEqual(1);
|
|
602
|
+
act(() => {
|
|
603
|
+
const slotId = card1NameAreas.at(0).prop('id') || card1NameAreas.at(0).key();
|
|
604
|
+
card1NameAreas.at(0).props().onChange({ target: { id: slotId, value: 'raghav' } });
|
|
605
|
+
});
|
|
606
|
+
wrapper.update();
|
|
607
|
+
|
|
608
|
+
const finalInlinePreview = wrapper.find('UnifiedPreview').filterWhere((n) => n.prop('content')?.carouselData).at(0);
|
|
609
|
+
const finalInlineCards = finalInlinePreview.props().content.carouselData;
|
|
610
|
+
expect(finalInlineCards[0].title).toBe('Hi madhavi');
|
|
611
|
+
expect(finalInlineCards[0].bodyText).toBe('Welcome madhavi');
|
|
612
|
+
expect(finalInlineCards[1].title).toBe('Offer for raghav');
|
|
613
|
+
expect(finalInlineCards[1].bodyText).toBe('Dear raghav');
|
|
587
614
|
});
|
|
588
615
|
});
|
|
589
616
|
|
|
@@ -1161,7 +1188,12 @@ describe('RCS createPayload', () => {
|
|
|
1161
1188
|
cardContent: [
|
|
1162
1189
|
{
|
|
1163
1190
|
title: 'Hello {{user_name}} and {{user_name}}',
|
|
1164
|
-
|
|
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}}',
|
|
1165
1197
|
mediaType: 'IMAGE',
|
|
1166
1198
|
media: { mediaUrl: '', thumbnailUrl: '' },
|
|
1167
1199
|
cardVarMapped: {},
|
|
@@ -1227,12 +1259,18 @@ describe('RCS createPayload', () => {
|
|
|
1227
1259
|
});
|
|
1228
1260
|
wrapper.update();
|
|
1229
1261
|
|
|
1230
|
-
|
|
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) => {
|
|
1231
1269
|
const id = n.prop('id') || '';
|
|
1232
1270
|
return id.includes('{{user_name}}_');
|
|
1233
1271
|
});
|
|
1234
|
-
expect(
|
|
1235
|
-
expect(
|
|
1272
|
+
expect(renamedVarArea.at(0).prop('value')).toBe('{{first_name}}');
|
|
1273
|
+
expect(duplicateVarArea.at(0).prop('value')).toBe('{{first_name}}');
|
|
1236
1274
|
});
|
|
1237
1275
|
|
|
1238
1276
|
it('should keep two tags + freetext inside the variable textarea in non-full mode edit (not merged into static text)', () => {
|
|
@@ -364,6 +364,14 @@ 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
|
+
|
|
367
375
|
it('returns {} for an empty object', () => {
|
|
368
376
|
expect(pickRcsCardVarMappedEntries({})).toEqual({});
|
|
369
377
|
});
|
|
@@ -439,6 +447,7 @@ describe('rcsLibraryHydrationUtils', () => {
|
|
|
439
447
|
template: 'Hello',
|
|
440
448
|
templateName: 'T1',
|
|
441
449
|
registeredSenderIds: ['S1'],
|
|
450
|
+
templateVariableMapping: { '{#a#}_0': { data: 'x', count: 1 } },
|
|
442
451
|
},
|
|
443
452
|
},
|
|
444
453
|
});
|
|
@@ -497,6 +506,7 @@ describe('rcsLibraryHydrationUtils', () => {
|
|
|
497
506
|
expect(out.smsFallBackContent.templateConfigs).toEqual({
|
|
498
507
|
templateName: 'ConfigName',
|
|
499
508
|
registeredSenderIds: ['H1'],
|
|
509
|
+
templateVariableMapping: { a: { data: '1', count: 1 } },
|
|
500
510
|
});
|
|
501
511
|
expect(out.smsFallBackContent.rcsSmsFallbackVarMapped).toEqual({ a: '1' });
|
|
502
512
|
});
|
|
@@ -537,6 +547,25 @@ describe('rcsLibraryHydrationUtils', () => {
|
|
|
537
547
|
});
|
|
538
548
|
});
|
|
539
549
|
|
|
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
|
+
|
|
540
569
|
it('non-full mode: DLT campaign but no derivable templateConfigs omits templateConfigs', () => {
|
|
541
570
|
enableTraiDlt();
|
|
542
571
|
const out = buildSmsFallBackContentForPayload({
|
|
@@ -744,6 +773,48 @@ describe('rcsLibraryHydrationUtils', () => {
|
|
|
744
773
|
).toBe('Direct Template');
|
|
745
774
|
});
|
|
746
775
|
|
|
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
|
+
|
|
747
818
|
it('joins an updated-sms-editor array into the fallback message', () => {
|
|
748
819
|
const out = resolveSmsFallbackHydrationFromDetails({
|
|
749
820
|
smsFallBackContent: {
|
|
@@ -648,6 +648,69 @@ 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('BUG REPRO: 2-card carousel — card 1 description resolves to card 0\'s value instead of its own tag pick', () => {
|
|
673
|
+
// Mirrors CreativesContainer/index.js ~1448-1461: rootRcsCardVarMapped is derived ONLY from
|
|
674
|
+
// cardContentFromSubmit[0] (the FIRST card's own cardVarMapped) and reused for every card.
|
|
675
|
+
// Card 0: title slot 1 -> user_id_b64, desc slot 2 -> voucher(1190738).
|
|
676
|
+
// Card 1: title slot 3 -> user_id_b64 (same semantic name as card 0), desc slot 4 -> first_name
|
|
677
|
+
// (the user's actual pick for card 1's own description slot, via "Add labels" while focused on card 1).
|
|
678
|
+
const card0 = {
|
|
679
|
+
title: 'This is a bird {{1}}',
|
|
680
|
+
description: 'This is Bird with colorful feathers {{2}}',
|
|
681
|
+
mediaType: 'IMAGE',
|
|
682
|
+
cardVarMapped: { 1: '{{user_id_b64}}', 2: '{{voucher(1190738)}}' },
|
|
683
|
+
};
|
|
684
|
+
const card1 = {
|
|
685
|
+
title: 'This is a park {{3}}',
|
|
686
|
+
description: 'This park has a cat and a bird {{4}}',
|
|
687
|
+
mediaType: 'IMAGE',
|
|
688
|
+
cardVarMapped: { 3: '{{user_id_b64}}', 4: '{{first_name}}' },
|
|
689
|
+
};
|
|
690
|
+
// rootRcsCardVarMapped, as CreativesContainer actually builds it: card 0's own map only.
|
|
691
|
+
const rootRcsCardVarMappedFromFirstCardOnly = { 1: '{{user_id_b64}}', 2: '{{voucher(1190738)}}' };
|
|
692
|
+
|
|
693
|
+
const out = mapRcsCardContentForConsumerWithResolvedTags(
|
|
694
|
+
[card0, card1],
|
|
695
|
+
rootRcsCardVarMappedFromFirstCardOnly,
|
|
696
|
+
true,
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
// Card 0 resolves correctly.
|
|
700
|
+
expect(out[0].title).toBe('This is a bird {{user_id_b64}}');
|
|
701
|
+
expect(out[0].description).toBe('This is Bird with colorful feathers {{voucher(1190738)}}');
|
|
702
|
+
|
|
703
|
+
// Card 1's title happens to resolve "correctly" only by coincidence (both root slot "1" and
|
|
704
|
+
// card 1's own slot "3" hold the same semantic value, user_id_b64).
|
|
705
|
+
expect(out[1].title).toBe('This is a park {{user_id_b64}}');
|
|
706
|
+
|
|
707
|
+
// Card 1's description SHOULD resolve using its own cardVarMapped slot 4 ({{first_name}}),
|
|
708
|
+
// not card 0's leaked slot 2 ({{voucher(1190738)}}). This is the reported bug: it currently
|
|
709
|
+
// resolves to card 0's value because resolveRcsCardPreviewStrings restarts slot numbering at
|
|
710
|
+
// 0 for every card (utils.js ~345-351) while the merged map still carries card 0's leftover
|
|
711
|
+
// global-numbered keys (via the root map bug in CreativesContainer).
|
|
712
|
+
expect(out[1].description).toBe('This park has a cat and a bird {{first_name}}');
|
|
713
|
+
});
|
|
651
714
|
});
|
|
652
715
|
|
|
653
716
|
describe('isRcsTextOnlyCardMediaType', () => {
|
|
@@ -701,6 +764,42 @@ describe('RCS utils', () => {
|
|
|
701
764
|
expect(rcsTitle).toBe('');
|
|
702
765
|
expect(rcsDesc).toBe('Visit Store for {{loyalty_points}} discount');
|
|
703
766
|
});
|
|
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
|
+
});
|
|
704
803
|
});
|
|
705
804
|
|
|
706
805
|
describe('sanitizeCardVarMappedValue', () => {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
RCS_NUMERIC_VAR_NAME_REGEX,
|
|
7
7
|
REGEX_SPECIAL_CHARS_ESCAPE_PATTERN,
|
|
8
8
|
RCS_STRIP_MUSTACHE_DELIMITERS_REGEX,
|
|
9
|
+
RCS_MUSTACHE_WRAPPED_REGEX,
|
|
9
10
|
} from './constants';
|
|
10
11
|
import './index.scss';
|
|
11
12
|
// import { formatMessage } from '../../../utils/intl';
|
|
@@ -287,6 +288,20 @@ export function isRcsTextOnlyCardMediaType(mediaType) {
|
|
|
287
288
|
* Mirrors `resolveTemplateWithMap` in the Rcs editor: title vars use global slots 0..n-1, then description.
|
|
288
289
|
* For text-only cards (`textOnlyCard`), ignore persisted `title` and resolve description from slot 0 — matches
|
|
289
290
|
* the editor where only the message body is shown.
|
|
291
|
+
*
|
|
292
|
+
* @param {boolean} [preserveLiteralValueTokens=false] When true, a slot value that is NOT itself a
|
|
293
|
+
* `{{...}}`-wrapped tag (e.g. a literal preview string like "SAVE20" typed for a voucher/offer tag)
|
|
294
|
+
* is left as the raw token instead of being baked into the returned string. Personalization-style
|
|
295
|
+
* values (themselves another mustache tag, e.g. `{{loyalty_points}}`) still resolve either way — those
|
|
296
|
+
* are internal-alias renames, not literal content. Pass true for payloads that get persisted/replayed
|
|
297
|
+
* (see `mapRcsCardContentForConsumerWithResolvedTags`) so a voucher's saved token survives for the
|
|
298
|
+
* editor to re-render an editable slot from, and for the backend to resolve per-recipient at send time.
|
|
299
|
+
*
|
|
300
|
+
* @param {number} [cardSlotOffset=0] Starting global slot index for this card's title. Carousel cards
|
|
301
|
+
* beyond index 0 must pass the running total of every prior card's var count — numeric `{{N}}` tokens
|
|
302
|
+
* are numbered globally across the whole carousel, not per card (see `getNextCarouselVarToken` in
|
|
303
|
+
* carouselUtils.js). Without this, every card resolves as if it were card 0, colliding with an earlier
|
|
304
|
+
* card's numeric slot key in the merged map.
|
|
290
305
|
*/
|
|
291
306
|
export function resolveRcsCardPreviewStrings(
|
|
292
307
|
title,
|
|
@@ -294,6 +309,8 @@ export function resolveRcsCardPreviewStrings(
|
|
|
294
309
|
cardVarMapped,
|
|
295
310
|
isLibraryMode = false,
|
|
296
311
|
textOnlyCard = false,
|
|
312
|
+
preserveLiteralValueTokens = false,
|
|
313
|
+
cardSlotOffset = 0,
|
|
297
314
|
) {
|
|
298
315
|
const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
|
|
299
316
|
const getVarNameFromToken = (token = '') =>
|
|
@@ -321,6 +338,10 @@ export function resolveRcsCardPreviewStrings(
|
|
|
321
338
|
omitSemantic,
|
|
322
339
|
);
|
|
323
340
|
if (slotValue == null || String(slotValue).trim() === '') return elem;
|
|
341
|
+
const trimmedSlotValue = String(slotValue).trim();
|
|
342
|
+
if (preserveLiteralValueTokens && !RCS_MUSTACHE_WRAPPED_REGEX.test(trimmedSlotValue)) {
|
|
343
|
+
return elem;
|
|
344
|
+
}
|
|
324
345
|
return String(slotValue);
|
|
325
346
|
}
|
|
326
347
|
return elem;
|
|
@@ -333,8 +354,8 @@ export function resolveRcsCardPreviewStrings(
|
|
|
333
354
|
? 0
|
|
334
355
|
: (effectiveTitle.match(rcsVarRegex) || []).length;
|
|
335
356
|
return {
|
|
336
|
-
rcsTitle: textOnlyCard ? '' : resolveTemplateWithMap(effectiveTitle,
|
|
337
|
-
rcsDesc: resolveTemplateWithMap(String(description || ''), titleVarCount),
|
|
357
|
+
rcsTitle: textOnlyCard ? '' : resolveTemplateWithMap(effectiveTitle, cardSlotOffset),
|
|
358
|
+
rcsDesc: resolveTemplateWithMap(String(description || ''), cardSlotOffset + titleVarCount),
|
|
338
359
|
};
|
|
339
360
|
}
|
|
340
361
|
|
|
@@ -355,6 +376,7 @@ export function mapRcsCardContentForConsumerWithResolvedTags(
|
|
|
355
376
|
: {};
|
|
356
377
|
const list = Array.isArray(cardContentArray) ? cardContentArray : [];
|
|
357
378
|
const isLibraryMode = isFullMode !== true;
|
|
379
|
+
let carouselSlotOffset = 0;
|
|
358
380
|
return list.map((card) => {
|
|
359
381
|
if (!card || typeof card !== 'object') return card;
|
|
360
382
|
const nested =
|
|
@@ -365,13 +387,21 @@ export function mapRcsCardContentForConsumerWithResolvedTags(
|
|
|
365
387
|
const nestedClean = pickRcsCardVarMappedEntries(nested);
|
|
366
388
|
const merged = { ...rootClean, ...nestedClean };
|
|
367
389
|
const textOnly = isRcsTextOnlyCardMediaType(card.mediaType);
|
|
390
|
+
const cardTitle = card.title ?? '';
|
|
391
|
+
const cardDescription = card.description ?? '';
|
|
368
392
|
const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
|
|
369
|
-
|
|
370
|
-
|
|
393
|
+
cardTitle,
|
|
394
|
+
cardDescription,
|
|
371
395
|
merged,
|
|
372
396
|
isLibraryMode,
|
|
373
397
|
textOnly,
|
|
398
|
+
true,
|
|
399
|
+
carouselSlotOffset,
|
|
374
400
|
);
|
|
401
|
+
const effectiveTitleForCount = textOnly ? '' : String(cardTitle);
|
|
402
|
+
const titleVarCount = (effectiveTitleForCount.match(rcsVarRegex) || []).length;
|
|
403
|
+
const descVarCount = (String(cardDescription).match(rcsVarRegex) || []).length;
|
|
404
|
+
carouselSlotOffset += titleVarCount + descVarCount;
|
|
375
405
|
const { cardVarMapped: _drop, ...cardRest } = card;
|
|
376
406
|
return {
|
|
377
407
|
...cardRest,
|