@capillarytech/creatives-library 9.0.29 → 9.0.30

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 (147) hide show
  1. package/constants/unified.js +29 -0
  2. package/global-styles.js +1 -1
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/commonUtils.js +22 -1
  6. package/utils/rcsPayloadUtils.js +102 -0
  7. package/utils/templateVarUtils.js +198 -0
  8. package/utils/tests/rcsPayloadUtils.test.js +295 -0
  9. package/utils/tests/templateVarUtils.test.js +204 -0
  10. package/v2Components/CapActionButton/constants.js +21 -1
  11. package/v2Components/CapActionButton/index.js +261 -144
  12. package/v2Components/CapActionButton/index.scss +245 -14
  13. package/v2Components/CapActionButton/messages.js +32 -3
  14. package/v2Components/CapActionButton/tests/index.test.js +74 -19
  15. package/v2Components/CapImageUpload/index.js +2 -2
  16. package/v2Components/CapTagList/index.js +9 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +58 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +186 -22
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  28. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  29. package/v2Components/CommonTestAndPreview/constants.js +35 -2
  30. package/v2Components/CommonTestAndPreview/index.js +770 -231
  31. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  32. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  33. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  34. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +412 -257
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +337 -63
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  39. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  40. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +108 -15
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  46. package/v2Components/CommonTestAndPreview/tests/utils.test.js +151 -0
  47. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  48. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  49. package/v2Components/SmsFallback/constants.js +94 -0
  50. package/v2Components/SmsFallback/index.js +958 -0
  51. package/v2Components/SmsFallback/index.scss +266 -0
  52. package/v2Components/SmsFallback/messages.js +78 -0
  53. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  54. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  55. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  58. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  59. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +471 -0
  60. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  61. package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
  62. package/v2Components/TemplatePreview/constants.js +2 -0
  63. package/v2Components/TemplatePreview/index.js +147 -32
  64. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  65. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  66. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  67. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  68. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  69. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  70. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  71. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  72. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  75. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +27 -0
  76. package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
  77. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  78. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  79. package/v2Containers/CreativesContainer/constants.js +11 -0
  80. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +77 -0
  81. package/v2Containers/CreativesContainer/index.js +322 -106
  82. package/v2Containers/CreativesContainer/index.scss +102 -1
  83. package/v2Containers/CreativesContainer/tests/CreativesSlideBoxWrapper.test.js +58 -0
  84. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  85. package/v2Containers/CreativesContainer/tests/SlideBoxContent.smsDltPreview.test.js +73 -0
  86. package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +37 -0
  87. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +103 -34
  88. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +193 -15
  89. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +88 -0
  90. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +549 -57
  91. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  92. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  93. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  94. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  95. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  96. package/v2Containers/Rcs/carouselUtils.js +224 -0
  97. package/v2Containers/Rcs/components/CarouselCard.js +317 -0
  98. package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
  99. package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
  100. package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
  101. package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
  102. package/v2Containers/Rcs/constants.js +132 -16
  103. package/v2Containers/Rcs/index.js +1822 -946
  104. package/v2Containers/Rcs/index.scss +443 -8
  105. package/v2Containers/Rcs/messages.js +45 -22
  106. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +494 -0
  107. package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
  108. package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
  109. package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
  110. package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
  111. package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
  112. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74440 -39387
  113. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  114. package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
  115. package/v2Containers/Rcs/tests/index.test.js +275 -40
  116. package/v2Containers/Rcs/tests/mockData.js +38 -0
  117. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +912 -0
  118. package/v2Containers/Rcs/tests/utils.test.js +682 -30
  119. package/v2Containers/Rcs/utils.js +514 -12
  120. package/v2Containers/Sms/Create/index.js +115 -48
  121. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  122. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  123. package/v2Containers/SmsTrai/Create/index.js +9 -4
  124. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  125. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  126. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  127. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  128. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  129. package/v2Containers/SmsWrapper/index.js +37 -8
  130. package/v2Containers/TagList/index.js +5 -0
  131. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  132. package/v2Containers/Templates/_templates.scss +196 -12
  133. package/v2Containers/Templates/actions.js +11 -0
  134. package/v2Containers/Templates/constants.js +2 -0
  135. package/v2Containers/Templates/index.js +131 -59
  136. package/v2Containers/Templates/sagas.js +57 -13
  137. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
  138. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  139. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  140. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  141. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  142. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  143. package/v2Containers/TemplatesV2/index.js +88 -25
  144. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  145. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  146. package/v2Containers/Whatsapp/index.js +3 -20
  147. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -0,0 +1,912 @@
1
+ import {
2
+ getUnmappedDesc,
3
+ hasRcsVarTokens,
4
+ normalizeLibraryLoadedTitleDesc,
5
+ mergeRcsSmsFallBackContentFromDetails,
6
+ mergeRcsSmsFallbackVarMapLayers,
7
+ pickFirstSmsFallbackTemplateString,
8
+ syncCardVarMappedSemanticsFromSlots,
9
+ hasMeaningfulSmsFallbackShape,
10
+ getLibrarySmsFallbackApiBaselineFromTemplateData,
11
+ extractRegisteredSenderIdsFromSmsFallbackRecord,
12
+ pickRcsCardVarMappedEntries,
13
+ mergeSmsFallbackForLibrary,
14
+ buildSmsFallBackContentForPayload,
15
+ resolveSmsFallbackHydrationFromDetails,
16
+ applySmsFallbackHydration,
17
+ } from '../rcsLibraryHydrationUtils';
18
+ import { rcsVarRegex } from '../constants';
19
+
20
+ const enableTraiDlt = () => {
21
+ window.capAuth = { accessibleFeatures: ['TRAI_DLT'] };
22
+ };
23
+ const disableTraiDlt = () => {
24
+ delete window.capAuth;
25
+ };
26
+
27
+ describe('rcsLibraryHydrationUtils', () => {
28
+ describe('mergeRcsSmsFallBackContentFromDetails', () => {
29
+ it('returns empty object for missing details', () => {
30
+ expect(mergeRcsSmsFallBackContentFromDetails(null)).toEqual({});
31
+ expect(mergeRcsSmsFallBackContentFromDetails(undefined)).toEqual({});
32
+ });
33
+
34
+ it('merges nested RCS smsFallBackContent with root mirror; nested wins on overlap', () => {
35
+ const details = {
36
+ versions: {
37
+ base: {
38
+ content: {
39
+ RCS: {
40
+ smsFallBackContent: {
41
+ smsTemplateName: 'OldName',
42
+ message: 'from nested',
43
+ smsContent: 'nested body',
44
+ },
45
+ },
46
+ },
47
+ },
48
+ },
49
+ smsFallBackContent: {
50
+ message: 'from root',
51
+ },
52
+ };
53
+ const out = mergeRcsSmsFallBackContentFromDetails(details);
54
+ expect(out.message).toBe('from nested');
55
+ expect(out.smsContent).toBe('nested body');
56
+ expect(out.smsTemplateName).toBe('OldName');
57
+ });
58
+
59
+ it('uses root when nested path is absent', () => {
60
+ const details = {
61
+ smsFallBackContent: { message: 'only root' },
62
+ };
63
+ expect(mergeRcsSmsFallBackContentFromDetails(details)).toEqual({ message: 'only root' });
64
+ });
65
+ });
66
+
67
+ describe('mergeRcsSmsFallbackVarMapLayers', () => {
68
+ it('merges API map with local so empty local object does not wipe API slots', () => {
69
+ const api = { rcsSmsFallbackVarMapped: { '{#a#}_0': 'x' } };
70
+ const local = { rcsSmsFallbackVarMapped: {} };
71
+ expect(mergeRcsSmsFallbackVarMapLayers(api, local)).toEqual({ '{#a#}_0': 'x' });
72
+ });
73
+
74
+ it('local slot values win on same key', () => {
75
+ const api = { rcsSmsFallbackVarMapped: { '{#a#}_0': 'old' } };
76
+ const local = { rcsSmsFallbackVarMapped: { '{#a#}_0': 'new' } };
77
+ expect(mergeRcsSmsFallbackVarMapLayers(api, local)).toEqual({ '{#a#}_0': 'new' });
78
+ });
79
+
80
+ it('reads kebab-case var map from a layer when camel is absent', () => {
81
+ const api = { 'rcs-sms-fallback-var-mapped': { '{#b#}_1': 'y' } };
82
+ expect(mergeRcsSmsFallbackVarMapLayers(api, {})).toEqual({ '{#b#}_1': 'y' });
83
+ });
84
+
85
+ it('defaults both layers to {} when called without arguments', () => {
86
+ expect(mergeRcsSmsFallbackVarMapLayers()).toEqual({});
87
+ });
88
+
89
+ it('treats a null layer as empty rather than throwing', () => {
90
+ expect(mergeRcsSmsFallbackVarMapLayers(null, null)).toEqual({});
91
+ });
92
+ });
93
+
94
+ describe('hasMeaningfulSmsFallbackShape', () => {
95
+ it('is false for empty / missing', () => {
96
+ expect(hasMeaningfulSmsFallbackShape(null)).toBe(false);
97
+ expect(hasMeaningfulSmsFallbackShape({})).toBe(false);
98
+ });
99
+
100
+ it('is true when template body or name or var map exists', () => {
101
+ expect(hasMeaningfulSmsFallbackShape({ message: 'hi' })).toBe(true);
102
+ expect(hasMeaningfulSmsFallbackShape({ smsTemplateName: 'T' })).toBe(true);
103
+ expect(hasMeaningfulSmsFallbackShape({ rcsSmsFallbackVarMapped: { '{#a#}_0': 'x' } })).toBe(true);
104
+ });
105
+ });
106
+
107
+ describe('getLibrarySmsFallbackApiBaselineFromTemplateData', () => {
108
+ it('merges root and nested; nested wins on overlap', () => {
109
+ const templateData = {
110
+ smsFallBackContent: { message: 'root' },
111
+ versions: {
112
+ base: {
113
+ content: {
114
+ RCS: {
115
+ smsFallBackContent: { message: 'nested', smsTemplateName: 'N' },
116
+ },
117
+ },
118
+ },
119
+ },
120
+ };
121
+ const out = getLibrarySmsFallbackApiBaselineFromTemplateData(templateData);
122
+ expect(out.message).toBe('nested');
123
+ expect(out.smsTemplateName).toBe('N');
124
+ });
125
+ });
126
+
127
+ describe('extractRegisteredSenderIdsFromSmsFallbackRecord', () => {
128
+ it('returns null for missing / empty', () => {
129
+ expect(extractRegisteredSenderIdsFromSmsFallbackRecord(null)).toBe(null);
130
+ expect(extractRegisteredSenderIdsFromSmsFallbackRecord({})).toBe(null);
131
+ expect(
132
+ extractRegisteredSenderIdsFromSmsFallbackRecord({ registeredSenderIds: [] }),
133
+ ).toBe(null);
134
+ });
135
+
136
+ it('prefers top-level registeredSenderIds', () => {
137
+ expect(
138
+ extractRegisteredSenderIdsFromSmsFallbackRecord({
139
+ registeredSenderIds: ['A'],
140
+ templateConfigs: { registeredSenderIds: ['B'] },
141
+ }),
142
+ ).toEqual(['A']);
143
+ });
144
+
145
+ it('reads templateConfigs.registeredSenderIds when top-level absent', () => {
146
+ expect(
147
+ extractRegisteredSenderIdsFromSmsFallbackRecord({
148
+ templateConfigs: { registeredSenderIds: ['DLT1', 'DLT2'] },
149
+ }),
150
+ ).toEqual(['DLT1', 'DLT2']);
151
+ });
152
+
153
+ it('falls back to templateConfigs.header array', () => {
154
+ expect(
155
+ extractRegisteredSenderIdsFromSmsFallbackRecord({
156
+ templateConfigs: { header: ['H1'] },
157
+ }),
158
+ ).toEqual(['H1']);
159
+ });
160
+ });
161
+
162
+ describe('pickFirstSmsFallbackTemplateString', () => {
163
+ it('prefers smsTemplateContent over resolved message so DLT tokens stay in the string', () => {
164
+ const sms = {
165
+ message: 'Hello user',
166
+ smsTemplateContent: 'Hello {#name#}',
167
+ };
168
+ expect(pickFirstSmsFallbackTemplateString(sms)).toBe('Hello {#name#}');
169
+ });
170
+
171
+ it('skips whitespace-only templateContent and uses next field', () => {
172
+ expect(
173
+ pickFirstSmsFallbackTemplateString({
174
+ templateContent: ' ',
175
+ message: 'x',
176
+ }),
177
+ ).toBe('x');
178
+ });
179
+
180
+ it('defaults to {} and returns "" when called without arguments', () => {
181
+ expect(pickFirstSmsFallbackTemplateString()).toBe('');
182
+ });
183
+
184
+ it('returns "" for a non-object input', () => {
185
+ expect(pickFirstSmsFallbackTemplateString(null)).toBe('');
186
+ expect(pickFirstSmsFallbackTemplateString('not-an-object')).toBe('');
187
+ });
188
+ });
189
+
190
+ describe('getUnmappedDesc', () => {
191
+ it('returns empty string for falsy str', () => {
192
+ expect(getUnmappedDesc('', { a: '1' })).toBe('');
193
+ expect(getUnmappedDesc(null, { a: '1' })).toBe('');
194
+ });
195
+
196
+ it('returns str when mapping is empty or missing', () => {
197
+ expect(getUnmappedDesc('hello', null)).toBe('hello');
198
+ expect(getUnmappedDesc('hello', {})).toBe('hello');
199
+ });
200
+
201
+ it('replaces literal values with {{key}} placeholders', () => {
202
+ expect(getUnmappedDesc('Hi Alice', { user_name: 'Alice' })).toBe('Hi {{user_name}}');
203
+ });
204
+
205
+ it('skips mapping entries with empty or whitespace-only values', () => {
206
+ expect(getUnmappedDesc('x', { a: '', b: ' ' })).toBe('x');
207
+ });
208
+
209
+ it('treats already-braced raw values without duplicating braced needle', () => {
210
+ expect(getUnmappedDesc('v {{user_id}}', { user_id: '{{user_id}}' })).toContain('{{user_id}}');
211
+ });
212
+
213
+ it('adds braced needle when resolved value is not already in {{…}} form', () => {
214
+ const out = getUnmappedDesc('Hello Bob', { nick: 'Bob' });
215
+ expect(out).toBe('Hello {{nick}}');
216
+ });
217
+
218
+ it('ignores undefined mapping values', () => {
219
+ expect(getUnmappedDesc('x', { a: undefined })).toBe('x');
220
+ });
221
+
222
+ it('matches longer needles first to avoid partial replaces', () => {
223
+ const out = getUnmappedDesc('ab', { x: 'a', y: 'ab' });
224
+ expect(out).toBe('{{y}}');
225
+ });
226
+
227
+ it('escapes regex metacharacters in resolved values', () => {
228
+ expect(getUnmappedDesc('price 9.99', { p: '9.99' })).toBe('price {{p}}');
229
+ });
230
+ });
231
+
232
+ describe('hasRcsVarTokens', () => {
233
+ it('is false for empty or non-matching strings', () => {
234
+ expect(hasRcsVarTokens('', rcsVarRegex)).toBe(false);
235
+ expect(hasRcsVarTokens('plain', rcsVarRegex)).toBe(false);
236
+ });
237
+
238
+ it('is true when string contains {{word}} tokens', () => {
239
+ expect(hasRcsVarTokens('{{1}} hi', rcsVarRegex)).toBe(true);
240
+ expect(hasRcsVarTokens('{{user_id}}', rcsVarRegex)).toBe(true);
241
+ expect(hasRcsVarTokens('{{dynamic_expiry_date_after_3_days.FORMAT_1}}', rcsVarRegex)).toBe(true);
242
+ });
243
+ });
244
+
245
+ describe('normalizeLibraryLoadedTitleDesc', () => {
246
+ const cardVarMappedAfterHydration = { user_id: '123' };
247
+
248
+ it('returns originals in full mode', () => {
249
+ const out = normalizeLibraryLoadedTitleDesc({
250
+ loadedTitle: 'Hello 123',
251
+ loadedDesc: 'Desc',
252
+ isFullMode: true,
253
+ cardVarMappedAfterHydration,
254
+ rcsVarRegex,
255
+ });
256
+ expect(out).toEqual({ normalizedTitle: 'Hello 123', normalizedDesc: 'Desc' });
257
+ });
258
+
259
+ it('unmaps when library mode, map present, and no mustache tokens', () => {
260
+ const out = normalizeLibraryLoadedTitleDesc({
261
+ loadedTitle: 'Hello 123',
262
+ loadedDesc: 'World 123',
263
+ isFullMode: false,
264
+ cardVarMappedAfterHydration,
265
+ rcsVarRegex,
266
+ });
267
+ expect(out.normalizedTitle).toBe('Hello {{user_id}}');
268
+ expect(out.normalizedDesc).toBe('World {{user_id}}');
269
+ });
270
+
271
+ it('keeps {{numeric}} slots when tokens already exist', () => {
272
+ const out = normalizeLibraryLoadedTitleDesc({
273
+ loadedTitle: 'Hi {{1}}',
274
+ loadedDesc: 'Body 123',
275
+ isFullMode: false,
276
+ cardVarMappedAfterHydration,
277
+ rcsVarRegex,
278
+ });
279
+ expect(out.normalizedTitle).toBe('Hi {{1}}');
280
+ expect(out.normalizedDesc).toBe('Body {{user_id}}');
281
+ });
282
+
283
+ it('does not unmap when cardVarMappedAfterHydration is empty', () => {
284
+ const out = normalizeLibraryLoadedTitleDesc({
285
+ loadedTitle: 'Hello',
286
+ loadedDesc: 'D',
287
+ isFullMode: false,
288
+ cardVarMappedAfterHydration: {},
289
+ rcsVarRegex,
290
+ });
291
+ expect(out).toEqual({ normalizedTitle: 'Hello', normalizedDesc: 'D' });
292
+ });
293
+ });
294
+
295
+ describe('syncCardVarMappedSemanticsFromSlots', () => {
296
+ it('copies numeric slot values onto empty semantic keys (campaign round-trip)', () => {
297
+ const out = syncCardVarMappedSemanticsFromSlots(
298
+ { 1: '[FirstName]', user_name: '', 2: 'x', other: '' },
299
+ 'Hi {{user_name}}',
300
+ 'More {{other}}',
301
+ rcsVarRegex,
302
+ );
303
+ expect(out.user_name).toBe('[FirstName]');
304
+ expect(out.other).toBe('x');
305
+ });
306
+
307
+ it('does not overwrite non-empty semantic keys', () => {
308
+ const out = syncCardVarMappedSemanticsFromSlots(
309
+ { 1: 'from-slot', user_name: 'kept' },
310
+ 'Hello {{user_name}}',
311
+ '',
312
+ rcsVarRegex,
313
+ );
314
+ expect(out.user_name).toBe('kept');
315
+ });
316
+
317
+ it('treats a non-object cardVarMappedInput as an empty starting map', () => {
318
+ const out = syncCardVarMappedSemanticsFromSlots(null, 'Hi {{a}}', '', rcsVarRegex);
319
+ expect(out).toEqual({});
320
+ });
321
+
322
+ it('falls back to an empty token list when the title has no matches', () => {
323
+ const out = syncCardVarMappedSemanticsFromSlots(
324
+ {},
325
+ 'plain title with no tokens',
326
+ 'More {{other}}',
327
+ rcsVarRegex,
328
+ );
329
+ expect(out.other).toBeUndefined();
330
+ });
331
+
332
+ it('skips a token whose semantic name is blank after trimming', () => {
333
+ const out = syncCardVarMappedSemanticsFromSlots({}, 'Hi {{ }}', '', rcsVarRegex);
334
+ expect(out).toEqual({});
335
+ });
336
+
337
+ it('treats missing semantic and numeric slot values as empty rather than throwing', () => {
338
+ const out = syncCardVarMappedSemanticsFromSlots({}, 'Hi {{user_name}}', '', rcsVarRegex);
339
+ expect(out.user_name).toBeUndefined();
340
+ });
341
+ });
342
+
343
+ describe('pickRcsCardVarMappedEntries', () => {
344
+ it('returns {} for null, undefined, and array inputs', () => {
345
+ expect(pickRcsCardVarMappedEntries(null)).toEqual({});
346
+ expect(pickRcsCardVarMappedEntries(undefined)).toEqual({});
347
+ expect(pickRcsCardVarMappedEntries(['{#v#}_0', 'x'])).toEqual({});
348
+ });
349
+
350
+ it('keeps numeric slot keys', () => {
351
+ expect(pickRcsCardVarMappedEntries({ 1: 'a', 2: 'b' })).toEqual({ 1: 'a', 2: 'b' });
352
+ });
353
+
354
+ it('keeps semantic word keys', () => {
355
+ expect(pickRcsCardVarMappedEntries({ user_name: 'Alice', gt: '{{tag}}' })).toEqual({
356
+ user_name: 'Alice',
357
+ gt: '{{tag}}',
358
+ });
359
+ });
360
+
361
+ it('strips SMS fallback slot keys like {#var#}_0', () => {
362
+ const input = { 1: 'x', user_name: 'y', '{#promo#}_0': 'z', '{{name}}_1': 'w' };
363
+ const out = pickRcsCardVarMappedEntries(input);
364
+ expect(out).toEqual({ 1: 'x', user_name: 'y' });
365
+ });
366
+
367
+ it('returns {} for an empty object', () => {
368
+ expect(pickRcsCardVarMappedEntries({})).toEqual({});
369
+ });
370
+ });
371
+
372
+ describe('mergeSmsFallbackForLibrary', () => {
373
+ afterEach(() => disableTraiDlt());
374
+
375
+ it('full mode returns smsFallbackData as-is', () => {
376
+ const smsFallbackData = { message: 'hi' };
377
+ expect(mergeSmsFallbackForLibrary({}, smsFallbackData, true)).toBe(smsFallbackData);
378
+ });
379
+
380
+ it('full mode returns {} when smsFallbackData is missing', () => {
381
+ expect(mergeSmsFallbackForLibrary({}, null, true)).toEqual({});
382
+ expect(mergeSmsFallbackForLibrary({}, undefined, true)).toEqual({});
383
+ });
384
+
385
+ it('non-full mode: local wins over API baseline except var map merges', () => {
386
+ const templateData = {
387
+ smsFallBackContent: { message: 'api', rcsSmsFallbackVarMapped: { '{#a#}_0': 'x' } },
388
+ };
389
+ const local = { message: 'local', rcsSmsFallbackVarMapped: {} };
390
+ const out = mergeSmsFallbackForLibrary(templateData, local, false);
391
+ expect(out.message).toBe('local');
392
+ expect(out.rcsSmsFallbackVarMapped).toEqual({ '{#a#}_0': 'x' });
393
+ });
394
+
395
+ it('non-full mode: treats a non-object smsFallbackData as empty local layer', () => {
396
+ const templateData = { smsFallBackContent: { message: 'api-only' } };
397
+ const out = mergeSmsFallbackForLibrary(templateData, undefined, false);
398
+ expect(out.message).toBe('api-only');
399
+ expect(out.rcsSmsFallbackVarMapped).toEqual({});
400
+ });
401
+ });
402
+
403
+ describe('buildSmsFallBackContentForPayload', () => {
404
+ afterEach(() => disableTraiDlt());
405
+
406
+ it('full mode: returns undefined when smsFallbackData has no meaningful shape', () => {
407
+ const out = buildSmsFallBackContentForPayload({
408
+ smsFallbackData: {},
409
+ templateData: {},
410
+ isFullMode: true,
411
+ smsRegister: undefined,
412
+ });
413
+ expect(out).toBeUndefined();
414
+ });
415
+
416
+ it('full mode: builds full shape with unicodeValidity, var map, and templateConfigs from top-level fields', () => {
417
+ const out = buildSmsFallBackContentForPayload({
418
+ smsFallbackData: {
419
+ message: 'Hello',
420
+ templateName: 'T1',
421
+ unicodeValidity: true,
422
+ rcsSmsFallbackVarMapped: { '{#a#}_0': 'x' },
423
+ smsTemplateId: '111',
424
+ registeredSenderIds: ['S1'],
425
+ },
426
+ templateData: {},
427
+ isFullMode: true,
428
+ smsRegister: undefined,
429
+ });
430
+ expect(out).toEqual({
431
+ smsFallBackContent: {
432
+ smsTemplateName: 'T1',
433
+ smsContent: 'Hello',
434
+ message: 'Hello',
435
+ unicodeValidity: true,
436
+ rcsSmsFallbackVarMapped: { '{#a#}_0': 'x' },
437
+ templateConfigs: {
438
+ templateId: '111',
439
+ template: 'Hello',
440
+ templateName: 'T1',
441
+ registeredSenderIds: ['S1'],
442
+ },
443
+ },
444
+ });
445
+ });
446
+
447
+ it('full mode: omits unicodeValidity / var map / templateConfigs when absent', () => {
448
+ const out = buildSmsFallBackContentForPayload({
449
+ smsFallbackData: { message: 'Body' },
450
+ templateData: {},
451
+ isFullMode: true,
452
+ smsRegister: undefined,
453
+ });
454
+ expect(out).toEqual({
455
+ smsFallBackContent: {
456
+ smsTemplateName: '',
457
+ smsContent: 'Body',
458
+ message: 'Body',
459
+ },
460
+ });
461
+ });
462
+
463
+ it('full mode: templateConfigs.templateId / .template fall back when smsTemplateId absent', () => {
464
+ const out = buildSmsFallBackContentForPayload({
465
+ smsFallbackData: {
466
+ templateName: 'TN',
467
+ templateConfigs: {
468
+ templateId: 'TC2',
469
+ template: 'fallback template str',
470
+ registeredSenderIds: ['R2'],
471
+ },
472
+ },
473
+ templateData: {},
474
+ isFullMode: true,
475
+ smsRegister: undefined,
476
+ });
477
+ expect(out.smsFallBackContent.templateConfigs).toEqual({
478
+ templateId: 'TC2',
479
+ template: 'fallback template str',
480
+ templateName: 'TN',
481
+ registeredSenderIds: ['R2'],
482
+ });
483
+ });
484
+
485
+ it('full mode: templateConfigs.name / .header are used when nothing else is present', () => {
486
+ const out = buildSmsFallBackContentForPayload({
487
+ smsFallbackData: {
488
+ smsTemplateName: '',
489
+ message: '',
490
+ rcsSmsFallbackVarMapped: { a: '1' },
491
+ templateConfigs: { header: ['H1'], name: 'ConfigName' },
492
+ },
493
+ templateData: {},
494
+ isFullMode: true,
495
+ smsRegister: undefined,
496
+ });
497
+ expect(out.smsFallBackContent.templateConfigs).toEqual({
498
+ templateName: 'ConfigName',
499
+ registeredSenderIds: ['H1'],
500
+ });
501
+ expect(out.smsFallBackContent.rcsSmsFallbackVarMapped).toEqual({ a: '1' });
502
+ });
503
+
504
+ it('non-full mode: returns undefined when merged shape has no message/name/var map', () => {
505
+ const out = buildSmsFallBackContentForPayload({
506
+ smsFallbackData: {},
507
+ templateData: {},
508
+ isFullMode: false,
509
+ smsRegister: undefined,
510
+ });
511
+ expect(out).toBeUndefined();
512
+ });
513
+
514
+ it('non-full mode: DLT campaign with templateConfigs derived includes templateConfigs', () => {
515
+ enableTraiDlt();
516
+ const out = buildSmsFallBackContentForPayload({
517
+ smsFallbackData: {
518
+ message: 'Body2',
519
+ smsTemplateId: 'ID9',
520
+ registeredSenderIds: ['S9'],
521
+ },
522
+ templateData: {},
523
+ isFullMode: false,
524
+ smsRegister: 'DLT',
525
+ });
526
+ expect(out).toEqual({
527
+ smsFallBackContent: {
528
+ message: 'Body2',
529
+ templateContent: 'Body2',
530
+ registeredSenderIds: ['S9'],
531
+ templateConfigs: {
532
+ templateId: 'ID9',
533
+ template: 'Body2',
534
+ registeredSenderIds: ['S9'],
535
+ },
536
+ },
537
+ });
538
+ });
539
+
540
+ it('non-full mode: DLT campaign but no derivable templateConfigs omits templateConfigs', () => {
541
+ enableTraiDlt();
542
+ const out = buildSmsFallBackContentForPayload({
543
+ smsFallbackData: {
544
+ message: 'Hi',
545
+ unicodeValidity: false,
546
+ rcsSmsFallbackVarMapped: { '{#a#}_0': 'v' },
547
+ },
548
+ templateData: {},
549
+ isFullMode: false,
550
+ smsRegister: 'DLT',
551
+ });
552
+ expect(out).toEqual({
553
+ smsFallBackContent: {
554
+ message: 'Hi',
555
+ templateContent: 'Hi',
556
+ unicodeValidity: false,
557
+ rcsSmsFallbackVarMapped: { '{#a#}_0': 'v' },
558
+ },
559
+ });
560
+ });
561
+
562
+ it('non-full mode: non-DLT campaign omits templateConfigs even when derivable, but includes smsTemplateName', () => {
563
+ const out = buildSmsFallBackContentForPayload({
564
+ smsFallbackData: {
565
+ message: 'Body3',
566
+ templateName: 'TN3',
567
+ smsTemplateId: 'ID3',
568
+ },
569
+ templateData: {},
570
+ isFullMode: false,
571
+ smsRegister: 'NON_DLT',
572
+ });
573
+ expect(out).toEqual({
574
+ smsFallBackContent: {
575
+ message: 'Body3',
576
+ smsTemplateName: 'TN3',
577
+ templateContent: 'Body3',
578
+ },
579
+ });
580
+ });
581
+
582
+ it('non-full mode: omits templateContent when it resolves empty', () => {
583
+ const out = buildSmsFallBackContentForPayload({
584
+ smsFallbackData: { templateName: 'OnlyName' },
585
+ templateData: {},
586
+ isFullMode: false,
587
+ smsRegister: undefined,
588
+ });
589
+ expect(out).toEqual({
590
+ smsFallBackContent: {
591
+ message: '',
592
+ smsTemplateName: 'OnlyName',
593
+ },
594
+ });
595
+ });
596
+
597
+ it('non-full mode: falls back to a raw whitespace-only templateContent string over templateConfigs.template', () => {
598
+ enableTraiDlt();
599
+ const out = buildSmsFallBackContentForPayload({
600
+ smsFallbackData: {
601
+ templateName: 'HasName',
602
+ templateContent: ' ',
603
+ smsTemplateId: 'IDX',
604
+ templateConfigs: { template: 'should not be used' },
605
+ },
606
+ templateData: {},
607
+ isFullMode: false,
608
+ smsRegister: 'DLT',
609
+ });
610
+ expect(out.smsFallBackContent.message).toBe('');
611
+ expect(out.smsFallBackContent.templateConfigs.template).toBe(' ');
612
+ });
613
+
614
+ it('non-full mode: merges API baseline templateData with local editor state', () => {
615
+ const templateData = {
616
+ smsFallBackContent: { message: 'from-api', smsTemplateName: 'ApiName' },
617
+ };
618
+ const out = buildSmsFallBackContentForPayload({
619
+ smsFallbackData: { message: 'from-local' },
620
+ templateData,
621
+ isFullMode: false,
622
+ smsRegister: undefined,
623
+ });
624
+ expect(out.smsFallBackContent.message).toBe('from-local');
625
+ expect(out.smsFallBackContent.smsTemplateName).toBe('ApiName');
626
+ });
627
+ });
628
+
629
+ describe('resolveSmsFallbackHydrationFromDetails', () => {
630
+ let warnSpy;
631
+
632
+ beforeEach(() => {
633
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
634
+ });
635
+
636
+ afterEach(() => {
637
+ warnSpy.mockRestore();
638
+ });
639
+
640
+ it('returns hasFallbackPayload false with __EMPTY__ key when there is no fallback content', () => {
641
+ expect(resolveSmsFallbackHydrationFromDetails({})).toEqual({
642
+ hasFallbackPayload: false,
643
+ nextSmsState: null,
644
+ hydrationKey: '__EMPTY__',
645
+ });
646
+ });
647
+
648
+ it('is meaningful from smsTemplateName alone and warns when no message/var map found', () => {
649
+ const out = resolveSmsFallbackHydrationFromDetails({
650
+ _id: 'c1',
651
+ smsFallBackContent: { smsTemplateName: 'T1' },
652
+ });
653
+ expect(out.hasFallbackPayload).toBe(true);
654
+ expect(out.nextSmsState.templateName).toBe('T1');
655
+ expect(out.nextSmsState.content).toBe('');
656
+ expect(warnSpy).toHaveBeenCalled();
657
+ });
658
+
659
+ it('falls back to templateConfigs.templateName when root-level name fields are absent', () => {
660
+ const out = resolveSmsFallbackHydrationFromDetails({
661
+ smsFallBackContent: {
662
+ templateConfigs: { templateName: 'ConfigTemplateName' },
663
+ },
664
+ });
665
+ expect(out.nextSmsState.templateName).toBe('ConfigTemplateName');
666
+ });
667
+
668
+ it('falls back to templateConfigs.name when no other name field is present anywhere', () => {
669
+ const out = resolveSmsFallbackHydrationFromDetails({
670
+ smsFallBackContent: {
671
+ templateConfigs: { name: 'ConfigName' },
672
+ },
673
+ });
674
+ expect(out.nextSmsState.templateName).toBe('ConfigName');
675
+ });
676
+
677
+ it('falls back to the root-level templateName when templateConfigs has no name field', () => {
678
+ const out = resolveSmsFallbackHydrationFromDetails({
679
+ smsFallBackContent: {
680
+ templateName: 'RootName',
681
+ templateConfigs: { registeredSenderIds: ['123'] },
682
+ },
683
+ });
684
+ expect(out.nextSmsState.templateName).toBe('RootName');
685
+ });
686
+
687
+ it('prefers the root templateName over a conflicting nested templateConfigs.name', () => {
688
+ const out = resolveSmsFallbackHydrationFromDetails({
689
+ smsFallBackContent: {
690
+ templateName: 'RootName',
691
+ smsTemplateName: '',
692
+ templateConfigs: { name: 'NestedName', header: ['H1'] },
693
+ },
694
+ });
695
+ expect(out.nextSmsState.templateName).toBe('RootName');
696
+ });
697
+
698
+ it('is meaningful from var map alone, with no template name or message text', () => {
699
+ const out = resolveSmsFallbackHydrationFromDetails({
700
+ smsFallBackContent: { rcsSmsFallbackVarMapped: { '{#a#}_0': 'v' } },
701
+ });
702
+ expect(out.hasFallbackPayload).toBe(true);
703
+ expect(out.nextSmsState.templateName).toBe('');
704
+ expect(out.nextSmsState.content).toBe('');
705
+ expect(warnSpy).not.toHaveBeenCalled();
706
+ });
707
+
708
+ it('does not warn when a var map is present even without message text', () => {
709
+ const out = resolveSmsFallbackHydrationFromDetails({
710
+ smsFallBackContent: {
711
+ smsTemplateName: 'T2',
712
+ rcsSmsFallbackVarMapped: { '{#a#}_0': 'x' },
713
+ },
714
+ });
715
+ expect(out.hasFallbackPayload).toBe(true);
716
+ expect(out.nextSmsState.rcsSmsFallbackVarMapped).toEqual({ '{#a#}_0': 'x' });
717
+ expect(warnSpy).not.toHaveBeenCalled();
718
+ });
719
+
720
+ it('does not warn in production even without message text or var map', () => {
721
+ const prevEnv = process.env.NODE_ENV;
722
+ process.env.NODE_ENV = 'production';
723
+ try {
724
+ const out = resolveSmsFallbackHydrationFromDetails({
725
+ smsFallBackContent: { smsTemplateName: 'T3' },
726
+ });
727
+ expect(out.hasFallbackPayload).toBe(true);
728
+ expect(warnSpy).not.toHaveBeenCalled();
729
+ } finally {
730
+ process.env.NODE_ENV = prevEnv;
731
+ }
732
+ });
733
+
734
+ it('prefers smsContent, then smsTemplateContent, then message, over nested editor text', () => {
735
+ expect(
736
+ resolveSmsFallbackHydrationFromDetails({
737
+ smsFallBackContent: { smsTemplateName: 'X', smsContent: 'Direct SMS' },
738
+ }).nextSmsState.content,
739
+ ).toBe('Direct SMS');
740
+ expect(
741
+ resolveSmsFallbackHydrationFromDetails({
742
+ smsFallBackContent: { smsTemplateName: 'X', smsTemplateContent: 'Direct Template' },
743
+ }).nextSmsState.content,
744
+ ).toBe('Direct Template');
745
+ });
746
+
747
+ it('joins an updated-sms-editor array into the fallback message', () => {
748
+ const out = resolveSmsFallbackHydrationFromDetails({
749
+ smsFallBackContent: {
750
+ smsTemplateName: 'X',
751
+ versions: { base: { 'updated-sms-editor': ['a', 'b', 'c'] } },
752
+ },
753
+ });
754
+ expect(out.nextSmsState.content).toBe('abc');
755
+ });
756
+
757
+ it('uses updated-sms-editor string directly when present', () => {
758
+ const out = resolveSmsFallbackHydrationFromDetails({
759
+ smsFallBackContent: {
760
+ smsTemplateName: 'X',
761
+ versions: { base: { 'updated-sms-editor': 'strval' } },
762
+ },
763
+ });
764
+ expect(out.nextSmsState.content).toBe('strval');
765
+ });
766
+
767
+ it('falls back to sms-editor when updated-sms-editor is neither an array nor a string', () => {
768
+ const out = resolveSmsFallbackHydrationFromDetails({
769
+ smsFallBackContent: {
770
+ smsTemplateName: 'X',
771
+ versions: { base: { 'updated-sms-editor': 5, 'sms-editor': 'final' } },
772
+ },
773
+ });
774
+ expect(out.nextSmsState.content).toBe('final');
775
+ });
776
+
777
+ it('resolves unicodeValidity from the fallback record boolean first', () => {
778
+ const out = resolveSmsFallbackHydrationFromDetails({
779
+ smsFallBackContent: {
780
+ smsContent: 'x',
781
+ unicodeValidity: false,
782
+ versions: { base: { 'unicode-validity': true } },
783
+ },
784
+ });
785
+ expect(out.nextSmsState.unicodeValidity).toBe(false);
786
+ });
787
+
788
+ it('falls back to nested unicode-validity boolean when top-level is not boolean', () => {
789
+ const out = resolveSmsFallbackHydrationFromDetails({
790
+ smsFallBackContent: {
791
+ smsContent: 'x',
792
+ versions: { base: { 'unicode-validity': false } },
793
+ },
794
+ });
795
+ expect(out.nextSmsState.unicodeValidity).toBe(false);
796
+ });
797
+
798
+ it('defaults unicodeValidity to true when neither source is a boolean', () => {
799
+ const out = resolveSmsFallbackHydrationFromDetails({
800
+ smsFallBackContent: { smsContent: 'x' },
801
+ });
802
+ expect(out.nextSmsState.unicodeValidity).toBe(true);
803
+ });
804
+
805
+ it('includes registeredSenderIds only when a non-empty array is extracted', () => {
806
+ const withIds = resolveSmsFallbackHydrationFromDetails({
807
+ smsFallBackContent: { smsContent: 'x', registeredSenderIds: ['S1'] },
808
+ });
809
+ expect(withIds.nextSmsState.registeredSenderIds).toEqual(['S1']);
810
+
811
+ const withoutIds = resolveSmsFallbackHydrationFromDetails({
812
+ smsFallBackContent: { smsContent: 'x' },
813
+ });
814
+ expect(withoutIds.nextSmsState.registeredSenderIds).toBeUndefined();
815
+ });
816
+
817
+ it('builds hydrationKey using _id, then name, then creativeName as the creative fingerprint', () => {
818
+ const base = { smsFallBackContent: { smsContent: 'x' } };
819
+ const byId = resolveSmsFallbackHydrationFromDetails({ ...base, _id: 'id1', name: 'n1', creativeName: 'cn1' });
820
+ const byName = resolveSmsFallbackHydrationFromDetails({ ...base, name: 'n1', creativeName: 'cn1' });
821
+ const byCreativeName = resolveSmsFallbackHydrationFromDetails({ ...base, creativeName: 'cn1' });
822
+ expect(byId.hydrationKey).toContain('id1');
823
+ expect(byName.hydrationKey).toContain('n1');
824
+ expect(byCreativeName.hydrationKey).toContain('cn1');
825
+ });
826
+ });
827
+
828
+ describe('applySmsFallbackHydration', () => {
829
+ it('full mode always calls setSmsFallbackData when a fallback payload exists, even if key is unchanged', () => {
830
+ const details = { smsFallBackContent: { smsContent: 'hi' } };
831
+ const { hydrationKey } = resolveSmsFallbackHydrationFromDetails(details);
832
+ const ref = { current: hydrationKey };
833
+ const setSmsFallbackData = jest.fn();
834
+ applySmsFallbackHydration({
835
+ details,
836
+ isFullMode: true,
837
+ lastSmsFallbackHydrationKeyRef: ref,
838
+ setSmsFallbackData,
839
+ });
840
+ expect(setSmsFallbackData).toHaveBeenCalledTimes(1);
841
+ expect(setSmsFallbackData).toHaveBeenCalledWith(expect.objectContaining({ content: 'hi' }));
842
+ });
843
+
844
+ it('non-full mode calls setSmsFallbackData when the hydration key changed', () => {
845
+ const details = { smsFallBackContent: { smsContent: 'hi' } };
846
+ const ref = { current: 'some-other-key' };
847
+ const setSmsFallbackData = jest.fn();
848
+ applySmsFallbackHydration({
849
+ details,
850
+ isFullMode: false,
851
+ lastSmsFallbackHydrationKeyRef: ref,
852
+ setSmsFallbackData,
853
+ });
854
+ expect(setSmsFallbackData).toHaveBeenCalledTimes(1);
855
+ expect(ref.current).not.toBe('some-other-key');
856
+ });
857
+
858
+ it('non-full mode skips setSmsFallbackData when the hydration key is unchanged (dedup)', () => {
859
+ const details = { smsFallBackContent: { smsContent: 'hi' } };
860
+ const { hydrationKey } = resolveSmsFallbackHydrationFromDetails(details);
861
+ const ref = { current: hydrationKey };
862
+ const setSmsFallbackData = jest.fn();
863
+ applySmsFallbackHydration({
864
+ details,
865
+ isFullMode: false,
866
+ lastSmsFallbackHydrationKeyRef: ref,
867
+ setSmsFallbackData,
868
+ });
869
+ expect(setSmsFallbackData).not.toHaveBeenCalled();
870
+ expect(ref.current).toBe(hydrationKey);
871
+ });
872
+
873
+ it('full mode always calls setSmsFallbackData(null) when there is no fallback payload', () => {
874
+ const ref = { current: '__EMPTY__' };
875
+ const setSmsFallbackData = jest.fn();
876
+ applySmsFallbackHydration({
877
+ details: {},
878
+ isFullMode: true,
879
+ lastSmsFallbackHydrationKeyRef: ref,
880
+ setSmsFallbackData,
881
+ });
882
+ expect(setSmsFallbackData).toHaveBeenCalledTimes(1);
883
+ expect(setSmsFallbackData).toHaveBeenCalledWith(null);
884
+ });
885
+
886
+ it('non-full mode skips setSmsFallbackData(null) when ref is already __EMPTY__', () => {
887
+ const ref = { current: '__EMPTY__' };
888
+ const setSmsFallbackData = jest.fn();
889
+ applySmsFallbackHydration({
890
+ details: {},
891
+ isFullMode: false,
892
+ lastSmsFallbackHydrationKeyRef: ref,
893
+ setSmsFallbackData,
894
+ });
895
+ expect(setSmsFallbackData).not.toHaveBeenCalled();
896
+ });
897
+
898
+ it('non-full mode calls setSmsFallbackData(null) once and marks ref __EMPTY__ when not yet deduped', () => {
899
+ const ref = { current: 'some-previous-key' };
900
+ const setSmsFallbackData = jest.fn();
901
+ applySmsFallbackHydration({
902
+ details: {},
903
+ isFullMode: false,
904
+ lastSmsFallbackHydrationKeyRef: ref,
905
+ setSmsFallbackData,
906
+ });
907
+ expect(setSmsFallbackData).toHaveBeenCalledTimes(1);
908
+ expect(setSmsFallbackData).toHaveBeenCalledWith(null);
909
+ expect(ref.current).toBe('__EMPTY__');
910
+ });
911
+ });
912
+ });