@capillarytech/creatives-library 8.0.358 → 8.0.359-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/tests/api.test.js +35 -20
  4. package/utils/commonUtils.js +19 -1
  5. package/utils/rcsPayloadUtils.js +92 -0
  6. package/utils/templateVarUtils.js +201 -0
  7. package/utils/tests/rcsPayloadUtils.test.js +226 -0
  8. package/utils/tests/templateVarUtils.test.js +204 -0
  9. package/v2Components/CapActionButton/constants.js +7 -0
  10. package/v2Components/CapActionButton/index.js +166 -108
  11. package/v2Components/CapActionButton/index.scss +157 -6
  12. package/v2Components/CapActionButton/messages.js +19 -3
  13. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  14. package/v2Components/CapImageUpload/index.js +2 -2
  15. package/v2Components/CapTagList/index.js +10 -0
  16. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +72 -49
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  23. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +157 -15
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +346 -76
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +150 -4
  27. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  28. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  29. package/v2Components/CommonTestAndPreview/index.js +810 -222
  30. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  31. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  32. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  33. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  34. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  35. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  37. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  38. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  39. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  40. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  41. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  42. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  43. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  44. package/v2Components/FormBuilder/index.js +5 -4
  45. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  46. package/v2Components/SmsFallback/constants.js +73 -0
  47. package/v2Components/SmsFallback/index.js +956 -0
  48. package/v2Components/SmsFallback/index.scss +265 -0
  49. package/v2Components/SmsFallback/messages.js +78 -0
  50. package/v2Components/SmsFallback/smsFallbackUtils.js +119 -0
  51. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  52. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  53. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  54. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +223 -0
  55. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  56. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  57. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  58. package/v2Components/TemplatePreview/_templatePreview.scss +37 -22
  59. package/v2Components/TemplatePreview/constants.js +2 -0
  60. package/v2Components/TemplatePreview/index.js +143 -31
  61. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  62. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  63. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  64. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  65. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  66. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  67. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  68. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +17 -0
  69. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  70. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  71. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  72. package/v2Containers/CreativesContainer/constants.js +9 -0
  73. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +79 -0
  74. package/v2Containers/CreativesContainer/index.js +322 -103
  75. package/v2Containers/CreativesContainer/index.scss +83 -1
  76. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  77. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +79 -34
  78. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  79. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  80. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  81. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  82. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  83. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  84. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  85. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  86. package/v2Containers/Rcs/constants.js +120 -11
  87. package/v2Containers/Rcs/index.js +2577 -812
  88. package/v2Containers/Rcs/index.scss +281 -8
  89. package/v2Containers/Rcs/messages.js +34 -3
  90. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  91. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98036 -70145
  92. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  93. package/v2Containers/Rcs/tests/index.test.js +152 -121
  94. package/v2Containers/Rcs/tests/mockData.js +38 -0
  95. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  96. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  97. package/v2Containers/Rcs/utils.js +478 -11
  98. package/v2Containers/Sms/Create/index.js +106 -40
  99. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  100. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  101. package/v2Containers/SmsTrai/Create/index.js +9 -4
  102. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  103. package/v2Containers/SmsTrai/Edit/index.js +640 -130
  104. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  105. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  106. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  107. package/v2Containers/SmsWrapper/index.js +37 -8
  108. package/v2Containers/TagList/index.js +6 -0
  109. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  110. package/v2Containers/Templates/_templates.scss +166 -9
  111. package/v2Containers/Templates/actions.js +11 -0
  112. package/v2Containers/Templates/constants.js +2 -0
  113. package/v2Containers/Templates/index.js +121 -53
  114. package/v2Containers/Templates/sagas.js +56 -12
  115. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  116. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1062 -1017
  117. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  118. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  119. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  120. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  121. package/v2Containers/TemplatesV2/index.js +86 -23
  122. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  123. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  124. package/v2Containers/Whatsapp/index.js +3 -20
  125. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -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
@@ -562,6 +571,195 @@ describe('UnifiedPreview', () => {
562
571
 
563
572
  expect(screen.getByTestId('rcs-sender-id')).toHaveTextContent('RCS_SENDER');
564
573
  });
574
+
575
+ describe('RCS SMS fallback — Test & Preview tabs', () => {
576
+ it('without SMS fallback selected, shows only RCS preview (no RCS+SMS tab layout)', () => {
577
+ const props = {
578
+ ...defaultProps,
579
+ channel: CHANNELS.RCS,
580
+ content: { rcsTitle: 'Hello RCS' },
581
+ smsFallbackContent: undefined,
582
+ };
583
+
584
+ const { container } = render(
585
+ <TestWrapper>
586
+ <ComponentToRender {...props} />
587
+ </TestWrapper>
588
+ );
589
+
590
+ expect(container.querySelector('.unified-preview-rcs-tabs')).toBeNull();
591
+ expect(screen.getByTestId('rcs-preview')).toBeInTheDocument();
592
+ expect(screen.queryByTestId('sms-preview')).not.toBeInTheDocument();
593
+ });
594
+
595
+ it('with SMS fallback template body, shows RCS and Fallback SMS tabs', () => {
596
+ const props = {
597
+ ...defaultProps,
598
+ channel: CHANNELS.RCS,
599
+ content: { rcsTitle: 'Hello RCS' },
600
+ smsFallbackContent: {
601
+ content: 'SMS fallback body',
602
+ templateContent: 'SMS fallback body',
603
+ },
604
+ };
605
+
606
+ const { container } = render(
607
+ <TestWrapper>
608
+ <ComponentToRender {...props} />
609
+ </TestWrapper>
610
+ );
611
+
612
+ expect(container.querySelector('.unified-preview-rcs-tabs')).toBeInTheDocument();
613
+ expect(
614
+ screen.getByRole('tab', { name: messages.rcsTab.defaultMessage })
615
+ ).toBeInTheDocument();
616
+ expect(
617
+ screen.getByRole('tab', { name: messages.smsFallbackTab.defaultMessage })
618
+ ).toBeInTheDocument();
619
+ });
620
+
621
+ it('on SMS fallback tab, renders SMS preview with resolved fallback text when smsFallbackResolvedText is set', () => {
622
+ const props = {
623
+ ...defaultProps,
624
+ channel: CHANNELS.RCS,
625
+ content: { rcsTitle: 'Hello RCS' },
626
+ smsFallbackContent: {
627
+ content: '{{var}}',
628
+ templateContent: '{{var}}',
629
+ },
630
+ smsFallbackResolvedText: 'Resolved SMS for preview',
631
+ activePreviewTab: PREVIEW_TAB_SMS_FALLBACK,
632
+ onPreviewTabChange: jest.fn(),
633
+ };
634
+
635
+ render(
636
+ <TestWrapper>
637
+ <ComponentToRender {...props} />
638
+ </TestWrapper>
639
+ );
640
+
641
+ expect(screen.getByTestId('sms-content')).toHaveTextContent('Resolved SMS for preview');
642
+ });
643
+
644
+ it('on RCS tab (default), renders RCS preview when dual tabs are shown', () => {
645
+ const props = {
646
+ ...defaultProps,
647
+ channel: CHANNELS.RCS,
648
+ content: { rcsTitle: 'Only RCS pane' },
649
+ smsFallbackContent: { content: 'SMS', templateContent: 'SMS' },
650
+ activePreviewTab: PREVIEW_TAB_RCS,
651
+ onPreviewTabChange: jest.fn(),
652
+ };
653
+
654
+ render(
655
+ <TestWrapper>
656
+ <ComponentToRender {...props} />
657
+ </TestWrapper>
658
+ );
659
+
660
+ expect(screen.getByTestId('rcs-preview')).toBeInTheDocument();
661
+ expect(screen.getByTestId('rcs-content')).toHaveTextContent(/Only RCS pane/);
662
+ });
663
+
664
+ it('on SMS fallback tab, shows raw template when no varmap and no resolved text', () => {
665
+ const props = {
666
+ ...defaultProps,
667
+ channel: CHANNELS.RCS,
668
+ content: { rcsTitle: 'Hello RCS' },
669
+ smsFallbackContent: {
670
+ content: 'Hello {{name}}',
671
+ templateContent: 'Hello {{name}}',
672
+ // no rcsSmsFallbackVarMapped
673
+ },
674
+ smsFallbackResolvedText: undefined,
675
+ activePreviewTab: PREVIEW_TAB_SMS_FALLBACK,
676
+ onPreviewTabChange: jest.fn(),
677
+ };
678
+
679
+ render(
680
+ <TestWrapper>
681
+ <ComponentToRender {...props} />
682
+ </TestWrapper>
683
+ );
684
+
685
+ // rawFallbackTemplate is shown directly — {{tags}} remain visible
686
+ expect(screen.getByTestId('sms-content')).toHaveTextContent('Hello {{name}}');
687
+ });
688
+
689
+ it('on SMS fallback tab, treats empty resolved text as absent and shows raw template', () => {
690
+ const props = {
691
+ ...defaultProps,
692
+ channel: CHANNELS.RCS,
693
+ content: { rcsTitle: 'Hello RCS' },
694
+ smsFallbackContent: {
695
+ content: 'Raw {{var}} template',
696
+ templateContent: 'Raw {{var}} template',
697
+ },
698
+ smsFallbackResolvedText: '',
699
+ activePreviewTab: PREVIEW_TAB_SMS_FALLBACK,
700
+ onPreviewTabChange: jest.fn(),
701
+ };
702
+
703
+ render(
704
+ <TestWrapper>
705
+ <ComponentToRender {...props} />
706
+ </TestWrapper>
707
+ );
708
+
709
+ expect(screen.getByTestId('sms-content')).toHaveTextContent('Raw {{var}} template');
710
+ });
711
+
712
+ it('on SMS fallback tab, applies varmap slot substitution when varmap entries exist and no resolved text', () => {
713
+ // getFallbackResolvedContent key format: `${fullToken}_${segmentIndex}`
714
+ // 'Hello {{name}}' → segments ['Hello ', '{{name}}'], so {{name}} is at index 1 → key '{{name}}_1'
715
+ const props = {
716
+ ...defaultProps,
717
+ channel: CHANNELS.RCS,
718
+ content: { rcsTitle: 'Hello RCS' },
719
+ smsFallbackContent: {
720
+ content: 'Hello {{name}}',
721
+ templateContent: 'Hello {{name}}',
722
+ rcsSmsFallbackVarMapped: { '{{name}}_1': 'World' },
723
+ },
724
+ smsFallbackResolvedText: undefined,
725
+ activePreviewTab: PREVIEW_TAB_SMS_FALLBACK,
726
+ onPreviewTabChange: jest.fn(),
727
+ };
728
+
729
+ render(
730
+ <TestWrapper>
731
+ <ComponentToRender {...props} />
732
+ </TestWrapper>
733
+ );
734
+
735
+ expect(screen.getByTestId('sms-content')).toHaveTextContent('Hello World');
736
+ });
737
+
738
+ it('on SMS fallback tab, resolved text takes priority over varmap entries', () => {
739
+ const props = {
740
+ ...defaultProps,
741
+ channel: CHANNELS.RCS,
742
+ content: { rcsTitle: 'Hello RCS' },
743
+ smsFallbackContent: {
744
+ content: 'Hello {{name}}',
745
+ templateContent: 'Hello {{name}}',
746
+ rcsSmsFallbackVarMapped: { '{{name}}_1': 'World' },
747
+ },
748
+ smsFallbackResolvedText: 'Hello John',
749
+ activePreviewTab: PREVIEW_TAB_SMS_FALLBACK,
750
+ onPreviewTabChange: jest.fn(),
751
+ };
752
+
753
+ render(
754
+ <TestWrapper>
755
+ <ComponentToRender {...props} />
756
+ </TestWrapper>
757
+ );
758
+
759
+ // resolvedText wins over varmap substitution
760
+ expect(screen.getByTestId('sms-content')).toHaveTextContent('Hello John');
761
+ });
762
+ });
565
763
  });
566
764
 
567
765
  describe('Channel Routing - INAPP', () => {
@@ -30,23 +30,28 @@ jest.mock('@capillarytech/cap-ui-library/CapNotification', () => ({
30
30
  }));
31
31
 
32
32
  // Mock child components - must use React.createElement to avoid hoisting issues
33
+ let lastLeftPanelContentProps = null;
33
34
  jest.mock('../LeftPanelContent', () => {
34
35
  // eslint-disable-next-line global-require, import/no-extraneous-dependencies
35
36
  const ReactLib = require('react');
36
37
  return {
37
38
  __esModule: true,
38
- default: function MockLeftPanelContent() {
39
- return ReactLib.createElement('div', { 'data-testid': 'left-panel' }, 'Left Panel');
39
+ default: function MockLeftPanelContent(props) {
40
+ lastLeftPanelContentProps = props;
41
+ const editorEl = props.renderCustomValuesEditor ? props.renderCustomValuesEditor() : null;
42
+ return ReactLib.createElement('div', { 'data-testid': 'left-panel' }, 'Left Panel', editorEl);
40
43
  },
41
44
  };
42
45
  });
43
46
 
47
+ let lastCustomValuesEditorProps = null;
44
48
  jest.mock('../CustomValuesEditor', () => {
45
49
  // eslint-disable-next-line global-require, import/no-extraneous-dependencies
46
50
  const ReactLib = require('react');
47
51
  return {
48
52
  __esModule: true,
49
- default: function MockCustomValuesEditor() {
53
+ default: function MockCustomValuesEditor(props) {
54
+ lastCustomValuesEditorProps = props;
50
55
  return ReactLib.createElement('div', { 'data-testid': 'custom-values-editor' }, 'Custom Values Editor');
51
56
  },
52
57
  };
@@ -191,6 +196,8 @@ describe('CommonTestAndPreview', () => {
191
196
  beforeEach(() => {
192
197
  jest.clearAllMocks();
193
198
  lastSendTestMessageProps = null;
199
+ lastLeftPanelContentProps = null;
200
+ lastCustomValuesEditorProps = null;
194
201
  // Reset all mock function implementations
195
202
  Object.values(mockActions).forEach((mockFn) => {
196
203
  if (jest.isMockFunction(mockFn)) {
@@ -245,6 +252,25 @@ describe('CommonTestAndPreview', () => {
245
252
  });
246
253
  });
247
254
 
255
+ it('should call getSenderDetailsRequested for RCS and SMS when channel is RCS', async () => {
256
+ render(
257
+ <TestWrapper>
258
+ <CommonTestAndPreview {...defaultProps} channel={CHANNELS.RCS} />
259
+ </TestWrapper>
260
+ );
261
+ await waitFor(() => {
262
+ expect(mockActions.getSenderDetailsRequested).toHaveBeenCalledTimes(2);
263
+ expect(mockActions.getSenderDetailsRequested).toHaveBeenNthCalledWith(1, {
264
+ channel: CHANNELS.RCS,
265
+ orgUnitId: -1,
266
+ });
267
+ expect(mockActions.getSenderDetailsRequested).toHaveBeenNthCalledWith(2, {
268
+ channel: CHANNELS.SMS,
269
+ orgUnitId: -1,
270
+ });
271
+ });
272
+ });
273
+
248
274
  it('should not call getSenderDetailsRequested when channel is INAPP', async () => {
249
275
  render(
250
276
  <TestWrapper>
@@ -303,7 +329,7 @@ describe('CommonTestAndPreview', () => {
303
329
  });
304
330
  expect(lastSendTestMessageProps).toBeDefined();
305
331
  expect(lastSendTestMessageProps.deliverySettings).toBeDefined();
306
- expect(lastSendTestMessageProps.senderDetailsOptions).toEqual(senderDetailsByChannel[CHANNELS.SMS]);
332
+ expect(lastSendTestMessageProps.senderDetailsByChannel).toEqual(senderDetailsByChannel);
307
333
  expect(lastSendTestMessageProps.wecrmAccounts).toEqual([]);
308
334
  expect(typeof lastSendTestMessageProps.onSaveDeliverySettings).toBe('function');
309
335
  expect(lastSendTestMessageProps.isLoadingSenderDetails).toBe(false);
@@ -3475,4 +3501,107 @@ describe('CommonTestAndPreview', () => {
3475
3501
  });
3476
3502
  });
3477
3503
  });
3504
+
3505
+ describe('SMS DLT and mustache tag discrimination (smsTemplateHasMustacheTags / buildSyntheticSmsMustacheTags)', () => {
3506
+ it('should return no tags for SMS content that contains only DLT {#var#} tokens', async () => {
3507
+ render(
3508
+ <TestWrapper>
3509
+ <CommonTestAndPreview
3510
+ {...defaultProps}
3511
+ channel={CHANNELS.SMS}
3512
+ formData={{ 0: { 'sms-editor': 'Order {#orderId#} is confirmed' } }}
3513
+ extractedTags={[]}
3514
+ />
3515
+ </TestWrapper>
3516
+ );
3517
+
3518
+ await waitFor(() => expect(lastLeftPanelContentProps).not.toBeNull());
3519
+
3520
+ // smsTemplateHasMustacheTags returns false for DLT-only → extractedTags is []
3521
+ expect(lastLeftPanelContentProps.extractedTags).toEqual([]);
3522
+ });
3523
+
3524
+ it('should build synthetic tags from {{mustache}} tokens, excluding DLT {#var#} tokens', async () => {
3525
+ render(
3526
+ <TestWrapper>
3527
+ <CommonTestAndPreview
3528
+ {...defaultProps}
3529
+ channel={CHANNELS.SMS}
3530
+ formData={{ 0: { 'sms-editor': 'Hi {{name}}, order {#orderId#} shipped' } }}
3531
+ extractedTags={[]}
3532
+ />
3533
+ </TestWrapper>
3534
+ );
3535
+
3536
+ await waitFor(() => expect(lastLeftPanelContentProps).not.toBeNull());
3537
+
3538
+ const tags = lastLeftPanelContentProps.extractedTags;
3539
+ // Only {{name}} should become a tag — {#orderId#} must be excluded
3540
+ expect(tags).toHaveLength(1);
3541
+ expect(tags[0].name).toBe('name');
3542
+ });
3543
+
3544
+ it('should use API-extracted tags when present instead of building synthetic ones', async () => {
3545
+ const apiTags = [{ name: 'firstName', metaData: { userDriven: true }, children: [] }];
3546
+ render(
3547
+ <TestWrapper>
3548
+ <CommonTestAndPreview
3549
+ {...defaultProps}
3550
+ channel={CHANNELS.SMS}
3551
+ formData={{ 0: { 'sms-editor': 'Hi {{firstName}}' } }}
3552
+ extractedTags={apiTags}
3553
+ />
3554
+ </TestWrapper>
3555
+ );
3556
+
3557
+ await waitFor(() => expect(lastLeftPanelContentProps).not.toBeNull());
3558
+
3559
+ // API tags take priority over buildSyntheticSmsMustacheTags
3560
+ expect(lastLeftPanelContentProps.extractedTags).toEqual(apiTags);
3561
+ });
3562
+ });
3563
+
3564
+ describe('handleDiscardCustomValues — preview reset', () => {
3565
+ it('should call updatePreviewRequested when handleDiscardCustomValues is invoked', async () => {
3566
+ render(
3567
+ <TestWrapper>
3568
+ <CommonTestAndPreview
3569
+ {...defaultProps}
3570
+ channel={CHANNELS.SMS}
3571
+ formData={{ 0: { 'sms-editor': 'Hi {{name}}' } }}
3572
+ />
3573
+ </TestWrapper>
3574
+ );
3575
+
3576
+ await waitFor(() => expect(lastCustomValuesEditorProps).not.toBeNull());
3577
+
3578
+ lastCustomValuesEditorProps.handleDiscardCustomValues();
3579
+
3580
+ expect(mockActions.updatePreviewRequested).toHaveBeenCalledTimes(1);
3581
+ });
3582
+
3583
+ it('should call updatePreviewRequested with RCS channel when handleDiscardCustomValues is invoked for RCS with fallback', async () => {
3584
+ render(
3585
+ <TestWrapper>
3586
+ <CommonTestAndPreview
3587
+ {...defaultProps}
3588
+ channel={CHANNELS.RCS}
3589
+ smsFallbackContent={{
3590
+ templateContent: 'Fallback {{name}}',
3591
+ content: 'Fallback {{name}}',
3592
+ }}
3593
+ formData={{}}
3594
+ />
3595
+ </TestWrapper>
3596
+ );
3597
+
3598
+ await waitFor(() => expect(lastCustomValuesEditorProps).not.toBeNull());
3599
+
3600
+ lastCustomValuesEditorProps.handleDiscardCustomValues();
3601
+
3602
+ // updatePreviewRequested is called; syncSmsFallbackPreview (which hits Api directly) is NOT called
3603
+ expect(mockActions.updatePreviewRequested).toHaveBeenCalledTimes(1);
3604
+ });
3605
+ });
3478
3606
  });
3607
+
@@ -0,0 +1,67 @@
1
+ import {
2
+ normalizePreviewApiPayload,
3
+ extractPreviewFromLiquidResponse,
4
+ getSmsFallbackTextForTagExtraction,
5
+ } from '../previewApiUtils';
6
+ import { RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../constants';
7
+
8
+ describe('previewApiUtils', () => {
9
+ describe('normalizePreviewApiPayload', () => {
10
+ it('maps messageBody to resolvedBody when resolvedBody is missing', () => {
11
+ expect(normalizePreviewApiPayload({ messageBody: 'Hello' }).resolvedBody).toBe('Hello');
12
+ });
13
+ });
14
+
15
+ describe('extractPreviewFromLiquidResponse', () => {
16
+ it('accepts nested data shape', () => {
17
+ const r = { data: { resolvedBody: 'ok' } };
18
+ expect(extractPreviewFromLiquidResponse(r)).toEqual({ resolvedBody: 'ok' });
19
+ });
20
+
21
+ it('accepts top-level preview (typical SMS)', () => {
22
+ const r = { messageBody: 'sms text' };
23
+ expect(extractPreviewFromLiquidResponse(r)).toMatchObject({
24
+ messageBody: 'sms text',
25
+ resolvedBody: 'sms text',
26
+ });
27
+ });
28
+
29
+ it('returns null when errors array is non-empty', () => {
30
+ expect(extractPreviewFromLiquidResponse({ errors: [{ code: 1 }] })).toBeNull();
31
+ });
32
+ });
33
+
34
+ describe('getSmsFallbackTextForTagExtraction', () => {
35
+ it('returns empty string when context is missing or has no template body', () => {
36
+ expect(getSmsFallbackTextForTagExtraction(null)).toBe('');
37
+ expect(getSmsFallbackTextForTagExtraction({})).toBe('');
38
+ expect(getSmsFallbackTextForTagExtraction({ templateContent: '', content: '' })).toBe('');
39
+ });
40
+
41
+ it('returns raw template when rcsSmsFallbackVarMapped is absent or empty', () => {
42
+ const raw = '{{optout}} {{fullname}} test SMS';
43
+ expect(getSmsFallbackTextForTagExtraction({ templateContent: raw })).toBe(raw);
44
+ expect(getSmsFallbackTextForTagExtraction({
45
+ content: raw,
46
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: {},
47
+ })).toBe(raw);
48
+ });
49
+
50
+ it('prefers templateContent over content when both are set', () => {
51
+ expect(getSmsFallbackTextForTagExtraction({
52
+ templateContent: 'A',
53
+ content: 'B',
54
+ })).toBe('A');
55
+ });
56
+
57
+ it('applies VarSegment slot map so preview / meta payloads are not stale vs raw template', () => {
58
+ const rawTemplateWithSlots = '{{optout}} tail';
59
+ const rcsSmsFallbackVarMappedSlots = { '{{optout}}_0': 'STOP' };
60
+ const resolvedFallbackText = getSmsFallbackTextForTagExtraction({
61
+ templateContent: rawTemplateWithSlots,
62
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: rcsSmsFallbackVarMappedSlots,
63
+ });
64
+ expect(resolvedFallbackText).toBe('STOP tail');
65
+ });
66
+ });
67
+ });
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import {
8
- call, put, takeLatest, all,
8
+ call, put, takeLatest, takeEvery, all, take, fork, cancel,
9
9
  } from 'redux-saga/effects';
10
10
  import {
11
11
  searchCustomersSaga,
@@ -1333,9 +1333,26 @@ describe('CommonTestAndPreview Sagas', () => {
1333
1333
  it('should watch for GET_SENDER_DETAILS_REQUESTED', () => {
1334
1334
  const generator = watchGetSenderDetails();
1335
1335
  expect(generator.next().value).toEqual(
1336
- takeLatest('app/CommonTestAndPreview/GET_SENDER_DETAILS_REQUESTED', getSenderDetailsSaga)
1336
+ take('app/CommonTestAndPreview/GET_SENDER_DETAILS_REQUESTED')
1337
+ );
1338
+ const firstAction = {
1339
+ payload: { channel: 'RCS', orgUnitId: 10 },
1340
+ };
1341
+ expect(generator.next(firstAction).value).toEqual(
1342
+ fork(getSenderDetailsSaga, firstAction)
1343
+ );
1344
+ expect(generator.next({ '@@redux-saga/TASK': true }).value).toEqual(
1345
+ take('app/CommonTestAndPreview/GET_SENDER_DETAILS_REQUESTED')
1346
+ );
1347
+ const secondAction = {
1348
+ payload: { channel: 'RCS', orgUnitId: 20 },
1349
+ };
1350
+ expect(generator.next(secondAction).value).toEqual(
1351
+ cancel({ '@@redux-saga/TASK': true })
1352
+ );
1353
+ expect(generator.next().value).toEqual(
1354
+ fork(getSenderDetailsSaga, secondAction)
1337
1355
  );
1338
- expect(generator.next().done).toBe(true);
1339
1356
  });
1340
1357
 
1341
1358
  it('should watch for GET_WECRM_ACCOUNTS_REQUESTED', () => {
@@ -1454,34 +1471,24 @@ describe('CommonTestAndPreview Sagas', () => {
1454
1471
  const action = { payload: {} };
1455
1472
  const generator = getWeCrmAccountsSaga(action);
1456
1473
  generator.next();
1457
- const err = generator.throw(new Error('API failed'));
1458
- expect(err.value).toEqual(
1474
+ const throwResult = generator.throw(new Error('API failed'));
1475
+ expect(throwResult.value).toEqual(
1459
1476
  put({ type: GET_WECRM_ACCOUNTS_FAILURE, payload: { error: 'API failed' } })
1460
1477
  );
1461
1478
  expect(generator.next().done).toBe(true);
1462
1479
  });
1463
1480
  });
1464
1481
 
1465
- describe('commonTestAndPreviewSaga', () => {
1466
- it('should fork all watcher sagas', () => {
1467
- const generator = commonTestAndPreviewSaga();
1482
+ it('should initialize all watcher sagas', () => {
1483
+ const generator = commonTestAndPreviewSaga();
1468
1484
 
1469
- expect(generator.next().value).toEqual(
1470
- all([
1471
- watchSearchCustomers(),
1472
- watchExtractTags(),
1473
- watchUpdatePreview(),
1474
- watchSendTestMessage(),
1475
- watchFetchTestCustomers(),
1476
- watchFetchTestGroups(),
1477
- watchCreateMessageMeta(),
1478
- watchGetPrefilledValues(),
1479
- watchGetSenderDetails(),
1480
- watchGetWeCrmAccounts(),
1481
- ])
1482
- );
1485
+ const effect = generator.next().value;
1483
1486
 
1484
- expect(generator.next().done).toBe(true);
1485
- });
1487
+ // redux-saga v0.x stores the array under effect[ALL] key; v1.x uses effect.payload
1488
+ const effectData = effect?.payload || effect?.ALL;
1489
+ const watchers = Array.isArray(effectData) ? effectData : Object.values(effectData || {});
1490
+ expect(Array.isArray(watchers)).toBe(true);
1491
+ expect(watchers.length).toBeGreaterThan(0);
1492
+ expect(generator.next().done).toBe(true);
1486
1493
  });
1487
1494
  });
@@ -37,6 +37,7 @@ import { createStructuredSelector } from 'reselect';
37
37
  import { CAP_SPACE_12, CAP_SPACE_08, FONT_COLOR_05, FONT_COLOR_04 } from '@capillarytech/cap-ui-library/styled/variables';
38
38
  import UnifiedPreview from '../CommonTestAndPreview/UnifiedPreview';
39
39
  import { ANDROID } from '../CommonTestAndPreview/constants';
40
+ import TemplatePreview from '../TemplatePreview';
40
41
  import TagList from '../../v2Containers/TagList';
41
42
  import CapTagListWithInput from '../CapTagListWithInput';
42
43
  import SlideBox from '../SlideBox';
@@ -3053,8 +3054,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3053
3054
  userLocale={this.props.userLocale}
3054
3055
  selectedOfferDetails={this.props.selectedOfferDetails}
3055
3056
  eventContextTags={this.props?.eventContextTags}
3056
- restrictPersonalization={this.props.restrictPersonalization}
3057
3057
  waitEventContextTags={this.props?.waitEventContextTags}
3058
+ restrictPersonalization={this.props.restrictPersonalization}
3058
3059
  />
3059
3060
  </CapColumn>
3060
3061
  );
@@ -3758,8 +3759,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3758
3759
  selectedOfferDetails={this.props.selectedOfferDetails}
3759
3760
  channel={channel}
3760
3761
  eventContextTags={this.props?.eventContextTags}
3761
- restrictPersonalization={this.props.restrictPersonalization}
3762
3762
  waitEventContextTags={this.props?.waitEventContextTags}
3763
+ restrictPersonalization={this.props.restrictPersonalization}
3763
3764
  />
3764
3765
  </CapColumn>
3765
3766
  );
@@ -4417,8 +4418,8 @@ FormBuilder.defaultProps = {
4417
4418
  userLocale: localStorage.getItem('jlocale') || 'en',
4418
4419
  showLiquidErrorInFooter: () => {},
4419
4420
  metaDataStatus: "",
4420
- waitEventContextTags: {},
4421
4421
  isTestAndPreviewMode: false, // Default to false to maintain existing behavior
4422
+ waitEventContextTags: {},
4422
4423
  };
4423
4424
 
4424
4425
  FormBuilder.propTypes = {
@@ -4466,7 +4467,7 @@ FormBuilder.propTypes = {
4466
4467
  type: PropTypes.string.isRequired,
4467
4468
  isEmailLoading: PropTypes.bool.isRequired,
4468
4469
  moduleType: PropTypes.string.isRequired,
4469
- showLiquidErrorInFooter: PropTypes.bool.isRequired,
4470
+ showLiquidErrorInFooter: PropTypes.func.isRequired,
4470
4471
  eventContextTags: PropTypes.array.isRequired,
4471
4472
  waitEventContextTags: PropTypes.object,
4472
4473
  forwardedTags: PropTypes.object.isRequired,
@@ -0,0 +1,91 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import CreativesContainer from '../../v2Containers/CreativesContainer';
4
+ import CapPageSpinner from '../CapPageSpinner';
5
+ import {
6
+ EMBEDDED_SMS_CREATIVES_LOCATION,
7
+ SMS_FALLBACK_CHANNEL_KEY,
8
+ SMS_FALLBACK_CREATIVE_EDITOR,
9
+ SMS_FALLBACK_ENABLE_NEW_CHANNELS,
10
+ } from './constants';
11
+
12
+ /**
13
+ * Reuse the exact embedded CreativesContainer SMS flow (same as normal SMS create).
14
+ * Avoid overriding Templates with local config so "Create new" follows built-in createTemplate path.
15
+ */
16
+ export function SmsFallbackLocalSelector({
17
+ hidden,
18
+ fetchDetailsLoading,
19
+ templateList,
20
+ channelsToHide,
21
+ smsRegister,
22
+ onCloseCreatives,
23
+ onSelectTemplate,
24
+ filterContent,
25
+ location,
26
+ /** Required when user completes embedded SMS create/edit — `CreativesContainer` calls this on save (see `processCentralCommsMetaId`). */
27
+ getCreativesData,
28
+ }) {
29
+ const rootClassName = [
30
+ 'sms-fallback-selector',
31
+ hidden ? 'sms-fallback-selector--visually-hidden' : '',
32
+ ]
33
+ .filter(Boolean)
34
+ .join(' ');
35
+
36
+ return (
37
+ <div
38
+ className={rootClassName}
39
+ aria-hidden={hidden}
40
+ inert={hidden ? '' : undefined}
41
+ >
42
+ {fetchDetailsLoading && (
43
+ <div className="sms-fallback-selector__loading" aria-busy="true" aria-live="polite">
44
+ <CapPageSpinner spinning />
45
+ </div>
46
+ )}
47
+ <CreativesContainer
48
+ creativesMode="create"
49
+ location={location || EMBEDDED_SMS_CREATIVES_LOCATION}
50
+ templateData={null}
51
+ handleCloseCreatives={onCloseCreatives}
52
+ isFullMode={false}
53
+ smsRegister={smsRegister}
54
+ editor={SMS_FALLBACK_CREATIVE_EDITOR}
55
+ enableNewChannels={SMS_FALLBACK_ENABLE_NEW_CHANNELS}
56
+ channel={SMS_FALLBACK_CHANNEL_KEY}
57
+ channelsToHide={channelsToHide}
58
+ selectedBadges={[]}
59
+ localTemplatesConfig={{
60
+ useLocalTemplates: true,
61
+ localTemplates: templateList.templates,
62
+ localTemplatesLoading: templateList.loading,
63
+ localTemplatesFilterContent: filterContent,
64
+ localTemplatesOnPageChange: templateList.loadMore,
65
+ localTemplatesUseSkeleton: true,
66
+ }}
67
+ onSelectTemplate={onSelectTemplate}
68
+ getCreativesData={getCreativesData}
69
+ />
70
+ </div>
71
+ );
72
+ }
73
+
74
+ SmsFallbackLocalSelector.propTypes = {
75
+ hidden: PropTypes.bool,
76
+ fetchDetailsLoading: PropTypes.bool,
77
+ templateList: PropTypes.shape({
78
+ templates: PropTypes.array,
79
+ loading: PropTypes.bool,
80
+ loadMore: PropTypes.func,
81
+ }).isRequired,
82
+ channelsToHide: PropTypes.arrayOf(PropTypes.string),
83
+ smsRegister: PropTypes.any,
84
+ onCloseCreatives: PropTypes.func.isRequired,
85
+ onSelectTemplate: PropTypes.func.isRequired,
86
+ filterContent: PropTypes.node,
87
+ location: PropTypes.object,
88
+ getCreativesData: PropTypes.func.isRequired,
89
+ };
90
+
91
+ export default SmsFallbackLocalSelector;