@capillarytech/creatives-library 8.0.340-beta.0.10 → 8.0.340-beta.0.11

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 (26) hide show
  1. package/package.json +1 -1
  2. package/v2Components/CommonTestAndPreview/UnifiedPreview/PreviewHeader.js +16 -0
  3. package/v2Components/CommonTestAndPreview/UnifiedPreview/WebPushPreviewContent.js +169 -0
  4. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +54 -0
  5. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +53 -7
  6. package/v2Components/CommonTestAndPreview/constants.js +2 -0
  7. package/v2Components/CommonTestAndPreview/index.js +51 -2
  8. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/PreviewHeader.test.js +163 -0
  9. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WebPushPreviewContent.test.js +522 -0
  10. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +255 -0
  11. package/v2Components/CommonTestAndPreview/tests/constants.test.js +2 -1
  12. package/v2Components/CommonTestAndPreview/tests/index.test.js +194 -0
  13. package/v2Components/FormBuilder/index.js +162 -52
  14. package/v2Components/TestAndPreviewSlidebox/index.js +2 -2
  15. package/v2Containers/App/constants.js +3 -0
  16. package/v2Containers/App/tests/constants.test.js +61 -0
  17. package/v2Containers/CreativesContainer/index.js +60 -24
  18. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +24 -36
  19. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -9
  20. package/v2Containers/Templates/index.js +72 -2
  21. package/v2Containers/Templates/tests/webpush.test.js +375 -0
  22. package/v2Containers/WebPush/Create/index.js +91 -8
  23. package/v2Containers/WebPush/Create/index.scss +9 -0
  24. package/v2Containers/WebPush/Create/tests/getTemplateContent.test.js +348 -0
  25. package/v2Containers/WebPush/Create/tests/testAndPreviewIntegration.test.js +325 -0
  26. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +68 -102
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.340-beta.0.10",
4
+ "version": "8.0.340-beta.0.11",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -23,6 +23,7 @@ const PreviewHeader = ({
23
23
  showDeviceToggle,
24
24
  onDeviceChange,
25
25
  channel,
26
+ setIsFullscreenOpen,
26
27
  }) => {
27
28
  // Determine if this is SMS, WhatsApp, RCS, InApp, MobilePush, or Viber channel (uses Android/iOS) or other channels (uses Desktop/Mobile)
28
29
  const isSmsChannel = channel === CHANNELS.SMS;
@@ -31,8 +32,13 @@ const PreviewHeader = ({
31
32
  const isInAppChannel = channel === CHANNELS.INAPP;
32
33
  const isMobilePushChannel = channel === CHANNELS.MOBILEPUSH;
33
34
  const isViberChannel = channel === CHANNELS.VIBER;
35
+ const isWebPushChannel = channel === CHANNELS.WEBPUSH;
34
36
  const isAndroidIosToggle = isSmsChannel || isWhatsappChannel || isRcsChannel || isInAppChannel || isMobilePushChannel || isViberChannel;
35
37
 
38
+ const handleOpenFullscreen = () => {
39
+ setIsFullscreenOpen(true);
40
+ };
41
+
36
42
  return (
37
43
  <CapRow className="preview-chrome">
38
44
  <div className="preview-header">
@@ -80,6 +86,12 @@ const PreviewHeader = ({
80
86
  )}
81
87
  </CapRow>
82
88
  )}
89
+ {isWebPushChannel && (
90
+ <CapIcon
91
+ type="expander"
92
+ onClick={() => handleOpenFullscreen()}
93
+ />
94
+ )}
83
95
  </div>
84
96
  </CapRow>
85
97
  );
@@ -95,6 +107,8 @@ PreviewHeader.propTypes = {
95
107
  showDeviceToggle: PropTypes.bool,
96
108
  onDeviceChange: PropTypes.func,
97
109
  channel: PropTypes.string,
110
+ isFullscreenOpen: PropTypes.bool,
111
+ setIsFullscreenOpen: PropTypes.func,
98
112
  };
99
113
 
100
114
  PreviewHeader.defaultProps = {
@@ -103,6 +117,8 @@ PreviewHeader.defaultProps = {
103
117
  showDeviceToggle: false,
104
118
  onDeviceChange: () => {},
105
119
  channel: null,
120
+ isFullscreenOpen: false,
121
+ setIsFullscreenOpen: () => {},
106
122
  };
107
123
 
108
124
  export default PreviewHeader;
@@ -0,0 +1,169 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormattedMessage } from 'react-intl';
4
+ import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
5
+ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
6
+ import CapModal from '@capillarytech/cap-ui-library/CapModal';
7
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
8
+ import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
9
+ import PreviewControls from '../../../v2Containers/WebPush/Create/preview/PreviewControls';
10
+ import PreviewContent from '../../../v2Containers/WebPush/Create/preview/PreviewContent';
11
+ import DevicePreviewContent from '../../../v2Containers/WebPush/Create/preview/DevicePreviewContent';
12
+ import {
13
+ OS_OPTIONS,
14
+ DEFAULT_OS,
15
+ DEFAULT_BROWSER,
16
+ } from '../../../v2Containers/WebPush/Create/preview/constants';
17
+ import { getBrowserOptionsForOS } from '../../../v2Containers/WebPush/Create/preview/config/notificationMappings';
18
+ import messages from '../messages';
19
+
20
+ const WebPushPreviewContent = ({
21
+ notificationTitle,
22
+ notificationBody,
23
+ imageSrc,
24
+ brandIconSrc,
25
+ buttons,
26
+ url,
27
+ isUpdating,
28
+ error,
29
+ isFullscreenOpen,
30
+ setIsFullscreenOpen,
31
+ selectedCustomer,
32
+ }) => {
33
+ const [selectedOS, setSelectedOS] = useState(DEFAULT_OS);
34
+ const [selectedBrowser, setSelectedBrowser] = useState(DEFAULT_BROWSER);
35
+
36
+ const browserOptionsForOS = getBrowserOptionsForOS(selectedOS);
37
+
38
+ // Coerce browser when OS change removes current browser from available options
39
+ useEffect(() => {
40
+ const isValid = browserOptionsForOS.some((opt) => opt.value === selectedBrowser);
41
+ if (!isValid && browserOptionsForOS.length) {
42
+ setSelectedBrowser(browserOptionsForOS[0].value);
43
+ }
44
+ }, [browserOptionsForOS, selectedBrowser]);
45
+
46
+ const handleOSChange = (value) => {
47
+ setSelectedOS(value);
48
+ };
49
+
50
+ const handleBrowserChange = (value) => {
51
+ setSelectedBrowser(value);
52
+ };
53
+
54
+ const handleCloseFullscreen = () => {
55
+ setIsFullscreenOpen(false);
56
+ };
57
+
58
+ if (isUpdating) {
59
+ return null;
60
+ }
61
+
62
+ if (error) {
63
+ return null;
64
+ }
65
+
66
+ return (
67
+ <CapRow className="webpush-test-preview-container webpush-preview-container">
68
+ <CapRow className="webpush-preview-panel">
69
+ <PreviewControls
70
+ selectedOS={selectedOS}
71
+ selectedBrowser={selectedBrowser}
72
+ osOptions={OS_OPTIONS}
73
+ browserOptions={browserOptionsForOS}
74
+ onOSChange={handleOSChange}
75
+ onBrowserChange={handleBrowserChange}
76
+ layoutMode="newRow"
77
+ showStateDropdown={false}
78
+ />
79
+
80
+ <PreviewContent
81
+ notificationTitle={notificationTitle}
82
+ notificationBody={notificationBody}
83
+ url={url}
84
+ selectedOS={selectedOS}
85
+ selectedBrowser={selectedBrowser}
86
+ notificationState="Collapsed"
87
+ imageSrc={imageSrc}
88
+ brandIconSrc={brandIconSrc}
89
+ buttons={buttons}
90
+ />
91
+ </CapRow>
92
+
93
+ <CapModal
94
+ visible={isFullscreenOpen}
95
+ onCancel={handleCloseFullscreen}
96
+ width="90%"
97
+ centered
98
+ closable={false}
99
+ footer={null}
100
+ title={null}
101
+ maskClosable
102
+ className="webpush-fullscreen-modal webpush-preview-container"
103
+ >
104
+ <CapRow className="webpush-preview-header">
105
+ <div className="webpush-heading-container">
106
+ <CapRow type="flex" className="preview-for">
107
+ <CapLabel type="label16">
108
+ <FormattedMessage {...messages.previewFor} />
109
+ </CapLabel>
110
+ <CapLabel type="label2">
111
+ {selectedCustomer ? selectedCustomer.name : <FormattedMessage {...messages.defaultPreview} />}
112
+ </CapLabel>
113
+ </CapRow>
114
+ <CapIcon
115
+ type="collapse2"
116
+ onClick={() => handleCloseFullscreen()}
117
+ className="webpush-fullscreen-close-icon"
118
+ />
119
+ </div>
120
+ </CapRow>
121
+ <CapDivider className="webpush-fullscreen-divider" />
122
+ <DevicePreviewContent
123
+ notificationTitle={notificationTitle}
124
+ notificationBody={notificationBody}
125
+ url={url}
126
+ imageSrc={imageSrc}
127
+ brandIconSrc={brandIconSrc}
128
+ buttons={buttons}
129
+ />
130
+ </CapModal>
131
+ </CapRow>
132
+ );
133
+ };
134
+
135
+ WebPushPreviewContent.propTypes = {
136
+ notificationTitle: PropTypes.string,
137
+ notificationBody: PropTypes.string,
138
+ imageSrc: PropTypes.string,
139
+ brandIconSrc: PropTypes.string,
140
+ buttons: PropTypes.arrayOf(
141
+ PropTypes.shape({
142
+ text: PropTypes.string,
143
+ url: PropTypes.string,
144
+ type: PropTypes.string,
145
+ })
146
+ ),
147
+ url: PropTypes.string,
148
+ isUpdating: PropTypes.bool,
149
+ error: PropTypes.string,
150
+ isFullscreenOpen: PropTypes.bool,
151
+ setIsFullscreenOpen: PropTypes.func,
152
+ selectedCustomer: PropTypes.object,
153
+ };
154
+
155
+ WebPushPreviewContent.defaultProps = {
156
+ notificationTitle: '',
157
+ notificationBody: '',
158
+ imageSrc: '',
159
+ brandIconSrc: '',
160
+ buttons: [],
161
+ url: '',
162
+ isUpdating: false,
163
+ error: null,
164
+ isFullscreenOpen: false,
165
+ setIsFullscreenOpen: () => {},
166
+ selectedCustomer: null,
167
+ };
168
+
169
+ export default WebPushPreviewContent;
@@ -2348,6 +2348,60 @@
2348
2348
  }
2349
2349
  }
2350
2350
 
2351
+ // WebPush Test & Preview Styles
2352
+ .webpush-test-preview-container {
2353
+ width: 100%;
2354
+ position: relative;
2355
+ padding-left: $CAP_SPACE_16;
2356
+ padding-right: $CAP_SPACE_16;
2357
+ }
2358
+
2359
+ .webpush-preview-panel {
2360
+ position: relative;
2361
+ width: 100%;
2362
+ }
2363
+
2364
+ .webpush-fullscreen-modal {
2365
+ .webpush-fullscreen-divider {
2366
+ margin-top: 0;
2367
+ margin-bottom: $CAP_SPACE_16;
2368
+ }
2369
+ .ant-modal.cap-modal-v2 {
2370
+ width: 90%;
2371
+ max-width: 100%;
2372
+ margin-top: $CAP_SPACE_40;
2373
+ }
2374
+ // Preview Chrome wrapper (matches old TestAndPreviewSlidebox design)
2375
+ .webpush-preview-header {
2376
+ background: $CAP_WHITE;
2377
+ overflow: hidden;
2378
+
2379
+ .preview-divider {
2380
+ margin: 0;
2381
+ }
2382
+
2383
+ .webpush-heading-container {
2384
+ display: flex;
2385
+ justify-content: space-between;
2386
+ align-items: center;
2387
+ padding: $CAP_SPACE_16 0 $CAP_SPACE_16 0;
2388
+
2389
+ .preview-for {
2390
+ gap: $CAP_SPACE_04;
2391
+ align-items: center;
2392
+ b {
2393
+ margin-left: $CAP_SPACE_08;
2394
+ }
2395
+ }
2396
+
2397
+ .webpush-fullscreen-close-icon {
2398
+ width: $CAP_SPACE_24;
2399
+ height: $CAP_SPACE_24;
2400
+ }
2401
+ }
2402
+ }
2403
+ }
2404
+
2351
2405
  // Responsive adjustments
2352
2406
  @media (max-width: 85.714rem) {
2353
2407
  .unified-preview {
@@ -6,7 +6,7 @@
6
6
  * Routes to channel-specific preview content components
7
7
  */
8
8
 
9
- import React from 'react';
9
+ import React, { useState } from 'react';
10
10
  import PropTypes from 'prop-types';
11
11
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
12
12
  import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
@@ -21,6 +21,7 @@ import InAppPreviewContent from './InAppPreviewContent';
21
21
  import MobilePushPreviewContent from './MobilePushPreviewContent';
22
22
  import ViberPreviewContent from './ViberPreviewContent';
23
23
  import ZaloPreviewContent from './ZaloPreviewContent';
24
+ import WebPushPreviewContent from './WebPushPreviewContent';
24
25
  import { CHANNELS, DESKTOP, TABLET, MOBILE, ANDROID, IOS } from '../constants';
25
26
  import messages from '../messages';
26
27
  import './_unifiedPreview.scss';
@@ -44,6 +45,7 @@ const UnifiedPreview = ({
44
45
  * For Phase 5, we'll render placeholders
45
46
  * Phase 6+ will implement actual content components
46
47
  */
48
+ const [isFullscreenOpen, setIsFullscreenOpen] = useState(false);
47
49
  const renderChannelContent = () => {
48
50
  // Phase 5: Placeholder content for all channels
49
51
  // Phase 6+: Import and render actual channel-specific components
@@ -197,6 +199,45 @@ const UnifiedPreview = ({
197
199
  );
198
200
  }
199
201
 
202
+ case CHANNELS.WEBPUSH: {
203
+ // WebPush content arrives as a JSON string (from getCurrentContent or preview API response)
204
+ let webPushOuter = {};
205
+ try {
206
+ webPushOuter = typeof content === 'string' ? JSON.parse(content) : (typeof content === 'object' && content !== null ? content : {});
207
+ } catch (e) {
208
+ webPushOuter = {};
209
+ }
210
+ const { content: webPushInner = {} } = webPushOuter;
211
+ const {
212
+ title,
213
+ message,
214
+ iconImageUrl,
215
+ cta,
216
+ expandableDetails = {},
217
+ } = webPushInner;
218
+ const { media: webPushMedia = [], ctas: webPushCtas = [] } = expandableDetails;
219
+ const webPushButtons = webPushCtas.map(({ title: text = '', actionLink: url = '', type = '' }) => ({
220
+ text,
221
+ url,
222
+ type,
223
+ }));
224
+ return (
225
+ <WebPushPreviewContent
226
+ notificationTitle={title || ''}
227
+ notificationBody={message || ''}
228
+ imageSrc={webPushMedia[0]?.url || ''}
229
+ brandIconSrc={iconImageUrl || ''}
230
+ buttons={webPushButtons}
231
+ url={cta?.actionLink || ''}
232
+ isUpdating={isUpdating}
233
+ error={error}
234
+ isFullscreenOpen={isFullscreenOpen}
235
+ setIsFullscreenOpen={setIsFullscreenOpen}
236
+ selectedCustomer={selectedCustomer}
237
+ />
238
+ );
239
+ }
240
+
200
241
  default:
201
242
  return (
202
243
  <div className="channel-preview-placeholder">
@@ -212,6 +253,8 @@ const UnifiedPreview = ({
212
253
  }
213
254
  };
214
255
 
256
+ const showToggle = channel !== CHANNELS.WEBPUSH && showDeviceToggle;
257
+
215
258
  /**
216
259
  * Render loading state for all channels
217
260
  */
@@ -222,9 +265,10 @@ const UnifiedPreview = ({
222
265
  <PreviewHeader
223
266
  selectedCustomer={selectedCustomer}
224
267
  device={device}
225
- showDeviceToggle={showDeviceToggle}
268
+ showDeviceToggle={showToggle}
226
269
  onDeviceChange={onDeviceChange}
227
270
  channel={channel}
271
+ setIsFullscreenOpen={setIsFullscreenOpen}
228
272
  />
229
273
  )}
230
274
  <CapRow className="preview-loading-container">
@@ -247,9 +291,10 @@ const UnifiedPreview = ({
247
291
  <PreviewHeader
248
292
  selectedCustomer={selectedCustomer}
249
293
  device={device}
250
- showDeviceToggle={showDeviceToggle}
294
+ showDeviceToggle={showToggle}
251
295
  onDeviceChange={onDeviceChange}
252
296
  channel={channel}
297
+ setIsFullscreenOpen={setIsFullscreenOpen}
253
298
  />
254
299
  )}
255
300
  <CapRow className="preview-error-container">
@@ -275,15 +320,16 @@ const UnifiedPreview = ({
275
320
  <PreviewHeader
276
321
  selectedCustomer={selectedCustomer}
277
322
  device={device}
278
- showDeviceToggle={showDeviceToggle}
323
+ showDeviceToggle={showToggle}
279
324
  onDeviceChange={onDeviceChange}
280
325
  channel={channel}
326
+ setIsFullscreenOpen={setIsFullscreenOpen}
281
327
  />
282
328
  )}
283
329
 
284
330
  {/* Channel-specific preview content */}
285
- <CapRow useLegacy className={`preview-content-container ${!showHeader ? 'preview-content-container-no-header' : ''}`}>
286
- {[CHANNELS.EMAIL, CHANNELS.SMS, CHANNELS.WHATSAPP, CHANNELS.RCS, CHANNELS.INAPP, CHANNELS.MOBILEPUSH, CHANNELS.VIBER].includes(channel) ? (
331
+ <CapRow className={`preview-content-container ${!showHeader ? 'preview-content-container-no-header' : ''}`}>
332
+ {[CHANNELS.EMAIL, CHANNELS.SMS, CHANNELS.WHATSAPP, CHANNELS.RCS, CHANNELS.INAPP, CHANNELS.MOBILEPUSH, CHANNELS.VIBER, CHANNELS.WEBPUSH].includes(channel) ? (
287
333
  renderChannelContent()
288
334
  ) : (
289
335
  <DeviceFrame device={device || DESKTOP}>
@@ -319,7 +365,7 @@ const UnifiedPreview = ({
319
365
  UnifiedPreview.propTypes = {
320
366
  // Core
321
367
  channel: PropTypes.oneOf(Object.values(CHANNELS)).isRequired,
322
- content: PropTypes.object.isRequired,
368
+ content: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired,
323
369
 
324
370
  // Display options
325
371
  device: PropTypes.oneOf([DESKTOP, TABLET, MOBILE, ANDROID, IOS]),
@@ -85,6 +85,7 @@ export const CHANNELS = {
85
85
  MOBILEPUSH: 'MOBILEPUSH',
86
86
  VIBER: 'VIBER',
87
87
  ZALO: 'ZALO',
88
+ WEBPUSH: 'WEBPUSH',
88
89
  };
89
90
  export const CHANNELS_USING_ANDROID_PREVIEW_DEVICE = [CHANNELS.SMS, CHANNELS.RCS, CHANNELS.WHATSAPP, CHANNELS.VIBER, CHANNELS.ZALO, CHANNELS.INAPP, CHANNELS.MOBILEPUSH];
90
91
 
@@ -203,6 +204,7 @@ export const DYNAMIC_URL = 'DYNAMIC_URL';
203
204
  export const IMAGE = 'IMAGE';
204
205
  export const VIDEO = 'VIDEO';
205
206
  export const URL = 'URL';
207
+ export const DAYS = 'DAYS';
206
208
 
207
209
  // Initial Payload Template (for reference)
208
210
  export const INITIAL_PAYLOAD = {
@@ -81,7 +81,8 @@ import {
81
81
  IMAGE,
82
82
  VIDEO,
83
83
  URL,
84
- CHANNELS_USING_ANDROID_PREVIEW_DEVICE
84
+ CHANNELS_USING_ANDROID_PREVIEW_DEVICE,
85
+ DAYS,
85
86
  } from './constants';
86
87
 
87
88
  // Import utilities
@@ -610,6 +611,12 @@ const CommonTestAndPreview = (props) => {
610
611
  messageBody: contentStr,
611
612
  };
612
613
 
614
+ case CHANNELS.WEBPUSH:
615
+ return {
616
+ ...basePayload,
617
+ messageBody: contentStr,
618
+ };
619
+
613
620
  default:
614
621
  return basePayload;
615
622
  }
@@ -662,6 +669,12 @@ const CommonTestAndPreview = (props) => {
662
669
  templateContent: contentStr,
663
670
  };
664
671
 
672
+ case CHANNELS.WEBPUSH:
673
+ return {
674
+ templateSubject: formDataObj?.content?.title || '',
675
+ templateContent: contentStr,
676
+ };
677
+
665
678
  case CHANNELS.ZALO: {
666
679
  // For Zalo, extract content from templateListParams array
667
680
  // Combine all variable values into a single string for tag extraction
@@ -1801,6 +1814,42 @@ const CommonTestAndPreview = (props) => {
1801
1814
  };
1802
1815
  }
1803
1816
 
1817
+ case CHANNELS.WEBPUSH: {
1818
+ const webpushData = (typeof formDataObj === 'object' && formDataObj !== null)
1819
+ ? formDataObj
1820
+ : {};
1821
+ const innerContent = webpushData?.content || {};
1822
+
1823
+ const resolvedTitle = resolveTagsInText(innerContent?.title || '', customValuesObj);
1824
+ const resolvedMessage = resolveTagsInText(innerContent?.message || '', customValuesObj);
1825
+
1826
+ return {
1827
+ ...basePayload,
1828
+ webPushMessageContent: {
1829
+ channel: CHANNELS.WEBPUSH,
1830
+ accountId: webpushData?.accountId || null,
1831
+ content: {
1832
+ title: resolvedTitle,
1833
+ message: resolvedMessage,
1834
+ ...(innerContent?.iconImageUrl && { iconImageUrl: innerContent.iconImageUrl }),
1835
+ ...(innerContent?.cta && { cta: innerContent.cta }),
1836
+ ...(innerContent?.expandableDetails && { expandableDetails: innerContent.expandableDetails }),
1837
+ },
1838
+ messageSubject: webpushData?.messageSubject || resolvedTitle || '',
1839
+ },
1840
+ webPushDeliverySettings: {
1841
+ channelSettings: {
1842
+ channel: CHANNELS.WEBPUSH,
1843
+ notificationTtl: {
1844
+ duration: 7,
1845
+ timeUnit: DAYS,
1846
+ },
1847
+ },
1848
+ additionalSettings: {},
1849
+ },
1850
+ };
1851
+ }
1852
+
1804
1853
  default:
1805
1854
  return basePayload;
1806
1855
  }
@@ -1832,7 +1881,7 @@ const CommonTestAndPreview = (props) => {
1832
1881
  contentObj = hasPreviewCallBeenMade && previewDataHtml?.resolvedBody
1833
1882
  ? previewDataHtml.resolvedBody
1834
1883
  : getCurrentContent || '';
1835
- } else if (channel === CHANNELS.WHATSAPP) {
1884
+ } else if ([CHANNELS.WHATSAPP, CHANNELS.WEBPUSH].includes(channel)) {
1836
1885
  // For WhatsApp, content is an object with templateMsg, media, CTA, etc.
1837
1886
  // Content comes from WhatsApp component state, passed via content prop
1838
1887
  let resolvedContent = null;
@@ -517,4 +517,167 @@ describe('PreviewHeader', () => {
517
517
  consoleSpy.mockRestore();
518
518
  });
519
519
  });
520
+
521
+ describe('WEBPUSH channel - Fullscreen expander', () => {
522
+ it('should render expander icon for WEBPUSH channel', () => {
523
+ const setIsFullscreenOpen = jest.fn();
524
+ const props = {
525
+ ...defaultProps,
526
+ channel: CHANNELS.WEBPUSH,
527
+ setIsFullscreenOpen,
528
+ };
529
+
530
+ const { container } = render(
531
+ <TestWrapper>
532
+ <PreviewHeader {...props} />
533
+ </TestWrapper>
534
+ );
535
+
536
+ // expander icon should be present for WEBPUSH
537
+ const expanderIcon = container.querySelector('[class*="expander"], [aria-label*="expander"], .anticon');
538
+ expect(expanderIcon).toBeTruthy();
539
+ });
540
+
541
+ it('should NOT render expander icon for non-WEBPUSH channels', () => {
542
+ const setIsFullscreenOpen = jest.fn();
543
+ const props = {
544
+ ...defaultProps,
545
+ channel: CHANNELS.EMAIL,
546
+ setIsFullscreenOpen,
547
+ showDeviceToggle: false,
548
+ };
549
+
550
+ const { container } = render(
551
+ <TestWrapper>
552
+ <PreviewHeader {...props} />
553
+ </TestWrapper>
554
+ );
555
+
556
+ // No expander icon for EMAIL channel (only device toggle icons may appear)
557
+ const expanderIcon = container.querySelector('[class*="expander"]');
558
+ expect(expanderIcon).toBeNull();
559
+ });
560
+
561
+ it('should call setIsFullscreenOpen(true) when expander icon is clicked', () => {
562
+ const setIsFullscreenOpen = jest.fn();
563
+ const props = {
564
+ ...defaultProps,
565
+ channel: CHANNELS.WEBPUSH,
566
+ setIsFullscreenOpen,
567
+ };
568
+
569
+ const { container } = render(
570
+ <TestWrapper>
571
+ <PreviewHeader {...props} />
572
+ </TestWrapper>
573
+ );
574
+
575
+ const expanderIcon = container.querySelector('[class*="expander"]') ||
576
+ container.querySelector('.anticon');
577
+ if (expanderIcon) {
578
+ fireEvent.click(expanderIcon);
579
+ expect(setIsFullscreenOpen).toHaveBeenCalledWith(true);
580
+ }
581
+ });
582
+
583
+ it('should render expander icon without ACTIVE class when device is MOBILE', () => {
584
+ const setIsFullscreenOpen = jest.fn();
585
+ const props = {
586
+ ...defaultProps,
587
+ channel: CHANNELS.WEBPUSH,
588
+ device: MOBILE,
589
+ setIsFullscreenOpen,
590
+ };
591
+
592
+ const { container } = render(
593
+ <TestWrapper>
594
+ <PreviewHeader {...props} />
595
+ </TestWrapper>
596
+ );
597
+
598
+ // Expander icon no longer adds ACTIVE class based on device
599
+ const activeEl = container.querySelector('.active');
600
+ expect(activeEl).toBeNull();
601
+ // But the expander icon itself should still be present
602
+ const expanderIcon = container.querySelector('[class*="expander"]') ||
603
+ container.querySelector('.anticon');
604
+ expect(expanderIcon).toBeTruthy();
605
+ });
606
+
607
+ it('should render expander icon without ACTIVE class when device is DESKTOP', () => {
608
+ const setIsFullscreenOpen = jest.fn();
609
+ const props = {
610
+ ...defaultProps,
611
+ channel: CHANNELS.WEBPUSH,
612
+ device: DESKTOP,
613
+ setIsFullscreenOpen,
614
+ };
615
+
616
+ const { container } = render(
617
+ <TestWrapper>
618
+ <PreviewHeader {...props} />
619
+ </TestWrapper>
620
+ );
621
+
622
+ // Expander icon does not have ACTIVE class for any device
623
+ const activeEl = container.querySelector('.active');
624
+ expect(activeEl).toBeNull();
625
+ });
626
+
627
+ it('should render expander icon alongside device content for WEBPUSH', () => {
628
+ // Note: suppression of showDeviceToggle for WEBPUSH is done in UnifiedPreview (parent),
629
+ // not in PreviewHeader itself. PreviewHeader only adds the expander icon for WEBPUSH.
630
+ const setIsFullscreenOpen = jest.fn();
631
+ const props = {
632
+ ...defaultProps,
633
+ channel: CHANNELS.WEBPUSH,
634
+ setIsFullscreenOpen,
635
+ showDeviceToggle: false,
636
+ };
637
+
638
+ const { container } = render(
639
+ <TestWrapper>
640
+ <PreviewHeader {...props} />
641
+ </TestWrapper>
642
+ );
643
+
644
+ // expander icon should be present for WEBPUSH
645
+ const expanderIcon = container.querySelector('[class*="expander"]') ||
646
+ container.querySelector('.anticon');
647
+ expect(expanderIcon).toBeTruthy();
648
+ });
649
+
650
+ it('should accept isFullscreenOpen and setIsFullscreenOpen as props', () => {
651
+ const setIsFullscreenOpen = jest.fn();
652
+ const props = {
653
+ ...defaultProps,
654
+ channel: CHANNELS.WEBPUSH,
655
+ isFullscreenOpen: false,
656
+ setIsFullscreenOpen,
657
+ };
658
+
659
+ expect(() =>
660
+ render(
661
+ <TestWrapper>
662
+ <PreviewHeader {...props} />
663
+ </TestWrapper>
664
+ )
665
+ ).not.toThrow();
666
+ });
667
+
668
+ it('should use default setIsFullscreenOpen when not provided', () => {
669
+ const props = {
670
+ ...defaultProps,
671
+ channel: CHANNELS.WEBPUSH,
672
+ };
673
+
674
+ expect(() =>
675
+ render(
676
+ <TestWrapper>
677
+ <PreviewHeader {...props} />
678
+ </TestWrapper>
679
+ )
680
+ ).not.toThrow();
681
+ });
682
+ });
520
683
  });