@capillarytech/creatives-library 9.0.73-alpha.sprint-testpreview.22 → 9.1.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 (66) hide show
  1. package/package.json +1 -1
  2. package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +58 -1
  3. package/v2Components/CapTagListWithInput/index.js +64 -35
  4. package/v2Components/CapTagListWithInput/index.scss +9 -0
  5. package/v2Components/CommonTestAndPreview/UnifiedPreview/EmailPreviewContent.js +8 -0
  6. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +7 -0
  7. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +1 -0
  8. package/v2Components/CommonTestAndPreview/actions.js +1 -1
  9. package/v2Components/CommonTestAndPreview/index.js +36 -75
  10. package/v2Components/CommonTestAndPreview/sagas.js +2 -1
  11. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/EmailPreviewContent.test.js +27 -0
  12. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +40 -0
  13. package/v2Components/CommonTestAndPreview/tests/index.test.js +92 -97
  14. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +24 -0
  15. package/v2Components/CommonTestAndPreview/tests/utils.test.js +0 -66
  16. package/v2Components/CommonTestAndPreview/utils.js +0 -14
  17. package/v2Components/EmailMobilePreview/index.js +8 -1
  18. package/v2Components/EmailMobilePreview/index.scss +12 -3
  19. package/v2Components/EmailMobilePreview/tests/index.test.js +31 -0
  20. package/v2Components/EmailPreviewV2/_emailPreviewV2.scss +37 -5
  21. package/v2Components/EmailPreviewV2/index.js +18 -6
  22. package/v2Components/EmailPreviewV2/messages.js +4 -0
  23. package/v2Components/EmailPreviewV2/tests/index.test.js +43 -0
  24. package/v2Components/FormBuilder/Classic.js +134 -14
  25. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +9 -0
  26. package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +11 -12
  27. package/v2Components/FormBuilder/Functional/tests/mpush.singlePlatform.test.js +144 -0
  28. package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +17 -5
  29. package/v2Components/FormBuilder/_formBuilder.scss +21 -0
  30. package/v2Components/FormBuilder/messages.js +12 -0
  31. package/v2Components/FormBuilder/tests/mpush.characterization.test.js +45 -0
  32. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +2 -1
  33. package/v2Containers/CommunicationFlow/Tests/constants.test.js +4 -4
  34. package/v2Containers/CommunicationFlow/constants.js +1 -0
  35. package/v2Containers/CommunicationFlow/utils/Tests/mapCommDefinitionToStepData.test.js +2 -1
  36. package/v2Containers/CommunicationFlow/utils/mapCommDefinitionToStepData.js +1 -0
  37. package/v2Containers/CreativesContainer/SlideBoxContent.js +0 -2
  38. package/v2Containers/CreativesContainer/index.js +7 -1
  39. package/v2Containers/CreativesContainer/tests/index.test.js +81 -0
  40. package/v2Containers/Email/index.js +31 -4
  41. package/v2Containers/Email/initialSchema.js +30 -0
  42. package/v2Containers/Email/messages.js +8 -0
  43. package/v2Containers/Email/tests/index.test.js +107 -0
  44. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +172 -8
  45. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +264 -5
  46. package/v2Containers/EmailWrapper/components/_emailHTMLEditor.scss +29 -0
  47. package/v2Containers/EmailWrapper/constants.js +5 -0
  48. package/v2Containers/EmailWrapper/messages.js +24 -0
  49. package/v2Containers/MobilePush/Create/constants.js +1 -0
  50. package/v2Containers/MobilePush/Create/index.js +2 -2
  51. package/v2Containers/MobilePush/Create/test/contentValidity.test.js +58 -0
  52. package/v2Containers/MobilePush/Edit/index.js +2 -2
  53. package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +17 -0
  54. package/v2Containers/MobilePush/commonMethods.js +7 -8
  55. package/v2Containers/Rcs/index.js +1 -27
  56. package/v2Containers/TagList/index.js +7 -0
  57. package/v2Containers/TagList/tests/TagList.test.js +42 -0
  58. package/v2Containers/Templates/index.js +4 -9
  59. package/v2Containers/Templates/tests/index.test.js +35 -107
  60. package/v2Containers/Viber/index.scss +31 -0
  61. package/v2Containers/Whatsapp/index.js +1 -18
  62. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +2 -2
  63. package/v2Containers/Zalo/index.js +0 -18
  64. package/v2Containers/Rcs/tests/testAndPreviewFromListing.test.js +0 -132
  65. package/v2Containers/Whatsapp/tests/testAndPreviewFromListing.test.js +0 -91
  66. package/v2Containers/Zalo/tests/testAndPreviewFromListing.test.js +0 -98
@@ -481,6 +481,33 @@ describe('EmailPreviewContent', () => {
481
481
 
482
482
  expect(container.querySelector('.email-content')).toBeTruthy();
483
483
  });
484
+
485
+ it('should not render a pre-header row when preheader is not provided', () => {
486
+ const { container } = render(
487
+ <TestWrapper>
488
+ <ComponentToRender {...defaultProps} />
489
+ </TestWrapper>
490
+ );
491
+
492
+ expect(container.querySelector('.email-preheader')).toBeNull();
493
+ });
494
+
495
+ it('should render the pre-header text when provided', () => {
496
+ const props = {
497
+ ...defaultProps,
498
+ preheader: 'Unlock 20% off your next order',
499
+ };
500
+
501
+ const { container } = render(
502
+ <TestWrapper>
503
+ <ComponentToRender {...props} />
504
+ </TestWrapper>
505
+ );
506
+
507
+ const preheaderElement = container.querySelector('.email-preheader');
508
+ expect(preheaderElement).toBeTruthy();
509
+ expect(preheaderElement.textContent).toBe('Unlock 20% off your next order');
510
+ });
484
511
  });
485
512
 
486
513
  describe('Device Handling', () => {
@@ -46,6 +46,7 @@ jest.mock('../../UnifiedPreview/EmailPreviewContent', () => ({
46
46
  <div data-testid="email-preview">
47
47
  <div data-testid="email-content">{props.content}</div>
48
48
  <div data-testid="email-subject">{props.subject}</div>
49
+ <div data-testid="email-preheader">{props.preheader}</div>
49
50
  <div data-testid="email-device">{props.device}</div>
50
51
  </div>
51
52
  ),
@@ -288,6 +289,45 @@ describe('UnifiedPreview', () => {
288
289
  expect(screen.getByTestId('email-subject')).toHaveTextContent('Email Subject');
289
290
  });
290
291
 
292
+ it('should pass resolvedPreheader through as the preheader prop', () => {
293
+ const props = {
294
+ ...defaultProps,
295
+ channel: CHANNELS.EMAIL,
296
+ content: {
297
+ resolvedBody: '<html>Email Content</html>',
298
+ resolvedTitle: 'Email Subject',
299
+ resolvedPreheader: 'Unlock 20% off',
300
+ },
301
+ };
302
+
303
+ render(
304
+ <TestWrapper>
305
+ <ComponentToRender {...props} />
306
+ </TestWrapper>
307
+ );
308
+
309
+ expect(screen.getByTestId('email-preheader')).toHaveTextContent('Unlock 20% off');
310
+ });
311
+
312
+ it('should default preheader to an empty string when resolvedPreheader is absent', () => {
313
+ const props = {
314
+ ...defaultProps,
315
+ channel: CHANNELS.EMAIL,
316
+ content: {
317
+ resolvedBody: '<html>Email Content</html>',
318
+ resolvedTitle: 'Email Subject',
319
+ },
320
+ };
321
+
322
+ render(
323
+ <TestWrapper>
324
+ <ComponentToRender {...props} />
325
+ </TestWrapper>
326
+ );
327
+
328
+ expect(screen.getByTestId('email-preheader')).toHaveTextContent('');
329
+ });
330
+
291
331
  it('should handle empty resolvedBody', () => {
292
332
  const props = {
293
333
  ...defaultProps,
@@ -7,9 +7,7 @@
7
7
 
8
8
  import React from 'react';
9
9
  import PropTypes from 'prop-types';
10
- import {
11
- render, screen, waitFor, act,
12
- } from '@testing-library/react';
10
+ import { render, screen, waitFor } from '@testing-library/react';
13
11
  import { IntlProvider } from 'react-intl';
14
12
  import CommonTestAndPreview from '../index';
15
13
  import { CHANNELS, MEDIA_TYPE_CAROUSEL } from '../constants';
@@ -72,14 +70,12 @@ jest.mock('../SendTestMessage', () => {
72
70
  };
73
71
  });
74
72
 
75
- let lastPreviewSectionProps = null;
76
73
  jest.mock('../PreviewSection', () => {
77
74
  // eslint-disable-next-line global-require, import/no-extraneous-dependencies
78
75
  const ReactLib = require('react');
79
76
  return {
80
77
  __esModule: true,
81
- default: function MockPreviewSection(props) {
82
- lastPreviewSectionProps = props;
78
+ default: function MockPreviewSection() {
83
79
  return ReactLib.createElement('div', { 'data-testid': 'preview-section' }, 'Preview Section');
84
80
  },
85
81
  };
@@ -203,7 +199,6 @@ describe('CommonTestAndPreview', () => {
203
199
  lastSendTestMessageProps = null;
204
200
  lastLeftPanelContentProps = null;
205
201
  lastCustomValuesEditorProps = null;
206
- lastPreviewSectionProps = null;
207
202
  // Reset all mock function implementations
208
203
  Object.values(mockActions).forEach((mockFn) => {
209
204
  if (jest.isMockFunction(mockFn)) {
@@ -541,6 +536,45 @@ describe('CommonTestAndPreview', () => {
541
536
  expect(initialPayload.emailDeliverySettings.channelSettings.channel).toBe(CHANNELS.EMAIL);
542
537
  });
543
538
 
539
+ it('should send the pre-header as messagePreHeader alongside messageSubject/messageBody for EMAIL', async () => {
540
+ render(
541
+ <TestWrapper>
542
+ <CommonTestAndPreview
543
+ {...defaultProps}
544
+ channel={CHANNELS.EMAIL}
545
+ formData={{
546
+ ...defaultProps.formData,
547
+ 'template-preheader': 'Test Preheader',
548
+ }}
549
+ />
550
+ </TestWrapper>
551
+ );
552
+ await waitFor(() => {
553
+ expect(lastSendTestMessageProps).toBeDefined();
554
+ expect(typeof lastSendTestMessageProps.handleSendTestMessage).toBe('function');
555
+ });
556
+ lastSendTestMessageProps.handleSendTestMessage();
557
+ expect(mockActions.createMessageMetaRequested).toHaveBeenCalled();
558
+ const [initialPayload] = mockActions.createMessageMetaRequested.mock.calls[0];
559
+ expect(initialPayload.emailMessageContent.messageSubject).toBe('Test Subject');
560
+ expect(initialPayload.emailMessageContent.messagePreHeader).toBe('Test Preheader');
561
+ });
562
+
563
+ it('should default messagePreHeader to an empty string when no pre-header is present for EMAIL', async () => {
564
+ render(
565
+ <TestWrapper>
566
+ <CommonTestAndPreview {...defaultProps} channel={CHANNELS.EMAIL} />
567
+ </TestWrapper>
568
+ );
569
+ await waitFor(() => {
570
+ expect(lastSendTestMessageProps).toBeDefined();
571
+ expect(typeof lastSendTestMessageProps.handleSendTestMessage).toBe('function');
572
+ });
573
+ lastSendTestMessageProps.handleSendTestMessage();
574
+ const [initialPayload] = mockActions.createMessageMetaRequested.mock.calls[0];
575
+ expect(initialPayload.emailMessageContent.messagePreHeader).toBe('');
576
+ });
577
+
544
578
  it('should include delivery settings in payload when handleSendTestMessage is called for SMS', async () => {
545
579
  render(
546
580
  <TestWrapper>
@@ -2982,6 +3016,37 @@ describe('CommonTestAndPreview', () => {
2982
3016
  expect(mockActions.extractTagsRequested).toHaveBeenCalled();
2983
3017
  });
2984
3018
 
3019
+ it('should pass template-preheader separately (not merged into templateSubject) in the extraction payload for EMAIL', async () => {
3020
+ const props = {
3021
+ ...defaultProps,
3022
+ channel: CHANNELS.EMAIL,
3023
+ formData: {
3024
+ 'template-subject': 'Hello {{name}}',
3025
+ 'template-preheader': 'Enjoy {{discount}} off, {{name}}',
3026
+ 0: {
3027
+ activeTab: 'base',
3028
+ base: {
3029
+ 'template-content': '<html>Your order {{orderId}} is ready</html>',
3030
+ },
3031
+ },
3032
+ },
3033
+ };
3034
+
3035
+ render(
3036
+ <TestWrapper>
3037
+ <CommonTestAndPreview {...props} />
3038
+ </TestWrapper>
3039
+ );
3040
+
3041
+ expect(mockActions.extractTagsRequested).toHaveBeenCalled();
3042
+ const call = mockActions.extractTagsRequested.mock.calls.find(
3043
+ ([payload]) => payload?.templatePreheader?.includes('{{discount}}')
3044
+ );
3045
+ expect(call).toBeDefined();
3046
+ expect(call[0].templateSubject).toBe('Hello {{name}}');
3047
+ expect(call[0].templatePreheader).toBe('Enjoy {{discount}} off, {{name}}');
3048
+ });
3049
+
2985
3050
  it('should extract tags from SMS content', async () => {
2986
3051
  const props = {
2987
3052
  ...defaultProps,
@@ -3764,17 +3829,16 @@ describe('CommonTestAndPreview', () => {
3764
3829
  expect(mockActions.updatePreviewRequested).toHaveBeenCalledTimes(1);
3765
3830
  });
3766
3831
 
3767
- it('should call updatePreviewRequested with RCS channel when handleDiscardCustomValues is invoked for RCS with fallback', async () => {
3832
+ it('should include messageSubtitle alongside messageTitle/messageBody in the EMAIL preview payload', async () => {
3768
3833
  render(
3769
3834
  <TestWrapper>
3770
3835
  <CommonTestAndPreview
3771
3836
  {...defaultProps}
3772
- channel={CHANNELS.RCS}
3773
- smsFallbackContent={{
3774
- templateContent: 'Fallback {{name}}',
3775
- content: 'Fallback {{name}}',
3837
+ channel={CHANNELS.EMAIL}
3838
+ formData={{
3839
+ ...defaultProps.formData,
3840
+ 'template-preheader': 'Enjoy {{discount}}',
3776
3841
  }}
3777
- formData={{}}
3778
3842
  />
3779
3843
  </TestWrapper>
3780
3844
  );
@@ -3783,103 +3847,34 @@ describe('CommonTestAndPreview', () => {
3783
3847
 
3784
3848
  lastCustomValuesEditorProps.handleDiscardCustomValues();
3785
3849
 
3786
- // updatePreviewRequested is called; syncSmsFallbackPreview (which hits Api directly) is NOT called
3787
3850
  expect(mockActions.updatePreviewRequested).toHaveBeenCalledTimes(1);
3788
- });
3789
- });
3790
-
3791
- describe('client-side preview fill-in when the preview API reply is not an object', () => {
3792
- const getPreviewContent = () => lastPreviewSectionProps.unifiedPreviewProps.content;
3793
-
3794
- const enterValuesAndUpdatePreview = async (values) => {
3795
- await waitFor(() => expect(lastCustomValuesEditorProps).not.toBeNull());
3796
- act(() => {
3797
- Object.entries(values).forEach(([tag, value]) => lastCustomValuesEditorProps.handleCustomValueChange(tag, value));
3798
- });
3799
- await act(async () => {
3800
- await lastCustomValuesEditorProps.handleUpdatePreview();
3801
- });
3802
- };
3803
-
3804
- const whatsappContent = {
3805
- templateHeader: 'Order for {{1}}',
3806
- templateMsg: 'Hi {{1}}, delivered by {{2}}',
3807
- };
3808
-
3809
- it('fills entered values into the WhatsApp header and message', async () => {
3810
- render(
3811
- <TestWrapper>
3812
- <CommonTestAndPreview
3813
- {...defaultProps}
3814
- channel={CHANNELS.WHATSAPP}
3815
- content={whatsappContent}
3816
- formData={whatsappContent}
3817
- />
3818
- </TestWrapper>
3819
- );
3820
-
3821
- await enterValuesAndUpdatePreview({ 1: 'Sh', 2: 'Monday' });
3822
-
3823
- expect(getPreviewContent().templateHeader).toBe('Order for Sh');
3824
- expect(getPreviewContent().templateMsg).toBe('Hi Sh, delivered by Monday');
3825
- });
3826
-
3827
- it('fills entered values when the preview API returns an object with unresolved {{N}}', async () => {
3828
- render(
3829
- <TestWrapper>
3830
- <CommonTestAndPreview
3831
- {...defaultProps}
3832
- channel={CHANNELS.WHATSAPP}
3833
- content={whatsappContent}
3834
- formData={whatsappContent}
3835
- previewData={{ resolvedBody: JSON.stringify(whatsappContent) }}
3836
- />
3837
- </TestWrapper>
3838
- );
3839
-
3840
- await enterValuesAndUpdatePreview({ 1: 'Shash', 2: 'Name' });
3841
-
3842
- expect(getPreviewContent().templateHeader).toBe('Order for Shash');
3843
- expect(getPreviewContent().templateMsg).toBe('Hi Shash, delivered by Name');
3851
+ const [payload] = mockActions.updatePreviewRequested.mock.calls[0];
3852
+ expect(payload.messageTitle).toBe('Test Subject');
3853
+ expect(payload.messageSubtitle).toBe('Enjoy {{discount}}');
3854
+ expect(payload.messageBody).toBeDefined();
3844
3855
  });
3845
3856
 
3846
- it('restores WhatsApp placeholders after Discard custom values', async () => {
3847
- render(
3848
- <TestWrapper>
3849
- <CommonTestAndPreview
3850
- {...defaultProps}
3851
- channel={CHANNELS.WHATSAPP}
3852
- content={whatsappContent}
3853
- formData={whatsappContent}
3854
- />
3855
- </TestWrapper>
3856
- );
3857
-
3858
- await enterValuesAndUpdatePreview({ 1: 'Sh', 2: 'Monday' });
3859
- act(() => {
3860
- lastCustomValuesEditorProps.handleDiscardCustomValues();
3861
- });
3862
-
3863
- expect(getPreviewContent().templateMsg).toBe('Hi {{1}}, delivered by {{2}}');
3864
- });
3865
-
3866
- it('fills entered values into an RCS standalone card', async () => {
3867
- const rcsContent = { templateHeader: 'Store {{1}}', templateMessage: 'Visit store for {{1}} offer' };
3857
+ it('should call updatePreviewRequested with RCS channel when handleDiscardCustomValues is invoked for RCS with fallback', async () => {
3868
3858
  render(
3869
3859
  <TestWrapper>
3870
3860
  <CommonTestAndPreview
3871
3861
  {...defaultProps}
3872
3862
  channel={CHANNELS.RCS}
3873
- content={rcsContent}
3874
- formData={{ rcsTitle: 'Store {{1}}', rcsDesc: 'Visit store for {{1}} offer' }}
3863
+ smsFallbackContent={{
3864
+ templateContent: 'Fallback {{name}}',
3865
+ content: 'Fallback {{name}}',
3866
+ }}
3867
+ formData={{}}
3875
3868
  />
3876
3869
  </TestWrapper>
3877
3870
  );
3878
3871
 
3879
- await enterValuesAndUpdatePreview({ 1: 'Goog' });
3872
+ await waitFor(() => expect(lastCustomValuesEditorProps).not.toBeNull());
3880
3873
 
3881
- expect(getPreviewContent().templateHeader).toBe('Store Goog');
3882
- expect(getPreviewContent().templateMessage).toBe('Visit store for Goog offer');
3874
+ lastCustomValuesEditorProps.handleDiscardCustomValues();
3875
+
3876
+ // updatePreviewRequested is called; syncSmsFallbackPreview (which hits Api directly) is NOT called
3877
+ expect(mockActions.updatePreviewRequested).toHaveBeenCalledTimes(1);
3883
3878
  });
3884
3879
  });
3885
3880
  });
@@ -224,6 +224,7 @@ describe('CommonTestAndPreview Sagas', () => {
224
224
  expect(generator.next().value).toEqual(
225
225
  call(Api.extractTagsWithMetaData, {
226
226
  messageTitle: 'Hello {{name}}',
227
+ messageSubtitle: '',
227
228
  messageBody: 'Welcome {{email}}',
228
229
  })
229
230
  );
@@ -238,6 +239,25 @@ describe('CommonTestAndPreview Sagas', () => {
238
239
  expect(generator.next().done).toBe(true);
239
240
  });
240
241
 
242
+ it('should pass templatePreheader through as messageSubtitle', () => {
243
+ const action = {
244
+ payload: {
245
+ templateSubject: 'Hello {{name}}',
246
+ templatePreheader: 'Enjoy {{discount}}',
247
+ templateContent: 'Welcome {{email}}',
248
+ },
249
+ };
250
+ const generator = extractTagsSaga(action);
251
+
252
+ expect(generator.next().value).toEqual(
253
+ call(Api.extractTagsWithMetaData, {
254
+ messageTitle: 'Hello {{name}}',
255
+ messageSubtitle: 'Enjoy {{discount}}',
256
+ messageBody: 'Welcome {{email}}',
257
+ })
258
+ );
259
+ });
260
+
241
261
  it('should handle empty templateSubject', () => {
242
262
  const action = {
243
263
  payload: {
@@ -250,6 +270,7 @@ describe('CommonTestAndPreview Sagas', () => {
250
270
  expect(generator.next().value).toEqual(
251
271
  call(Api.extractTagsWithMetaData, {
252
272
  messageTitle: '',
273
+ messageSubtitle: '',
253
274
  messageBody: 'Welcome {{email}}',
254
275
  })
255
276
  );
@@ -268,6 +289,7 @@ describe('CommonTestAndPreview Sagas', () => {
268
289
  expect(generator.next().value).toEqual(
269
290
  call(Api.extractTagsWithMetaData, {
270
291
  messageTitle: 'Hello {{name}}',
292
+ messageSubtitle: '',
271
293
  messageBody: '',
272
294
  })
273
295
  );
@@ -284,6 +306,7 @@ describe('CommonTestAndPreview Sagas', () => {
284
306
  expect(generator.next().value).toEqual(
285
307
  call(Api.extractTagsWithMetaData, {
286
308
  messageTitle: '',
309
+ messageSubtitle: '',
287
310
  messageBody: 'Welcome {{email}}',
288
311
  })
289
312
  );
@@ -301,6 +324,7 @@ describe('CommonTestAndPreview Sagas', () => {
301
324
  expect(generator.next().value).toEqual(
302
325
  call(Api.extractTagsWithMetaData, {
303
326
  messageTitle: 'Hello {{name}}',
327
+ messageSubtitle: '',
304
328
  messageBody: '',
305
329
  })
306
330
  );
@@ -10,9 +10,6 @@ import {
10
10
  hasTrimmedText,
11
11
  getCarouselButtonsWithTitles,
12
12
  getCarouselButtonSlotCount,
13
- mergeSyntheticMustacheTags,
14
- getWhatsappTagExtractionText,
15
- mergeZaloUnfilledParamTags,
16
13
  } from '../utils';
17
14
 
18
15
  describe('CommonTestAndPreview/utils', () => {
@@ -200,67 +197,4 @@ describe('CommonTestAndPreview/utils', () => {
200
197
  expect(getCarouselButtonSlotCount([{ buttons: [{ title: ' ' }] }])).toBe(0);
201
198
  });
202
199
  });
203
-
204
- describe('getWhatsappTagExtractionText', () => {
205
- it('joins header and message', () => {
206
- expect(getWhatsappTagExtractionText({ templateHeader: 'Order {{1}}', templateMsg: 'Hi {{2}}' }))
207
- .toBe('Order {{1}} Hi {{2}}');
208
- });
209
-
210
- it('handles missing fields', () => {
211
- expect(getWhatsappTagExtractionText({ templateMsg: 'Hi {{1}}' })).toBe('Hi {{1}}');
212
- expect(getWhatsappTagExtractionText(undefined)).toBe('');
213
- });
214
-
215
- it('surfaces unmapped WhatsApp slots as tags alongside API tags', () => {
216
- const apiTags = [{ name: 'adv', metaData: { userDriven: false }, children: [] }];
217
- const text = getWhatsappTagExtractionText({
218
- templateHeader: 'Order {{1}}',
219
- templateMsg: 'Hi {{adv}}, by {{2}}',
220
- });
221
- expect(mergeSyntheticMustacheTags(apiTags, text).map((tag) => tag.name)).toEqual(['adv', '1', '2']);
222
- });
223
- });
224
-
225
- describe('mergeZaloUnfilledParamTags', () => {
226
- const formData = {
227
- templateListParams: [
228
- { name: 'danh_xung', value: '' },
229
- { name: 'name', value: '{{first_name}}' },
230
- { name: 'ma_so', value: ' ' },
231
- ],
232
- };
233
-
234
- it('adds a tag for each empty param', () => {
235
- expect(mergeZaloUnfilledParamTags([], formData)).toEqual([
236
- { name: 'danh_xung', metaData: { userDriven: false }, children: [] },
237
- { name: 'ma_so', metaData: { userDriven: false }, children: [] },
238
- ]);
239
- });
240
-
241
- it('keeps API tags and does not duplicate names', () => {
242
- const apiTags = [{ name: 'danh_xung', metaData: { userDriven: true }, children: [] }];
243
- const result = mergeZaloUnfilledParamTags(apiTags, formData);
244
- expect(result.map((tag) => tag.name)).toEqual(['danh_xung', 'ma_so']);
245
- expect(result[0]).toBe(apiTags[0]);
246
- });
247
-
248
- it('returns API tags when there are no params', () => {
249
- expect(mergeZaloUnfilledParamTags(undefined, {})).toEqual([]);
250
- });
251
-
252
- it('handles missing args, unnamed params and null values', () => {
253
- expect(mergeZaloUnfilledParamTags()).toEqual([]);
254
- expect(mergeZaloUnfilledParamTags(null, {
255
- templateListParams: [{ value: '' }, { name: 'a', value: null }, { name: 'b' }],
256
- }).map((tag) => tag.name)).toEqual(['a', 'b']);
257
- });
258
- });
259
-
260
- describe('mergeSyntheticMustacheTags', () => {
261
- it('handles null API tags', () => {
262
- expect(mergeSyntheticMustacheTags(null, 'Hi {{1}}').map((tag) => tag.name)).toEqual(['1']);
263
- expect(mergeSyntheticMustacheTags(null, 'no tags')).toEqual([]);
264
- });
265
- });
266
200
  });
@@ -61,20 +61,6 @@ export const mergeSyntheticMustacheTags = (apiTags = [], rawText = '') => {
61
61
  return [...(apiTags ?? []), ...synthetic];
62
62
  };
63
63
 
64
- /** WhatsApp header + body text, scanned for unmapped `{{N}}` slots. */
65
- export const getWhatsappTagExtractionText = (formDataObj) => (
66
- `${formDataObj?.templateHeader || ''} ${formDataObj?.templateMsg || ''}`.trim()
67
- );
68
-
69
- /** Zalo params left empty on Overview, surfaced as fillable test values (keyed by param name). */
70
- export const mergeZaloUnfilledParamTags = (apiTags = [], formDataObj = {}) => {
71
- const existingNames = new Set((apiTags ?? []).map((tag) => tag?.name));
72
- const unfilledParamTags = (formDataObj?.templateListParams || [])
73
- .filter((param) => param?.name && !String(param?.value ?? '').trim() && !existingNames.has(param.name))
74
- .map((param) => ({ name: param.name, metaData: { userDriven: false }, children: [] }));
75
- return [...(apiTags ?? []), ...unfilledParamTags];
76
- };
77
-
78
64
  /** RCS createMessageMeta: normalize media shape (mediaUrl, thumbnailUrl, height string). */
79
65
  export const normalizeRcsTestCardMedia = (media) => {
80
66
  if (!media || typeof media !== 'object') return undefined;
@@ -29,7 +29,7 @@ const deviceAspectRatio = {
29
29
  },
30
30
  };
31
31
 
32
- class EmailMobilePreview extends React.Component {
32
+ export class EmailMobilePreview extends React.Component {
33
33
  static getDerivedStateFromProps(props) {
34
34
  if (props.showAllDevices) {
35
35
  return {
@@ -66,6 +66,7 @@ class EmailMobilePreview extends React.Component {
66
66
  templateContent,
67
67
  enableShowAllDevices,
68
68
  templateSubject,
69
+ templatePreHeader,
69
70
  } = this.props;
70
71
  const copyContent = templateContent && removeLinksFromHtmlContent(templateContent);
71
72
  const updateCopyContent = templateContent && iframePreviewAdjustWidth(copyContent);
@@ -76,6 +77,10 @@ class EmailMobilePreview extends React.Component {
76
77
  <CapLabel className="email-subject-mobile" type="label2">
77
78
  <FormattedMessage {...messages.subject} /> {templateSubject}</CapLabel>
78
79
  );
80
+ const emailPreHeaderLabel = templatePreHeader && (
81
+ <CapLabel className="email-preheader-mobile" type="label2">
82
+ <FormattedMessage {...messages.preheader} /> {templatePreHeader}</CapLabel>
83
+ );
79
84
 
80
85
  return (
81
86
  <div className="email-mobile-preview-container">
@@ -100,6 +105,7 @@ class EmailMobilePreview extends React.Component {
100
105
  <img src={mobileBody} alt="capillary" width="386" />
101
106
  <div className={`${messageContainerName} ${emailSubjectLabel ? 'with-subject' : ''}`}>
102
107
  {emailSubjectLabel}
108
+ {emailPreHeaderLabel}
103
109
  <iframe srcDoc={updateCopyContent} {...deviceAspectRatio.mobile} >
104
110
  <p><FormattedMessage {...messages.noIframeSupport}/></p>
105
111
  </iframe>
@@ -127,6 +133,7 @@ class EmailMobilePreview extends React.Component {
127
133
  EmailMobilePreview.propTypes = {
128
134
  templateContent: PropTypes.string,
129
135
  templateSubject: PropTypes.string,
136
+ templatePreHeader: PropTypes.string,
130
137
  templateData: PropTypes.object,
131
138
  templateActions: PropTypes.object,
132
139
  onClickShowInAllDevices: PropTypes.func,
@@ -44,10 +44,19 @@ $classPrefix: email-mobile-preview-container;
44
44
  justify-content: center;
45
45
  }
46
46
  .email-subject-mobile {
47
- width: 227px;
47
+ width: 14.1875rem;
48
48
  text-align: left;
49
- margin: 8px 0;
50
- margin-left: 4px;
49
+ margin: 0.5rem 0.25rem 0;
50
+ padding-bottom: 0.5rem;
51
+ border-bottom: 0.0625rem solid #d9d9d9;
52
+ }
53
+ .email-preheader-mobile {
54
+ width: 14.1875rem;
55
+ text-align: left;
56
+ margin: 0.5rem 0.25rem 0;
57
+ padding-bottom: 0.5rem;
58
+ border-bottom: 0.0625rem solid #d9d9d9;
59
+ color: #8c8c8c;
51
60
  }
52
61
  // .mobile-message-container {
53
62
  // position: absolute;
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { shallow } from 'enzyme';
3
+ import { EmailMobilePreview } from '../index';
4
+
5
+ describe('<EmailMobilePreview />', () => {
6
+ const baseProps = {
7
+ templateContent: '<p>Body</p>',
8
+ templateActions: {
9
+ getTemplateDetails: jest.fn(),
10
+ },
11
+ };
12
+
13
+ it('does not render a pre-header label when templatePreHeader is not provided', () => {
14
+ const wrapper = shallow(<EmailMobilePreview {...baseProps} templateSubject="Hello" />);
15
+ expect(wrapper.find('.email-preheader-mobile')).toHaveLength(0);
16
+ });
17
+
18
+ it('renders the pre-header label when templatePreHeader is provided', () => {
19
+ const wrapper = shallow(
20
+ <EmailMobilePreview {...baseProps} templateSubject="Hello" templatePreHeader="Enjoy 20% off" />
21
+ );
22
+ const label = wrapper.find('.email-preheader-mobile');
23
+ expect(label).toHaveLength(1);
24
+ expect(label.text()).toContain('Enjoy 20% off');
25
+ });
26
+
27
+ it('renders the pre-header label even when subject is absent', () => {
28
+ const wrapper = shallow(<EmailMobilePreview {...baseProps} templatePreHeader="Enjoy 20% off" />);
29
+ expect(wrapper.find('.email-preheader-mobile')).toHaveLength(1);
30
+ });
31
+ });
@@ -1,4 +1,8 @@
1
+ @import '~@capillarytech/cap-ui-library/styles/_variables';
2
+
1
3
  $classPrefix: email-preview-v2;
4
+ $preheaderMutedColor: #8c8c8c;
5
+
2
6
  .#{$classPrefix}:not(.email-mobile-preview-container){
3
7
  #emailPreivew-container {
4
8
  position: relative;
@@ -54,19 +58,47 @@ $classPrefix: email-preview-v2;
54
58
  .email-subject-mobile {
55
59
  width: 310px;
56
60
  text-align: left;
57
- margin: 8px 0;
58
- margin-left: 4px;
61
+ margin: 0.5rem 0.25rem 0;
62
+ padding-bottom: 0.5rem;
63
+ border-bottom: 0.0625rem solid $CAP_COLOR_16;
59
64
  }
60
65
  .email-subject-tablet {
61
66
  width: 450px;
62
67
  text-align: left;
63
- margin: 8px 0;
64
- margin-left: 4px;
68
+ margin: 0.5rem 0.25rem 0;
69
+ padding-bottom: 0.5rem;
70
+ border-bottom: 0.0625rem solid $CAP_COLOR_16;
65
71
  }
66
72
  .email-subject-desktop {
67
73
  text-align: left;
68
74
  margin-left: 6%;
69
- margin-bottom: 8px;
75
+ margin-right: 6%;
76
+ padding-bottom: 0.5rem;
77
+ border-bottom: 0.0625rem solid $CAP_COLOR_16;
78
+ }
79
+ .email-preheader-mobile {
80
+ width: 19.375rem;
81
+ text-align: left;
82
+ margin: 0.5rem 0.25rem 0;
83
+ padding-bottom: 0.5rem;
84
+ border-bottom: 0.0625rem solid $CAP_COLOR_16;
85
+ color: $preheaderMutedColor;
86
+ }
87
+ .email-preheader-tablet {
88
+ width: 28.125rem;
89
+ text-align: left;
90
+ margin: 0.5rem 0.25rem 0;
91
+ padding-bottom: 0.5rem;
92
+ border-bottom: 0.0625rem solid $CAP_COLOR_16;
93
+ color: $preheaderMutedColor;
94
+ }
95
+ .email-preheader-desktop {
96
+ text-align: left;
97
+ margin-left: 6%;
98
+ margin-right: 6%;
99
+ padding-bottom: 0.5rem;
100
+ border-bottom: 0.0625rem solid $CAP_COLOR_16;
101
+ color: $preheaderMutedColor;
70
102
  }
71
103
  .tablet-message-container {
72
104
  position: absolute;