@capillarytech/creatives-library 8.0.318 → 8.0.320

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 (139) hide show
  1. package/constants/unified.js +15 -0
  2. package/package.json +1 -1
  3. package/services/api.js +6 -0
  4. package/services/tests/api.test.js +7 -0
  5. package/utils/common.js +6 -1
  6. package/utils/templateVarUtils.js +172 -0
  7. package/utils/tests/templateVarUtils.test.js +160 -0
  8. package/v2Components/CapTagList/index.js +10 -0
  9. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  10. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  11. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  12. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  13. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  14. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  15. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  16. package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
  17. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  18. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  19. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
  20. package/v2Components/CommonTestAndPreview/constants.js +38 -0
  21. package/v2Components/CommonTestAndPreview/index.js +693 -155
  22. package/v2Components/CommonTestAndPreview/messages.js +41 -3
  23. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  24. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  25. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +352 -0
  26. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
  27. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  28. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  29. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
  30. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  31. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  32. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  33. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  34. package/v2Components/FormBuilder/index.js +7 -1
  35. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  36. package/v2Components/SmsFallback/constants.js +73 -0
  37. package/v2Components/SmsFallback/index.js +956 -0
  38. package/v2Components/SmsFallback/index.scss +265 -0
  39. package/v2Components/SmsFallback/messages.js +78 -0
  40. package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  41. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  42. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  43. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  44. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  45. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  46. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  47. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  48. package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
  49. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  50. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  51. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  52. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  53. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  54. package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
  55. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
  56. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
  57. package/v2Containers/CommunicationFlow/constants.js +200 -0
  58. package/v2Containers/CommunicationFlow/index.js +102 -0
  59. package/v2Containers/CommunicationFlow/messages.js +346 -0
  60. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
  61. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
  62. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
  63. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
  64. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
  65. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
  66. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
  67. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
  68. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
  69. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
  70. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
  71. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
  72. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
  75. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
  76. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
  77. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
  78. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
  79. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
  80. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
  81. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
  82. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
  83. package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
  84. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  85. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  86. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  87. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  88. package/v2Containers/CreativesContainer/constants.js +12 -0
  89. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  90. package/v2Containers/CreativesContainer/index.js +289 -99
  91. package/v2Containers/CreativesContainer/index.scss +51 -1
  92. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  93. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +104 -0
  94. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
  95. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  96. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
  97. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
  98. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  99. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  100. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  101. package/v2Containers/Rcs/constants.js +32 -1
  102. package/v2Containers/Rcs/index.js +950 -873
  103. package/v2Containers/Rcs/index.scss +85 -6
  104. package/v2Containers/Rcs/messages.js +10 -1
  105. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  106. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
  107. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  108. package/v2Containers/Rcs/tests/index.test.js +41 -38
  109. package/v2Containers/Rcs/tests/mockData.js +38 -0
  110. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  111. package/v2Containers/Rcs/tests/utils.test.js +379 -1
  112. package/v2Containers/Rcs/utils.js +358 -10
  113. package/v2Containers/Sms/Create/index.js +81 -36
  114. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  115. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  116. package/v2Containers/SmsTrai/Create/index.js +9 -4
  117. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  118. package/v2Containers/SmsTrai/Edit/index.js +609 -128
  119. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  120. package/v2Containers/SmsTrai/Edit/messages.js +9 -4
  121. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
  122. package/v2Containers/SmsWrapper/index.js +37 -8
  123. package/v2Containers/TagList/index.js +6 -0
  124. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  125. package/v2Containers/Templates/_templates.scss +61 -2
  126. package/v2Containers/Templates/actions.js +11 -0
  127. package/v2Containers/Templates/constants.js +2 -0
  128. package/v2Containers/Templates/index.js +90 -40
  129. package/v2Containers/Templates/sagas.js +57 -12
  130. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  131. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  132. package/v2Containers/Templates/tests/sagas.test.js +193 -12
  133. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  134. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  135. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  136. package/v2Containers/TemplatesV2/index.js +86 -23
  137. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  138. package/v2Containers/Whatsapp/index.js +3 -20
  139. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -55,6 +55,33 @@ describe('parseSenderDetailsResponse', () => {
55
55
  expect(result.domains[0].cdmaSenders[0].value).toBe('CDMA1');
56
56
  });
57
57
 
58
+ it('should populate SMS senders when channel argument is lowercase (normalized for branches)', () => {
59
+ const response = {
60
+ entity: {
61
+ SMS: [
62
+ {
63
+ id: 10,
64
+ priority: 1,
65
+ domainProperties: {
66
+ domainName: 'SMS Domain 1',
67
+ id: 100,
68
+ contactInfo: [
69
+ { type: 'gsm_sender_id', valid: true, value: 'GSM1', default: true },
70
+ { type: 'cdma_sender_id', valid: true, value: 'CDMA1' },
71
+ ],
72
+ connectionProperties: {},
73
+ },
74
+ },
75
+ ],
76
+ },
77
+ };
78
+ const result = parseSenderDetailsResponse('sms', response);
79
+ expect(result.domains).toHaveLength(1);
80
+ expect(result.domains[0].gsmSenders).toHaveLength(1);
81
+ expect(result.domains[0].gsmSenders[0].value).toBe('GSM1');
82
+ expect(result.domains[0].cdmaSenders).toHaveLength(1);
83
+ });
84
+
58
85
  it('should parse EMAIL channel entity array', () => {
59
86
  const response = {
60
87
  entity: {
@@ -231,5 +258,319 @@ describe('parseSenderDetailsResponse', () => {
231
258
  const result = parseSenderDetailsResponse('WHATSAPP', response);
232
259
  expect(result.domains[0].sourceAccountIdentifier).toBe('waba-456');
233
260
  });
261
+
262
+ it('should map hostName to domainName when domainName missing (RCS)', () => {
263
+ const response = {
264
+ entity: {
265
+ RCS: [
266
+ {
267
+ id: 1,
268
+ priority: 0,
269
+ domainProperties: {
270
+ hostName: 'rcs-host',
271
+ id: 42,
272
+ contactInfo: [
273
+ { type: 'gsm_sender_id', valid: true, value: 'S1', default: true },
274
+ ],
275
+ },
276
+ },
277
+ ],
278
+ },
279
+ };
280
+ const result = parseSenderDetailsResponse('RCS', response);
281
+ expect(result.domains[0].domainName).toBe('rcs-host');
282
+ expect(result.domains[0].domainId).toBe(42);
283
+ expect(result.domains[0].gsmSenders[0].value).toBe('S1');
284
+ });
285
+
286
+ it('should unwrap result.entity when top-level entity is absent', () => {
287
+ const response = {
288
+ result: {
289
+ entity: {
290
+ SMS: [
291
+ {
292
+ id: 1,
293
+ priority: 0,
294
+ domainProperties: {
295
+ domainName: 'ViaResult',
296
+ id: 9,
297
+ contactInfo: [{ type: 'gsm_sender_id', valid: true, value: 'G' }],
298
+ },
299
+ },
300
+ ],
301
+ },
302
+ },
303
+ };
304
+ const result = parseSenderDetailsResponse('SMS', response);
305
+ expect(result.domains[0].domainName).toBe('ViaResult');
306
+ });
307
+
308
+ it('should merge rcs_sender_id into gsmSenders for RCS', () => {
309
+ const response = {
310
+ entity: {
311
+ RCS: [
312
+ {
313
+ id: 1,
314
+ priority: 0,
315
+ domainProperties: {
316
+ domainName: 'RCS Domain',
317
+ id: 1,
318
+ contactInfo: [
319
+ { type: 'rcs_sender_id', valid: true, value: 'RCS123', default: true },
320
+ ],
321
+ },
322
+ },
323
+ ],
324
+ },
325
+ };
326
+ const result = parseSenderDetailsResponse('RCS', response);
327
+ expect(result.domains[0].gsmSenders).toHaveLength(1);
328
+ expect(result.domains[0].gsmSenders[0].value).toBe('RCS123');
329
+ });
330
+
331
+ it('should unwrap data.entity when top-level entity is absent', () => {
332
+ const response = {
333
+ data: {
334
+ entity: {
335
+ SMS: [
336
+ {
337
+ id: 1,
338
+ priority: 0,
339
+ domainProperties: {
340
+ domainName: 'ViaData',
341
+ id: 9,
342
+ contactInfo: [{ type: 'gsm_sender_id', valid: true, value: 'G' }],
343
+ },
344
+ },
345
+ ],
346
+ },
347
+ },
348
+ };
349
+ const result = parseSenderDetailsResponse('SMS', response);
350
+ expect(result.domains[0].domainName).toBe('ViaData');
351
+ });
352
+
353
+ it('should return empty domains when entity is explicitly null', () => {
354
+ expect(parseSenderDetailsResponse('SMS', { entity: null })).toEqual({ domains: [] });
355
+ });
356
+
357
+ it('should return empty when channel data is a non-domain object', () => {
358
+ expect(parseSenderDetailsResponse('SMS', { entity: { notAnArray: true } })).toEqual({ domains: [] });
359
+ });
360
+
361
+ it('should use userid when sourceAccountIdentifier and wabaId are missing (WHATSAPP)', () => {
362
+ const response = {
363
+ entity: {
364
+ WHATSAPP: [
365
+ {
366
+ id: 1,
367
+ priority: 0,
368
+ domainProperties: {
369
+ domainName: 'W',
370
+ id: 1,
371
+ connectionProperties: { userid: 'user-99' },
372
+ },
373
+ },
374
+ ],
375
+ },
376
+ };
377
+ const result = parseSenderDetailsResponse('WHATSAPP', response);
378
+ expect(result.domains[0].sourceAccountIdentifier).toBe('user-99');
379
+ });
380
+
381
+ it('should dedupe duplicate GSM values for RCS (merged contact types)', () => {
382
+ const response = {
383
+ entity: {
384
+ RCS: [
385
+ {
386
+ id: 1,
387
+ priority: 0,
388
+ domainProperties: {
389
+ domainName: 'R',
390
+ id: 1,
391
+ contactInfo: [
392
+ { type: 'gsm_sender_id', valid: true, value: 'SAME', default: true },
393
+ { type: 'rcs_sender_id', valid: true, value: 'SAME' },
394
+ ],
395
+ },
396
+ },
397
+ ],
398
+ },
399
+ };
400
+ const result = parseSenderDetailsResponse('RCS', response);
401
+ expect(result.domains[0].gsmSenders.filter((r) => r.value === 'SAME')).toHaveLength(1);
402
+ });
403
+
404
+ it('should return empty domains when response is not an object', () => {
405
+ expect(parseSenderDetailsResponse('SMS', 42)).toEqual({ domains: [] });
406
+ expect(parseSenderDetailsResponse('SMS', 'x')).toEqual({ domains: [] });
407
+ });
408
+
409
+ it('should return empty domains when entity is empty string', () => {
410
+ expect(parseSenderDetailsResponse('SMS', { entity: '' })).toEqual({ domains: [] });
411
+ });
412
+
413
+ it('should normalize hyphenated contactInfo.type to match gsm_sender_id', () => {
414
+ const response = {
415
+ entity: {
416
+ SMS: [
417
+ {
418
+ id: 1,
419
+ domainProperties: {
420
+ domainName: 'D',
421
+ id: 1,
422
+ contactInfo: [
423
+ { type: 'gsm-sender-id', valid: true, value: 'HYPH' },
424
+ ],
425
+ },
426
+ },
427
+ ],
428
+ },
429
+ };
430
+ const result = parseSenderDetailsResponse('SMS', response);
431
+ expect(result.domains[0].gsmSenders[0].value).toBe('HYPH');
432
+ });
433
+
434
+ it('should skip whitespace-only GSM values when merging RCS contact types', () => {
435
+ const response = {
436
+ entity: {
437
+ RCS: [
438
+ {
439
+ id: 1,
440
+ domainProperties: {
441
+ domainName: 'R',
442
+ id: 1,
443
+ contactInfo: [
444
+ { type: 'gsm_sender_id', valid: true, value: ' ' },
445
+ { type: 'gsm_sender_id', valid: true, value: 'OK' },
446
+ ],
447
+ },
448
+ },
449
+ ],
450
+ },
451
+ };
452
+ const values = parseSenderDetailsResponse('RCS', response).domains[0].gsmSenders.map((g) => g.value);
453
+ expect(values).toEqual(['OK']);
454
+ });
455
+
456
+ it('should use default priority 0 when priority is null', () => {
457
+ const response = {
458
+ entity: {
459
+ SMS: [
460
+ {
461
+ id: 1,
462
+ priority: null,
463
+ domainProperties: {
464
+ domainName: 'P',
465
+ id: 9,
466
+ contactInfo: [],
467
+ },
468
+ },
469
+ ],
470
+ },
471
+ };
472
+ expect(parseSenderDetailsResponse('SMS', response).domains[0].priority).toBe(0);
473
+ });
474
+
475
+ it('should unwrap entity from raw response object when entity key is absent', () => {
476
+ const response = { misc: 1 };
477
+ expect(parseSenderDetailsResponse('SMS', response)).toEqual({ domains: [] });
478
+ });
479
+ });
480
+
481
+ describe('unwrapEntity edge cases', () => {
482
+ it('should return empty domains when entity key is explicitly null', () => {
483
+ // unwrapEntity: response.entity === null → returns null → parseSenderDetailsResponse returns { domains: [] }
484
+ expect(parseSenderDetailsResponse('SMS', { entity: null })).toEqual({ domains: [] });
485
+ });
486
+ });
487
+
488
+ describe('null or empty channel', () => {
489
+ it('should return empty domains when channel is null', () => {
490
+ const response = { entity: { SMS: [{ id: 1, domainProperties: { domainName: 'D', id: 10, contactInfo: [] } }] } };
491
+ expect(parseSenderDetailsResponse(null, response)).toEqual({ domains: [] });
492
+ });
493
+
494
+ it('should return empty domains when channel is empty string', () => {
495
+ const response = { entity: { SMS: [{ id: 1, domainProperties: { domainName: 'D', id: 10, contactInfo: [] } }] } };
496
+ expect(parseSenderDetailsResponse('', response)).toEqual({ domains: [] });
497
+ });
498
+ });
499
+
500
+ describe('single non-array domain object', () => {
501
+ it('should wrap single domain with domainProperties in an array and return one domain', () => {
502
+ const response = {
503
+ entity: {
504
+ SMS: {
505
+ id: 5,
506
+ domainProperties: {
507
+ domainName: 'SingleDomain',
508
+ id: 99,
509
+ contactInfo: [{ type: 'gsm_sender_id', valid: true, value: 'GSMSINGLE' }],
510
+ },
511
+ },
512
+ },
513
+ };
514
+ const result = parseSenderDetailsResponse('SMS', response);
515
+ expect(result.domains).toHaveLength(1);
516
+ expect(result.domains[0].domainName).toBe('SingleDomain');
517
+ expect(result.domains[0].gsmSenders[0].value).toBe('GSMSINGLE');
518
+ });
519
+
520
+ it('should return empty domains for single non-array domain without domainProperties', () => {
521
+ const response = {
522
+ entity: { SMS: { id: 5, someProp: 'value' } },
523
+ };
524
+ expect(parseSenderDetailsResponse('SMS', response)).toEqual({ domains: [] });
525
+ });
526
+ });
527
+
528
+ describe('deduplication by domainId when domainName is null', () => {
529
+ it('should deduplicate two entries sharing the same domainId when domainName is null', () => {
530
+ const response = {
531
+ entity: {
532
+ SMS: [
533
+ {
534
+ id: 10,
535
+ priority: 1,
536
+ domainProperties: { domainName: null, id: 42, contactInfo: [] },
537
+ },
538
+ {
539
+ id: 11,
540
+ priority: 2,
541
+ domainProperties: { domainName: null, id: 42, contactInfo: [] },
542
+ },
543
+ ],
544
+ },
545
+ };
546
+ const result = parseSenderDetailsResponse('SMS', response);
547
+ expect(result.domains).toHaveLength(1);
548
+ expect(result.domains[0].domainId).toBe(42);
549
+ });
550
+ });
551
+
552
+ describe('typeMatches null-type filtering', () => {
553
+ it('should exclude contactInfo items with null type from gsmSenders', () => {
554
+ const response = {
555
+ entity: {
556
+ SMS: [
557
+ {
558
+ id: 1,
559
+ domainProperties: {
560
+ domainName: 'D',
561
+ id: 1,
562
+ contactInfo: [
563
+ { type: null, valid: true, value: 'EXCLUDED' },
564
+ { type: 'gsm_sender_id', valid: true, value: 'INCLUDED' },
565
+ ],
566
+ },
567
+ },
568
+ ],
569
+ },
570
+ };
571
+ const result = parseSenderDetailsResponse('SMS', response);
572
+ expect(result.domains[0].gsmSenders).toHaveLength(1);
573
+ expect(result.domains[0].gsmSenders[0].value).toBe('INCLUDED');
574
+ });
234
575
  });
235
576
  });
@@ -24,17 +24,19 @@ import SendTestMessage from '../SendTestMessage';
24
24
 
25
25
  // Mock DeliverySettings to assert props
26
26
  jest.mock('../DeliverySettings', () => function MockDeliverySettings(props) {
27
- return (
27
+ const smsRows = props.senderDetailsByChannel?.SMS || [];
28
+ return (
28
29
  <div data-testid="delivery-settings" data-props={JSON.stringify({
29
30
  channel: props.channel,
30
31
  hasDeliverySettings: !!props.deliverySettings,
31
- senderDetailsLength: (props.senderDetailsOptions || []).length,
32
+ senderDetailsLength: smsRows.length,
32
33
  wecrmAccountsLength: (props.wecrmAccounts || []).length,
33
34
  hasOnSave: typeof props.onSaveDeliverySettings === 'function',
34
35
  isLoadingSenderDetails: props.isLoadingSenderDetails,
35
36
  smsTraiDltEnabled: props.smsTraiDltEnabled,
36
37
  registeredSenderIds: props.registeredSenderIds,
37
38
  whatsappAccountFromForm: props.whatsappAccountFromForm,
39
+ isChannelSmsFallbackPreviewEnabled: props.isChannelSmsFallbackPreviewEnabled,
38
40
  })}
39
41
  />
40
42
  );
@@ -129,12 +131,13 @@ describe('SendTestMessage', () => {
129
131
  formatMessage: jest.fn((msg) => msg.defaultMessage || msg.id),
130
132
  channel: 'EMAIL',
131
133
  deliverySettings: {},
132
- senderDetailsOptions: [],
134
+ senderDetailsByChannel: {},
133
135
  wecrmAccounts: [],
134
136
  onSaveDeliverySettings: jest.fn(),
135
137
  isLoadingSenderDetails: false,
136
138
  smsTraiDltEnabled: false,
137
139
  registeredSenderIds: [],
140
+ isChannelSmsFallbackPreviewEnabled: false,
138
141
  };
139
142
 
140
143
  beforeEach(() => {
@@ -229,6 +232,22 @@ describe('SendTestMessage', () => {
229
232
  expect(screen.getByTestId('delivery-settings')).toBeTruthy();
230
233
  });
231
234
 
235
+ it('should render DeliverySettings when channel is RCS and pass SMS fallback preview flag', () => {
236
+ render(
237
+ <TestWrapper>
238
+ <SendTestMessage
239
+ {...defaultProps}
240
+ channel="RCS"
241
+ isChannelSmsFallbackPreviewEnabled
242
+ />
243
+ </TestWrapper>
244
+ );
245
+ const el = screen.getByTestId('delivery-settings');
246
+ const data = JSON.parse(el.getAttribute('data-props'));
247
+ expect(data.channel).toBe('RCS');
248
+ expect(data.isChannelSmsFallbackPreviewEnabled).toBe(true);
249
+ });
250
+
232
251
  it('should not render DeliverySettings when channel is INAPP', () => {
233
252
  render(
234
253
  <TestWrapper>
@@ -276,7 +295,9 @@ describe('SendTestMessage', () => {
276
295
  {...defaultProps}
277
296
  channel="SMS"
278
297
  deliverySettings={{ domainId: 1 }}
279
- senderDetailsOptions={[{ domainId: 1, domainName: 'SMS Dom' }]}
298
+ senderDetailsByChannel={{
299
+ SMS: [{ domainId: 1, domainName: 'SMS Dom' }],
300
+ }}
280
301
  wecrmAccounts={[]}
281
302
  onSaveDeliverySettings={onSave}
282
303
  isLoadingSenderDetails={true}
@@ -9,7 +9,16 @@ import { render, screen } from '@testing-library/react';
9
9
  import '@testing-library/jest-dom';
10
10
  import { injectIntl, IntlProvider } from 'react-intl';
11
11
  import UnifiedPreview from '../../UnifiedPreview';
12
- import { CHANNELS, DESKTOP, TABLET, MOBILE, ANDROID, IOS } from '../../constants';
12
+ import {
13
+ CHANNELS,
14
+ DESKTOP,
15
+ TABLET,
16
+ MOBILE,
17
+ ANDROID,
18
+ IOS,
19
+ PREVIEW_TAB_RCS,
20
+ PREVIEW_TAB_SMS_FALLBACK,
21
+ } from '../../constants';
13
22
  import messages from '../../messages';
14
23
 
15
24
  // Convert messages object to format expected by IntlProvider
@@ -547,6 +556,195 @@ describe('UnifiedPreview', () => {
547
556
 
548
557
  expect(screen.getByTestId('rcs-sender-id')).toHaveTextContent('RCS_SENDER');
549
558
  });
559
+
560
+ describe('RCS SMS fallback — Test & Preview tabs', () => {
561
+ it('without SMS fallback selected, shows only RCS preview (no RCS+SMS tab layout)', () => {
562
+ const props = {
563
+ ...defaultProps,
564
+ channel: CHANNELS.RCS,
565
+ content: { rcsTitle: 'Hello RCS' },
566
+ smsFallbackContent: undefined,
567
+ };
568
+
569
+ const { container } = render(
570
+ <TestWrapper>
571
+ <ComponentToRender {...props} />
572
+ </TestWrapper>
573
+ );
574
+
575
+ expect(container.querySelector('.unified-preview-rcs-tabs')).toBeNull();
576
+ expect(screen.getByTestId('rcs-preview')).toBeInTheDocument();
577
+ expect(screen.queryByTestId('sms-preview')).not.toBeInTheDocument();
578
+ });
579
+
580
+ it('with SMS fallback template body, shows RCS and Fallback SMS tabs', () => {
581
+ const props = {
582
+ ...defaultProps,
583
+ channel: CHANNELS.RCS,
584
+ content: { rcsTitle: 'Hello RCS' },
585
+ smsFallbackContent: {
586
+ content: 'SMS fallback body',
587
+ templateContent: 'SMS fallback body',
588
+ },
589
+ };
590
+
591
+ const { container } = render(
592
+ <TestWrapper>
593
+ <ComponentToRender {...props} />
594
+ </TestWrapper>
595
+ );
596
+
597
+ expect(container.querySelector('.unified-preview-rcs-tabs')).toBeInTheDocument();
598
+ expect(
599
+ screen.getByRole('tab', { name: messages.rcsTab.defaultMessage })
600
+ ).toBeInTheDocument();
601
+ expect(
602
+ screen.getByRole('tab', { name: messages.smsFallbackTab.defaultMessage })
603
+ ).toBeInTheDocument();
604
+ });
605
+
606
+ it('on SMS fallback tab, renders SMS preview with resolved fallback text when smsFallbackResolvedText is set', () => {
607
+ const props = {
608
+ ...defaultProps,
609
+ channel: CHANNELS.RCS,
610
+ content: { rcsTitle: 'Hello RCS' },
611
+ smsFallbackContent: {
612
+ content: '{{var}}',
613
+ templateContent: '{{var}}',
614
+ },
615
+ smsFallbackResolvedText: 'Resolved SMS for preview',
616
+ activePreviewTab: PREVIEW_TAB_SMS_FALLBACK,
617
+ onPreviewTabChange: jest.fn(),
618
+ };
619
+
620
+ render(
621
+ <TestWrapper>
622
+ <ComponentToRender {...props} />
623
+ </TestWrapper>
624
+ );
625
+
626
+ expect(screen.getByTestId('sms-content')).toHaveTextContent('Resolved SMS for preview');
627
+ });
628
+
629
+ it('on RCS tab (default), renders RCS preview when dual tabs are shown', () => {
630
+ const props = {
631
+ ...defaultProps,
632
+ channel: CHANNELS.RCS,
633
+ content: { rcsTitle: 'Only RCS pane' },
634
+ smsFallbackContent: { content: 'SMS', templateContent: 'SMS' },
635
+ activePreviewTab: PREVIEW_TAB_RCS,
636
+ onPreviewTabChange: jest.fn(),
637
+ };
638
+
639
+ render(
640
+ <TestWrapper>
641
+ <ComponentToRender {...props} />
642
+ </TestWrapper>
643
+ );
644
+
645
+ expect(screen.getByTestId('rcs-preview')).toBeInTheDocument();
646
+ expect(screen.getByTestId('rcs-content')).toHaveTextContent(/Only RCS pane/);
647
+ });
648
+
649
+ it('on SMS fallback tab, shows raw template when no varmap and no resolved text', () => {
650
+ const props = {
651
+ ...defaultProps,
652
+ channel: CHANNELS.RCS,
653
+ content: { rcsTitle: 'Hello RCS' },
654
+ smsFallbackContent: {
655
+ content: 'Hello {{name}}',
656
+ templateContent: 'Hello {{name}}',
657
+ // no rcsSmsFallbackVarMapped
658
+ },
659
+ smsFallbackResolvedText: undefined,
660
+ activePreviewTab: PREVIEW_TAB_SMS_FALLBACK,
661
+ onPreviewTabChange: jest.fn(),
662
+ };
663
+
664
+ render(
665
+ <TestWrapper>
666
+ <ComponentToRender {...props} />
667
+ </TestWrapper>
668
+ );
669
+
670
+ // rawFallbackTemplate is shown directly — {{tags}} remain visible
671
+ expect(screen.getByTestId('sms-content')).toHaveTextContent('Hello {{name}}');
672
+ });
673
+
674
+ it('on SMS fallback tab, treats empty resolved text as absent and shows raw template', () => {
675
+ const props = {
676
+ ...defaultProps,
677
+ channel: CHANNELS.RCS,
678
+ content: { rcsTitle: 'Hello RCS' },
679
+ smsFallbackContent: {
680
+ content: 'Raw {{var}} template',
681
+ templateContent: 'Raw {{var}} template',
682
+ },
683
+ smsFallbackResolvedText: '',
684
+ activePreviewTab: PREVIEW_TAB_SMS_FALLBACK,
685
+ onPreviewTabChange: jest.fn(),
686
+ };
687
+
688
+ render(
689
+ <TestWrapper>
690
+ <ComponentToRender {...props} />
691
+ </TestWrapper>
692
+ );
693
+
694
+ expect(screen.getByTestId('sms-content')).toHaveTextContent('Raw {{var}} template');
695
+ });
696
+
697
+ it('on SMS fallback tab, applies varmap slot substitution when varmap entries exist and no resolved text', () => {
698
+ // getFallbackResolvedContent key format: `${fullToken}_${segmentIndex}`
699
+ // 'Hello {{name}}' → segments ['Hello ', '{{name}}'], so {{name}} is at index 1 → key '{{name}}_1'
700
+ const props = {
701
+ ...defaultProps,
702
+ channel: CHANNELS.RCS,
703
+ content: { rcsTitle: 'Hello RCS' },
704
+ smsFallbackContent: {
705
+ content: 'Hello {{name}}',
706
+ templateContent: 'Hello {{name}}',
707
+ rcsSmsFallbackVarMapped: { '{{name}}_1': 'World' },
708
+ },
709
+ smsFallbackResolvedText: undefined,
710
+ activePreviewTab: PREVIEW_TAB_SMS_FALLBACK,
711
+ onPreviewTabChange: jest.fn(),
712
+ };
713
+
714
+ render(
715
+ <TestWrapper>
716
+ <ComponentToRender {...props} />
717
+ </TestWrapper>
718
+ );
719
+
720
+ expect(screen.getByTestId('sms-content')).toHaveTextContent('Hello World');
721
+ });
722
+
723
+ it('on SMS fallback tab, resolved text takes priority over varmap entries', () => {
724
+ const props = {
725
+ ...defaultProps,
726
+ channel: CHANNELS.RCS,
727
+ content: { rcsTitle: 'Hello RCS' },
728
+ smsFallbackContent: {
729
+ content: 'Hello {{name}}',
730
+ templateContent: 'Hello {{name}}',
731
+ rcsSmsFallbackVarMapped: { '{{name}}_1': 'World' },
732
+ },
733
+ smsFallbackResolvedText: 'Hello John',
734
+ activePreviewTab: PREVIEW_TAB_SMS_FALLBACK,
735
+ onPreviewTabChange: jest.fn(),
736
+ };
737
+
738
+ render(
739
+ <TestWrapper>
740
+ <ComponentToRender {...props} />
741
+ </TestWrapper>
742
+ );
743
+
744
+ // resolvedText wins over varmap substitution
745
+ expect(screen.getByTestId('sms-content')).toHaveTextContent('Hello John');
746
+ });
747
+ });
550
748
  });
551
749
 
552
750
  describe('Channel Routing - INAPP', () => {