@capillarytech/creatives-library 9.0.15-alpha.3 → 9.0.16-beta.0

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 (146) hide show
  1. package/AppRoot.js +80 -0
  2. package/app.js +5 -3
  3. package/constants/unified.js +0 -29
  4. package/entry.js +67 -1
  5. package/global-styles.js +1 -1
  6. package/mfe-exposed-components.js +2 -4
  7. package/package.json +2 -2
  8. package/services/api.js +1 -1
  9. package/services/tests/api.test.js +20 -35
  10. package/styles/containers/layout/_layoutPage.scss +8 -6
  11. package/utils/commonUtils.js +1 -19
  12. package/utils/getDataLayer.js +15 -0
  13. package/utils/gtmTrackers/gtmEvents/creativeDetails.js +2 -1
  14. package/utils/mfeDetect.js +1 -0
  15. package/v2Components/CapActionButton/constants.js +1 -8
  16. package/v2Components/CapActionButton/index.js +144 -202
  17. package/v2Components/CapActionButton/index.scss +14 -232
  18. package/v2Components/CapActionButton/messages.js +3 -19
  19. package/v2Components/CapActionButton/tests/index.test.js +18 -42
  20. package/v2Components/CapImageUpload/index.js +2 -2
  21. package/v2Components/CapTagList/index.js +0 -10
  22. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +48 -70
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +2 -8
  24. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +21 -214
  25. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +0 -16
  26. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +9 -83
  27. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +0 -30
  28. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +11 -60
  29. package/v2Components/CommonTestAndPreview/SendTestMessage.js +5 -10
  30. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +22 -202
  31. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +80 -381
  32. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +8 -155
  33. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +0 -11
  34. package/v2Components/CommonTestAndPreview/constants.js +2 -38
  35. package/v2Components/CommonTestAndPreview/index.js +222 -761
  36. package/v2Components/CommonTestAndPreview/messages.js +3 -45
  37. package/v2Components/CommonTestAndPreview/sagas.js +6 -25
  38. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +0 -1
  39. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +267 -284
  40. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +65 -231
  41. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +5 -118
  42. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +0 -341
  43. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +1 -8
  44. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +13 -34
  45. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +283 -281
  46. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +1 -199
  47. package/v2Components/CommonTestAndPreview/tests/index.test.js +4 -133
  48. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +24 -31
  49. package/v2Components/NavigationBar/index.js +9 -7
  50. package/v2Components/NavigationBar/mfeModuleHeader.config.js +16 -0
  51. package/v2Components/Pagination/_pagination.scss +2 -0
  52. package/v2Components/TemplatePreview/_templatePreview.scss +25 -45
  53. package/v2Components/TemplatePreview/index.js +32 -147
  54. package/v2Components/TemplatePreview/tests/index.test.js +0 -142
  55. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  56. package/v2Components/TestAndPreviewSlidebox/index.js +1 -13
  57. package/v2Components/TestAndPreviewSlidebox/sagas.js +4 -11
  58. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +1 -3
  59. package/v2Containers/Cap/constants.js +1 -0
  60. package/v2Containers/Cap/index.js +47 -21
  61. package/v2Containers/Cap/sagas.js +19 -0
  62. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +1 -4
  63. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +0 -4
  64. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  65. package/v2Containers/CreativesContainer/SlideBoxContent.js +7 -37
  66. package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -14
  67. package/v2Containers/CreativesContainer/SlideBoxHeader.js +5 -36
  68. package/v2Containers/CreativesContainer/constants.js +0 -11
  69. package/v2Containers/CreativesContainer/index.js +104 -323
  70. package/v2Containers/CreativesContainer/index.scss +1 -83
  71. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +34 -79
  72. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +16 -79
  73. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +0 -8
  74. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +90 -333
  75. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +15 -20
  76. package/v2Containers/CreativesContainer/tests/index.test.js +9 -71
  77. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  78. package/v2Containers/Rcs/constants.js +11 -131
  79. package/v2Containers/Rcs/index.js +829 -2682
  80. package/v2Containers/Rcs/index.scss +8 -443
  81. package/v2Containers/Rcs/messages.js +6 -45
  82. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +39409 -74176
  83. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +74 -10
  84. package/v2Containers/Rcs/tests/index.test.js +40 -151
  85. package/v2Containers/Rcs/tests/mockData.js +0 -38
  86. package/v2Containers/Rcs/tests/utils.test.js +30 -643
  87. package/v2Containers/Rcs/utils.js +12 -466
  88. package/v2Containers/Sms/Create/index.js +48 -115
  89. package/v2Containers/SmsTrai/Create/index.js +4 -9
  90. package/v2Containers/SmsTrai/Edit/constants.js +0 -2
  91. package/v2Containers/SmsTrai/Edit/index.js +169 -678
  92. package/v2Containers/SmsTrai/Edit/messages.js +4 -14
  93. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +2854 -5455
  94. package/v2Containers/SmsWrapper/index.js +8 -37
  95. package/v2Containers/TagList/index.js +0 -6
  96. package/v2Containers/Templates/_templates.scss +11 -196
  97. package/v2Containers/Templates/actions.js +0 -11
  98. package/v2Containers/Templates/constants.js +0 -2
  99. package/v2Containers/Templates/index.js +60 -131
  100. package/v2Containers/Templates/sagas.js +13 -57
  101. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1015 -1060
  102. package/v2Containers/Templates/tests/sagas.test.js +16 -199
  103. package/v2Containers/TemplatesV2/TemplatesV2.style.js +10 -75
  104. package/v2Containers/TemplatesV2/index.js +43 -86
  105. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  106. package/v2Containers/Whatsapp/index.js +25 -4
  107. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -578
  108. package/utils/rcsPayloadUtils.js +0 -92
  109. package/utils/templateVarUtils.js +0 -201
  110. package/utils/tests/rcsPayloadUtils.test.js +0 -226
  111. package/utils/tests/templateVarUtils.test.js +0 -204
  112. package/v2Components/CommonTestAndPreview/previewApiUtils.js +0 -59
  113. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +0 -67
  114. package/v2Components/CommonTestAndPreview/utils.js +0 -84
  115. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +0 -91
  116. package/v2Components/SmsFallback/constants.js +0 -94
  117. package/v2Components/SmsFallback/index.js +0 -958
  118. package/v2Components/SmsFallback/index.scss +0 -266
  119. package/v2Components/SmsFallback/messages.js +0 -78
  120. package/v2Components/SmsFallback/smsFallbackUtils.js +0 -120
  121. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +0 -50
  122. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +0 -147
  123. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +0 -304
  124. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +0 -208
  125. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +0 -309
  126. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +0 -422
  127. package/v2Components/SmsFallback/useLocalTemplateList.js +0 -92
  128. package/v2Components/TemplatePreview/constants.js +0 -2
  129. package/v2Components/VarSegmentMessageEditor/constants.js +0 -2
  130. package/v2Components/VarSegmentMessageEditor/index.js +0 -125
  131. package/v2Components/VarSegmentMessageEditor/index.scss +0 -46
  132. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +0 -17
  133. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +0 -79
  134. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +0 -90
  135. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +0 -258
  136. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +0 -125
  137. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +0 -227
  138. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +0 -318
  139. package/v2Containers/Sms/smsFormDataHelpers.js +0 -67
  140. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +0 -253
  141. package/v2Containers/SmsTrai/Edit/index.scss +0 -126
  142. package/v2Containers/Templates/TemplatesActionBar.js +0 -101
  143. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +0 -120
  144. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +0 -180
  145. package/v2Containers/Templates/utils/smsTemplatesListApi.js +0 -79
  146. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +0 -131
@@ -1,22 +1,7 @@
1
1
  import React from 'react';
2
2
  import renderer from 'react-test-renderer';
3
3
  import { render, screen } from '../../../utils/test-utils';
4
- import {
5
- getRCSContent,
6
- getRcsStatusType,
7
- getTemplateStatusType,
8
- normalizeCardVarMapped,
9
- coalesceCardVarMappedToTemplate,
10
- getRcsSemanticVarNamesSpanningTitleAndDesc,
11
- resolveCardVarMappedSlotValue,
12
- isRcsTextOnlyCardMediaType,
13
- mapRcsCardContentForConsumerWithResolvedTags,
14
- resolveRcsCardPreviewStrings,
15
- sanitizeCardVarMappedValue,
16
- areAllRcsSmsFallbackVarSlotsFilled,
17
- buildRcsNumericMustachePlaceholderRegex,
18
- } from '../utils';
19
- import { rcsVarRegex } from '../constants';
4
+ import { getRCSContent, getRcsStatusType, getTemplateStatusType } from '../utils';
20
5
  import { RCS, RCS_BUTTON_TYPES, STATUS_OPTIONS, RCS_STATUSES } from '../constants';
21
6
  import { mockData } from './mockData';
22
7
 
@@ -41,61 +26,53 @@ describe('RCS utils - renderRcsSuggestionsPreview', () => {
41
26
  },
42
27
  };
43
28
 
44
- const renderWithSuggestion = (buttonType, text) => {
29
+ it('renders divider and QUICK_REPLY suggestion with small-link icon', () => {
45
30
  const template = JSON.parse(JSON.stringify(templateBase));
46
31
  template.versions.base.content[RCS].rcsContent.cardContent[0].suggestions = [
47
- { type: buttonType, text },
32
+ { type: RCS_BUTTON_TYPES.QUICK_REPLY, text: 'Reply' },
48
33
  ];
49
34
  render(getRCSContent(template));
50
- };
51
-
52
- it.each([
53
- [RCS_BUTTON_TYPES.QUICK_REPLY, 'Reply', true],
54
- [RCS_BUTTON_TYPES.CTA, 'Visit', false],
55
- [RCS_BUTTON_TYPES.PHONE_NUMBER, 'Call', false],
56
- ])('renders %s suggestion with expected label', (buttonType, text, expectDivider) => {
57
- renderWithSuggestion(buttonType, text);
58
- if (expectDivider) {
59
- expect(document.querySelectorAll('.whatsapp-divider').length).toBeGreaterThan(0);
60
- }
35
+ expect(document.querySelectorAll('.whatsapp-divider').length).toBeGreaterThan(0);
61
36
  const labels = document.querySelectorAll('.rcs-cta-preview');
62
37
  expect(labels.length).toBe(1);
63
- expect(labels[0].textContent).toContain(text);
38
+ expect(labels[0].textContent).toContain('Reply');
64
39
  });
65
40
 
66
- it('renders only divider for unknown suggestion type', () => {
41
+ it('renders CTA suggestion with launch icon', () => {
67
42
  const template = JSON.parse(JSON.stringify(templateBase));
68
43
  template.versions.base.content[RCS].rcsContent.cardContent[0].suggestions = [
69
- { type: RCS_BUTTON_TYPES.NONE, text: 'Ignored' },
44
+ { type: RCS_BUTTON_TYPES.CTA, text: 'Visit' },
70
45
  ];
71
46
  render(getRCSContent(template));
72
- expect(document.querySelectorAll('.rcs-cta-preview').length).toBe(0);
73
- expect(document.querySelectorAll('.whatsapp-divider').length).toBeGreaterThan(0);
47
+ const labels = document.querySelectorAll('.rcs-cta-preview');
48
+ expect(labels.length).toBe(1);
49
+ expect(labels[0].textContent).toContain('Visit');
74
50
  });
75
51
 
76
- it('prefers thumbnailUrl over mediaUrl for preview image', () => {
52
+ it('renders PHONE_NUMBER suggestion with call icon', () => {
77
53
  const template = JSON.parse(JSON.stringify(templateBase));
78
- const card = template.versions.base.content[RCS].rcsContent.cardContent[0];
79
- card.media = { thumbnailUrl: 'thumb.jpg', mediaUrl: 'full.jpg' };
54
+ template.versions.base.content[RCS].rcsContent.cardContent[0].suggestions = [
55
+ { type: RCS_BUTTON_TYPES.PHONE_NUMBER, text: 'Call' },
56
+ ];
80
57
  render(getRCSContent(template));
81
- const img = document.querySelector('.rcs-listing-image');
82
- expect(img?.getAttribute('src')).toBe('thumb.jpg');
58
+ const labels = document.querySelectorAll('.rcs-cta-preview');
59
+ expect(labels.length).toBe(1);
60
+ expect(labels[0].textContent).toContain('Call');
83
61
  });
84
62
  });
85
63
 
86
64
  describe('RCS utils', () => {
87
65
  describe('getRCSContent', () => {
88
- /** Single place for snapshot serialization — keeps test bodies focused on template setup. */
89
- const expectGetRCSContentSnapshot = (template) => {
90
- expect(renderer.create(getRCSContent(template)).toJSON()).toMatchSnapshot();
91
- };
92
-
93
66
  it('renders RCS content with image and CTA suggestion', () => {
94
- expectGetRCSContentSnapshot(mockData.editData1.templateDetails);
67
+ const template = mockData.editData1.templateDetails;
68
+ const tree = renderer.create(getRCSContent(template)).toJSON();
69
+ expect(tree).toMatchSnapshot();
95
70
  });
96
71
 
97
72
  it('renders RCS content with no media', () => {
98
- expectGetRCSContentSnapshot(mockData.editData3.templateDetails);
73
+ const template = mockData.editData3.templateDetails;
74
+ const tree = renderer.create(getRCSContent(template)).toJSON();
75
+ expect(tree).toMatchSnapshot();
99
76
  });
100
77
 
101
78
  it('renders RCS content with multiple suggestion types', () => {
@@ -105,209 +82,19 @@ describe('RCS utils', () => {
105
82
  { type: RCS_BUTTON_TYPES.CTA, text: 'URL Action' },
106
83
  { type: RCS_BUTTON_TYPES.PHONE_NUMBER, text: 'Call Now' },
107
84
  ];
108
- expectGetRCSContentSnapshot(template);
85
+ const tree = renderer.create(getRCSContent(template)).toJSON();
86
+ expect(tree).toMatchSnapshot();
109
87
  });
110
88
 
111
89
  it('renders RCS content with missing cardContent', () => {
112
- expectGetRCSContentSnapshot({ versions: { base: { content: { RCS: { rcsContent: {} } } } } });
90
+ const template = { versions: { base: { content: { RCS: { rcsContent: {} } } } } };
91
+ const tree = renderer.create(getRCSContent(template)).toJSON();
92
+ expect(tree).toMatchSnapshot();
113
93
  });
114
94
 
115
95
  it('renders RCS content with empty template', () => {
116
- expectGetRCSContentSnapshot({});
117
- });
118
-
119
- it('renders RCS carousel listing preview when cardType is carousel — first card only, no peek', () => {
120
- const template = JSON.parse(JSON.stringify(mockData.editData1.templateDetails));
121
- const rcsContent = template.versions.base.content.RCS.rcsContent;
122
- rcsContent.cardType = 'CAROUSEL';
123
- rcsContent.cardContent = [
124
- {
125
- title: 'Card1',
126
- description: 'Desc1',
127
- media: { mediaUrl: 'https://example.com/1.png' },
128
- suggestions: [],
129
- },
130
- {
131
- title: 'Card2',
132
- description: 'Desc2',
133
- media: { mediaUrl: 'https://example.com/2.png' },
134
- suggestions: [],
135
- },
136
- {
137
- title: 'Card3',
138
- description: 'Desc3',
139
- media: { mediaUrl: 'https://example.com/3.png' },
140
- suggestions: [],
141
- },
142
- {
143
- title: 'Card4',
144
- description: 'Desc4',
145
- media: { mediaUrl: 'https://example.com/4.png' },
146
- suggestions: [],
147
- },
148
- ];
149
-
150
- render(getRCSContent(template));
151
- // Listing preview only ever shows the first card — no peek/scroll of the rest (Figma).
152
- expect(document.querySelectorAll('.whatsapp-carousel-container').length).toBe(1);
153
- expect(document.querySelector('.whatsapp-carousel-body').textContent).toContain('Desc1');
154
- expect(document.querySelector('.whatsapp-carousel-body').textContent).not.toContain('Desc2');
155
- });
156
-
157
- const carouselTemplate = (cardContent) => ({
158
- versions: {
159
- base: {
160
- content: {
161
- [RCS]: {
162
- rcsContent: {
163
- cardType: 'carousel',
164
- cardContent,
165
- },
166
- },
167
- },
168
- },
169
- },
170
- });
171
-
172
- const carouselSecondCardFiller = {
173
- title: 'B',
174
- description: 'D',
175
- media: { mediaUrl: 'https://example.com/b.png' },
176
- suggestions: [],
177
- };
178
-
179
- it('carousel: video card with thumbnailUrl uses thumbnail (not placeholder)', () => {
180
- const template = carouselTemplate([
181
- {
182
- title: 'Vid',
183
- description: 'With thumb',
184
- media: {
185
- mediaType: 'VIDEO',
186
- mediaUrl: 'https://example.com/video.mp4',
187
- thumbnailUrl: 'https://example.com/poster.jpg',
188
- },
189
- suggestions: [],
190
- },
191
- carouselSecondCardFiller,
192
- ]);
193
- render(getRCSContent(template));
194
- expect(document.querySelector('.rcs-video-preview-placeholder')).toBeFalsy();
195
- const firstImg = document.querySelector('.whatsapp-carousel-card .whatsapp-image');
196
- expect(firstImg).toBeTruthy();
197
- expect(firstImg.getAttribute('src')).toBe('https://example.com/poster.jpg');
198
- });
199
-
200
- it('carousel: shows video preview placeholder when VIDEO has mediaUrl but no thumbnail (no img src to mp4)', () => {
201
- const template = carouselTemplate([
202
- {
203
- title: 'Clip',
204
- description: 'No thumb',
205
- media: {
206
- mediaType: 'VIDEO',
207
- mediaUrl: 'https://example.com/video.mp4',
208
- },
209
- suggestions: [],
210
- },
211
- carouselSecondCardFiller,
212
- ]);
213
- render(getRCSContent(template));
214
- expect(document.querySelector('.rcs-video-preview-placeholder')).toBeTruthy();
215
- expect(document.querySelector('.rcs-video-preview-label')?.textContent).toContain('Video preview');
216
- });
217
-
218
- it('carousel: media box renders no image when card has no image/video URL', () => {
219
- const template = carouselTemplate([
220
- {
221
- title: 'Text only',
222
- description: 'No media',
223
- media: {},
224
- suggestions: [],
225
- },
226
- {
227
- title: 'Second',
228
- description: '',
229
- suggestions: [],
230
- },
231
- ]);
232
- render(getRCSContent(template));
233
- const mediaBoxes = document.querySelectorAll('.rcs-carousel-listing-media-box');
234
- expect(mediaBoxes.length).toBeGreaterThan(0);
235
- expect(mediaBoxes[0].querySelector('.whatsapp-image:not(.rcs-video-preview-placeholder)')).toBeFalsy();
236
- });
237
-
238
- it('carousel: body shows description only — no card title', () => {
239
- const template = carouselTemplate([
240
- {
241
- title: 'Head',
242
- description: 'Tail line',
243
- media: { mediaUrl: 'https://example.com/x.png' },
244
- suggestions: [],
245
- },
246
- {
247
- title: 'Y',
248
- description: 'Z',
249
- media: { mediaUrl: 'https://example.com/y.png' },
250
- suggestions: [],
251
- },
252
- ]);
253
- render(getRCSContent(template));
254
- const bodies = document.querySelectorAll('.whatsapp-carousel-body');
255
- expect(bodies[0].textContent).not.toContain('Head');
256
- expect(bodies[0].textContent).toContain('Tail line');
257
- });
258
-
259
- it('carousel: body handles missing title and empty description (first card only)', () => {
260
- const template = carouselTemplate([
261
- {
262
- title: 'Only title',
263
- description: '',
264
- media: { mediaUrl: 'https://example.com/x.png' },
265
- suggestions: [],
266
- },
267
- ]);
268
- render(getRCSContent(template));
269
- // Title is never rendered in the listing preview, so a title-only card is blank.
270
- const body = document.querySelector('.whatsapp-carousel-body');
271
- expect(body.textContent.trim()).toBe('');
272
- });
273
-
274
- it('carousel: does not render buttons/suggestions in the listing preview', () => {
275
- const template = carouselTemplate([
276
- {
277
- title: 'C1',
278
- description: 'D1',
279
- media: { mediaUrl: 'https://example.com/1.png' },
280
- suggestions: [{ type: RCS_BUTTON_TYPES.CTA, text: 'Go' }],
281
- },
282
- {
283
- title: 'C2',
284
- description: 'D2',
285
- media: { mediaUrl: 'https://example.com/2.png' },
286
- suggestions: [],
287
- },
288
- ]);
289
- render(getRCSContent(template));
290
- expect(document.querySelectorAll('.rcs-cta-preview').length).toBe(0);
291
- });
292
-
293
- it('carousel: does not render suggestion block when suggestions missing or not an array', () => {
294
- const template = carouselTemplate([
295
- {
296
- title: 'A',
297
- description: 'B',
298
- media: { mediaUrl: 'https://example.com/a.png' },
299
- },
300
- {
301
- title: 'C',
302
- description: 'D',
303
- media: { mediaUrl: 'https://example.com/c.png' },
304
- suggestions: null,
305
- },
306
- ]);
307
- render(getRCSContent(template));
308
- const firstCard = document.querySelectorAll('.whatsapp-carousel-card')[0];
309
- const dividersInFirst = firstCard.querySelectorAll('.whatsapp-divider');
310
- expect(dividersInFirst.length).toBe(0);
96
+ const tree = renderer.create(getRCSContent({})).toJSON();
97
+ expect(tree).toMatchSnapshot();
311
98
  });
312
99
  });
313
100
 
@@ -349,404 +136,4 @@ describe('RCS utils', () => {
349
136
  expect(getTemplateStatusType('some_unknown_status')).toBe('warning');
350
137
  });
351
138
  });
352
-
353
- describe('normalizeCardVarMapped', () => {
354
- it('maps numeric key + placeholder value to tag name', () => {
355
- expect(normalizeCardVarMapped({ 1: '{{user_id_b64}}' })).toEqual({ user_id_b64: '' });
356
- });
357
-
358
- it('maps numeric key + literal value when orderedTagNames is provided', () => {
359
- expect(
360
- normalizeCardVarMapped({ 1: 'hello' }, ['user_id_b64']),
361
- ).toEqual({ user_id_b64: 'hello' });
362
- });
363
-
364
- it('keeps numeric key when no orderedTagNames for literal value', () => {
365
- expect(normalizeCardVarMapped({ 1: 'hello' })).toEqual({ 1: 'hello' });
366
- });
367
-
368
- it('keeps semantic key + different tag token (TagList value)', () => {
369
- expect(normalizeCardVarMapped({ myKey: '{{tag}}' })).toEqual({ myKey: '{{tag}}' });
370
- });
371
-
372
- it('maps numeric slot + chosen tag onto template token when ordered names provided', () => {
373
- expect(
374
- normalizeCardVarMapped({ 1: '{{FirstName}}' }, ['user_name']),
375
- ).toEqual({ user_name: '{{FirstName}}' });
376
- });
377
-
378
- it('returns {} for null, undefined, or non-object raw', () => {
379
- expect(normalizeCardVarMapped(null)).toEqual({});
380
- expect(normalizeCardVarMapped(undefined)).toEqual({});
381
- expect(normalizeCardVarMapped('not-object')).toEqual({});
382
- });
383
-
384
- it('maps numeric key to order slot only when index is within orderedTagNames', () => {
385
- expect(normalizeCardVarMapped({ 3: 'z' }, ['a', 'b'])).toEqual({ 3: 'z' });
386
- });
387
-
388
- it('does not overwrite a literal slot value with a duplicate semantic placeholder entry', () => {
389
- expect(
390
- normalizeCardVarMapped(
391
- { 1: 'hello', user_id_b64: '{{user_id_b64}}' },
392
- ['user_id_b64'],
393
- ),
394
- ).toEqual({ user_id_b64: 'hello' });
395
- });
396
- });
397
-
398
- describe('getRcsSemanticVarNamesSpanningTitleAndDesc', () => {
399
- it('returns names that appear in both title and description', () => {
400
- const set = getRcsSemanticVarNamesSpanningTitleAndDesc(
401
- 'Hello {{adv}}',
402
- 'Body {{adv}} x {{other}}',
403
- rcsVarRegex,
404
- );
405
- expect(Array.from(set).sort()).toEqual(['adv']);
406
- });
407
-
408
- it('is empty when the same name is only repeated in the title', () => {
409
- const set = getRcsSemanticVarNamesSpanningTitleAndDesc(
410
- '{{user_name}} and {{user_name}}',
411
- 'Hi',
412
- rcsVarRegex,
413
- );
414
- expect(set.size).toBe(0);
415
- });
416
- });
417
-
418
- describe('coalesceCardVarMappedToTemplate', () => {
419
- it('does not copy a shared semantic value into the second slot when the tag spans title and description', () => {
420
- const out = coalesceCardVarMappedToTemplate(
421
- { adv: 'shared' },
422
- 'T {{adv}}',
423
- 'D {{adv}}',
424
- rcsVarRegex,
425
- );
426
- expect(out[1]).toBe('shared');
427
- expect(out[2]).toBe('');
428
- expect(out.adv).toBe('shared');
429
- });
430
-
431
- it('maps slot 1 to first token name from title', () => {
432
- expect(
433
- coalesceCardVarMappedToTemplate(
434
- { 1: 'abc' },
435
- 'Hi {{user_name}}',
436
- '',
437
- rcsVarRegex,
438
- ),
439
- ).toEqual({ 1: 'abc', user_name: 'abc' });
440
- });
441
-
442
- it('keeps {{1}} style token as key 1', () => {
443
- expect(
444
- coalesceCardVarMappedToTemplate(
445
- { 1: '' },
446
- 'Hi {{1}}',
447
- '',
448
- rcsVarRegex,
449
- ),
450
- ).toEqual({ 1: '' });
451
- });
452
-
453
- it('returns clone when no tokens', () => {
454
- const raw = { 1: 'x' };
455
- const out = coalesceCardVarMappedToTemplate(raw, 'no vars', '', rcsVarRegex);
456
- expect(out).toEqual({ 1: 'x' });
457
- expect(out).not.toBe(raw);
458
- });
459
-
460
- it('merges tokens from title and description', () => {
461
- const out = coalesceCardVarMappedToTemplate(
462
- { a: '1', b: '2' },
463
- 'T {{a}}',
464
- 'D {{b}}',
465
- rcsVarRegex,
466
- );
467
- expect(out).toEqual({ a: '1', b: '2', 1: '1', 2: '2' });
468
- });
469
-
470
- it('returns {} when there are no tokens and raw is null or not an object', () => {
471
- expect(coalesceCardVarMappedToTemplate(null, 'plain', '', rcsVarRegex)).toEqual({});
472
- expect(coalesceCardVarMappedToTemplate(undefined, 'plain', '', rcsVarRegex)).toEqual({});
473
- });
474
-
475
- it('fills token values from legacy numeric slots when named key is missing', () => {
476
- expect(
477
- coalesceCardVarMappedToTemplate(
478
- { 1: 'legacy-val' },
479
- 'Hello {{name}}',
480
- '',
481
- rcsVarRegex,
482
- ),
483
- ).toEqual({ 1: 'legacy-val', name: 'legacy-val' });
484
- });
485
-
486
- it('maps values when raw is non-null but missing named keys', () => {
487
- expect(
488
- coalesceCardVarMappedToTemplate(
489
- {},
490
- 'X {{a}}',
491
- '',
492
- rcsVarRegex,
493
- ),
494
- ).toEqual({ 1: '', a: '' });
495
- });
496
- });
497
-
498
- describe('resolveCardVarMappedSlotValue', () => {
499
- it('prefers per-slot numeric key when both semantic and numeric are present', () => {
500
- expect(
501
- resolveCardVarMappedSlotValue({ user_name: 'A', 1: 'B' }, 'user_name', 0),
502
- ).toBe('B');
503
- });
504
-
505
- it('falls back to slot 1 when name missing', () => {
506
- expect(resolveCardVarMappedSlotValue({ 1: 'legacy' }, 'user_name', 0)).toBe('legacy');
507
- });
508
-
509
- it('uses global slot index for second variable', () => {
510
- expect(resolveCardVarMappedSlotValue({ 2: 'second' }, 'b', 1)).toBe('second');
511
- });
512
-
513
- it('reads slot from string slot key when varName has no direct mapping', () => {
514
- expect(resolveCardVarMappedSlotValue({ 1: 'only-slot' }, 'missing', 0)).toBe('only-slot');
515
- });
516
-
517
- it('reads numeric object key for slot when string slot key is absent', () => {
518
- expect(resolveCardVarMappedSlotValue({ 2: 'n2' }, 'x', 1)).toBe('n2');
519
- });
520
-
521
- it('returns empty when named key is cleared, not numeric slot fallback', () => {
522
- expect(
523
- resolveCardVarMappedSlotValue({ user_name: '', 1: 'old value' }, 'user_name', 0),
524
- ).toBe('');
525
- });
526
-
527
- it('library mode: semantic empty does not hide non-empty numeric slot (campaign / journey payload)', () => {
528
- expect(
529
- resolveCardVarMappedSlotValue(
530
- { user_id_b64: '', 1: 'selected-from-library' },
531
- 'user_id_b64',
532
- 0,
533
- true,
534
- ),
535
- ).toBe('selected-from-library');
536
- });
537
-
538
- it('empty numeric slot falls back to semantic tag (preview after hydration leaves 1:"")', () => {
539
- expect(
540
- resolveCardVarMappedSlotValue(
541
- { promotion_points: '{{loyalty_points}}', 1: '' },
542
- 'promotion_points',
543
- 0,
544
- ),
545
- ).toBe('{{loyalty_points}}');
546
- });
547
-
548
- it('omitSemanticFallback: duplicate title+desc tag does not read shared semantic for either slot', () => {
549
- expect(
550
- resolveCardVarMappedSlotValue({ adv: '{{adv}}', 1: '', 2: '' }, 'adv', 0, false, true),
551
- ).toBe('');
552
- expect(
553
- resolveCardVarMappedSlotValue({ adv: '{{adv}}', 1: 'A', 2: 'B' }, 'adv', 0, false, true),
554
- ).toBe('A');
555
- expect(
556
- resolveCardVarMappedSlotValue({ adv: '{{adv}}', 1: 'A', 2: 'B' }, 'adv', 1, false, true),
557
- ).toBe('B');
558
- });
559
-
560
- it('omitSemanticFallback: cleared semantic does not force empty when numeric slot has a value', () => {
561
- expect(
562
- resolveCardVarMappedSlotValue({ adv: '', 1: 'first', 2: 'second' }, 'adv', 1, false, true),
563
- ).toBe('second');
564
- });
565
- });
566
-
567
- describe('mapRcsCardContentForConsumerWithResolvedTags', () => {
568
- it('sets title and description to resolved tag strings on each card', () => {
569
- const out = mapRcsCardContentForConsumerWithResolvedTags(
570
- [
571
- {
572
- title: '',
573
- description: 'Visit {{gt}} discount',
574
- mediaType: 'NONE',
575
- cardVarMapped: { gt: '{{loyalty_points}}', 1: '{{loyalty_points}}' },
576
- },
577
- ],
578
- {},
579
- false,
580
- );
581
- expect(out[0].title).toBe('');
582
- expect(out[0].description).toBe('Visit {{loyalty_points}} discount');
583
- });
584
-
585
- it('merges root rcsCardVarMapped with nested cardVarMapped', () => {
586
- const out = mapRcsCardContentForConsumerWithResolvedTags(
587
- [
588
- {
589
- title: 'Hi {{first_name}}',
590
- description: 'Pts {{promotion_points}}',
591
- mediaType: 'IMAGE',
592
- cardVarMapped: {
593
- promotion_points: '{{loyalty_points}}',
594
- 2: '{{loyalty_points}}',
595
- },
596
- },
597
- ],
598
- {
599
- first_name: '{{customer_name}}',
600
- 1: '{{customer_name}}',
601
- },
602
- false,
603
- );
604
- expect(out[0].title).toBe('Hi {{customer_name}}');
605
- expect(out[0].description).toBe('Pts {{loyalty_points}}');
606
- });
607
-
608
- it('passes through null or non-object items in the card array unchanged', () => {
609
- const out = mapRcsCardContentForConsumerWithResolvedTags([null, 'string', 42], {}, false);
610
- expect(out).toEqual([null, 'string', 42]);
611
- });
612
- });
613
-
614
- describe('isRcsTextOnlyCardMediaType', () => {
615
- it('returns true for NONE and TEXT (text message card)', () => {
616
- expect(isRcsTextOnlyCardMediaType('NONE')).toBe(true);
617
- expect(isRcsTextOnlyCardMediaType('TEXT')).toBe(true);
618
- expect(isRcsTextOnlyCardMediaType('text')).toBe(true);
619
- });
620
- it('returns false for rich card media', () => {
621
- expect(isRcsTextOnlyCardMediaType('IMAGE')).toBe(false);
622
- expect(isRcsTextOnlyCardMediaType('VIDEO')).toBe(false);
623
- });
624
- });
625
-
626
- describe('resolveRcsCardPreviewStrings', () => {
627
- it('substitutes mapped tags for title and description (campaign preview parity)', () => {
628
- const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
629
- 'Hi {{first_name}}',
630
- 'Pts {{promotion_points}}',
631
- {
632
- first_name: '{{customer_name}}',
633
- promotion_points: '{{loyalty_points}}',
634
- 1: '{{customer_name}}',
635
- 2: '{{loyalty_points}}',
636
- },
637
- true,
638
- );
639
- expect(rcsTitle).toBe('Hi {{customer_name}}');
640
- expect(rcsDesc).toBe('Pts {{loyalty_points}}');
641
- });
642
-
643
- it('leaves placeholders when map has no value', () => {
644
- const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
645
- '{{a}}',
646
- '{{b}}',
647
- {},
648
- false,
649
- );
650
- expect(rcsTitle).toBe('{{a}}');
651
- expect(rcsDesc).toBe('{{b}}');
652
- });
653
-
654
- it('text-only card: ignores stale title and resolves description from global slot 0', () => {
655
- const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
656
- 'Stale {{old_tag}} title',
657
- 'Visit Store for {{gt}} discount',
658
- { gt: '{{loyalty_points}}', 1: '{{loyalty_points}}' },
659
- true,
660
- true,
661
- );
662
- expect(rcsTitle).toBe('');
663
- expect(rcsDesc).toBe('Visit Store for {{loyalty_points}} discount');
664
- });
665
- });
666
-
667
- describe('sanitizeCardVarMappedValue', () => {
668
- it('returns empty for null', () => {
669
- expect(sanitizeCardVarMappedValue(null)).toBe('');
670
- });
671
-
672
- it('strips numeric-only self-placeholder stored as value', () => {
673
- expect(sanitizeCardVarMappedValue('{{1}}')).toBe('');
674
- expect(sanitizeCardVarMappedValue('{{12}}')).toBe('');
675
- });
676
-
677
- it('keeps semantic mustache values from TagList', () => {
678
- expect(sanitizeCardVarMappedValue('{{FirstName}}')).toBe('{{FirstName}}');
679
- expect(sanitizeCardVarMappedValue('{{user_name}}')).toBe('{{user_name}}');
680
- });
681
-
682
- it('returns original string for non-placeholder values', () => {
683
- expect(sanitizeCardVarMappedValue(' literal ')).toBe(' literal ');
684
- });
685
- });
686
-
687
- describe('areAllRcsSmsFallbackVarSlotsFilled', () => {
688
- it('returns true when template empty', () => {
689
- expect(areAllRcsSmsFallbackVarSlotsFilled('', { '{{a}}_0': 'x' })).toBe(true);
690
- });
691
-
692
- it('returns false when a mustache slot is only whitespace (parity with RCS title/desc vars)', () => {
693
- expect(
694
- areAllRcsSmsFallbackVarSlotsFilled('Hello {{name}}', { '{{name}}_1': ' ' }),
695
- ).toBe(false);
696
- });
697
-
698
- it('returns false when a mustache slot is missing or empty string', () => {
699
- expect(areAllRcsSmsFallbackVarSlotsFilled('Hello {{name}}', {})).toBe(false);
700
- expect(areAllRcsSmsFallbackVarSlotsFilled('Hello {{name}}', { '{{name}}_1': '' })).toBe(
701
- false,
702
- );
703
- });
704
-
705
- it('returns false when a DLT {#var#} slot is empty', () => {
706
- expect(areAllRcsSmsFallbackVarSlotsFilled('Hi {#x#}', { '{#x#}_1': ' ' })).toBe(false);
707
- });
708
-
709
- it('requires mapping for {{optout}} token like any other variable', () => {
710
- expect(areAllRcsSmsFallbackVarSlotsFilled('test {{optout}}', {})).toBe(false);
711
- expect(areAllRcsSmsFallbackVarSlotsFilled('test {{ optout }}', {})).toBe(false);
712
- expect(areAllRcsSmsFallbackVarSlotsFilled('test {{optout}}', { '{{optout}}_1': '{{optout}}' })).toBe(true);
713
- expect(areAllRcsSmsFallbackVarSlotsFilled('test {{optout}}', { '{{optout}}_1': '' })).toBe(false);
714
- });
715
-
716
- it('returns true when template is missing or not a string', () => {
717
- expect(areAllRcsSmsFallbackVarSlotsFilled(null, {})).toBe(true);
718
- expect(areAllRcsSmsFallbackVarSlotsFilled(undefined, {})).toBe(true);
719
- expect(areAllRcsSmsFallbackVarSlotsFilled(123, {})).toBe(true);
720
- });
721
-
722
- it('returns true when all variable slots have non-whitespace values', () => {
723
- expect(
724
- areAllRcsSmsFallbackVarSlotsFilled('Hello {{name}}', { '{{name}}_1': 'Ann' }),
725
- ).toBe(true);
726
- });
727
-
728
- it('coerces non-string slot values with String() so DLT/hydration payloads still count as filled', () => {
729
- expect(
730
- areAllRcsSmsFallbackVarSlotsFilled('Hello {{name}}', { '{{name}}_1': 42 }),
731
- ).toBe(true);
732
- });
733
-
734
- it('accepts legacy 1-based ordinal keys when the only token is at segment index 0 (DLT / API parity)', () => {
735
- expect(areAllRcsSmsFallbackVarSlotsFilled('{#shop#}', { '{#shop#}_1': 'Mart' })).toBe(true);
736
- expect(areAllRcsSmsFallbackVarSlotsFilled('{#shop#}', { '{#shop#}_1': '' })).toBe(false);
737
- });
738
- });
739
-
740
- describe('buildRcsNumericMustachePlaceholderRegex', () => {
741
- it('escapes regex metacharacters in numeric name', () => {
742
- const re = buildRcsNumericMustachePlaceholderRegex('1.5');
743
- expect(re.test('{{1.5}}')).toBe(true);
744
- });
745
-
746
- it('matches simple numeric placeholder', () => {
747
- const re = buildRcsNumericMustachePlaceholderRegex('2');
748
- expect(re.test('{{2}}')).toBe(true);
749
- expect(re.test('{{3}}')).toBe(false);
750
- });
751
- });
752
139
  });