@capillarytech/creatives-library 8.0.353-alpha.2 → 8.0.353-alpha.5

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 (24) hide show
  1. package/package.json +1 -1
  2. package/v2Components/CommonTestAndPreview/UnifiedPreview/PreviewHeader.js +17 -0
  3. package/v2Components/CommonTestAndPreview/UnifiedPreview/WebPushPreviewContent.js +169 -0
  4. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +70 -0
  5. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +44 -5
  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 +159 -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 +10 -48
  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 +25 -61
  18. package/v2Containers/Email/index.js +2 -33
  19. package/v2Containers/Templates/index.js +68 -2
  20. package/v2Containers/Templates/tests/webpush.test.js +375 -0
  21. package/v2Containers/WebPush/Create/index.js +91 -8
  22. package/v2Containers/WebPush/Create/index.scss +7 -0
  23. package/v2Containers/WebPush/Create/tests/getTemplateContent.test.js +338 -0
  24. package/v2Containers/WebPush/Create/tests/testAndPreviewIntegration.test.js +325 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.353-alpha.2",
4
+ "version": "8.0.353-alpha.5",
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,13 @@ const PreviewHeader = ({
80
86
  )}
81
87
  </CapRow>
82
88
  )}
89
+ {isWebPushChannel && (
90
+ <CapIcon
91
+ type="expander"
92
+ className={device === MOBILE ? ACTIVE : ''}
93
+ onClick={() => handleOpenFullscreen()}
94
+ />
95
+ )}
83
96
  </div>
84
97
  </CapRow>
85
98
  );
@@ -95,6 +108,8 @@ PreviewHeader.propTypes = {
95
108
  showDeviceToggle: PropTypes.bool,
96
109
  onDeviceChange: PropTypes.func,
97
110
  channel: PropTypes.string,
111
+ isFullscreenOpen: PropTypes.bool,
112
+ setIsFullscreenOpen: PropTypes.func,
98
113
  };
99
114
 
100
115
  PreviewHeader.defaultProps = {
@@ -103,6 +118,8 @@ PreviewHeader.defaultProps = {
103
118
  showDeviceToggle: false,
104
119
  onDeviceChange: () => {},
105
120
  channel: null,
121
+ isFullscreenOpen: () => {},
122
+ setIsFullscreenOpen: () => false,
106
123
  };
107
124
 
108
125
  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
+ <div 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
+ </div>
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;
@@ -2350,6 +2350,76 @@
2350
2350
  }
2351
2351
  }
2352
2352
 
2353
+ // WebPush Test & Preview Styles
2354
+ .webpush-test-preview-container {
2355
+ width: 100%;
2356
+ position: relative;
2357
+ padding-left: $CAP_SPACE_16;
2358
+ padding-right: $CAP_SPACE_16;
2359
+ }
2360
+
2361
+ .webpush-preview-panel {
2362
+ position: relative;
2363
+ width: 100%;
2364
+ }
2365
+
2366
+ .webpush-expand-btn {
2367
+ position: absolute;
2368
+ top: 8px;
2369
+ right: 8px;
2370
+ z-index: 10;
2371
+ padding: 4px;
2372
+ display: flex;
2373
+ align-items: center;
2374
+ justify-content: center;
2375
+
2376
+ &:hover {
2377
+ background-color: rgba(0, 0, 0, 0.06);
2378
+ border-radius: 4px;
2379
+ }
2380
+ }
2381
+
2382
+ .webpush-fullscreen-modal {
2383
+ .webpush-fullscreen-divider {
2384
+ margin-top: 0;
2385
+ margin-bottom: $CAP_SPACE_16;
2386
+ }
2387
+ .ant-modal.cap-modal-v2 {
2388
+ width: 90%;
2389
+ max-width: 100%;
2390
+ margin-top: $CAP_SPACE_40;
2391
+ }
2392
+ // Preview Chrome wrapper (matches old TestAndPreviewSlidebox design)
2393
+ .webpush-preview-header {
2394
+ background: $CAP_WHITE;
2395
+ overflow: hidden;
2396
+
2397
+ .preview-divider {
2398
+ margin: 0;
2399
+ }
2400
+
2401
+ .webpush-heading-container {
2402
+ display: flex;
2403
+ justify-content: space-between;
2404
+ align-items: center;
2405
+ padding: $CAP_SPACE_16 0 $CAP_SPACE_16 0;
2406
+
2407
+ .preview-for {
2408
+ gap: $CAP_SPACE_04;
2409
+ align-items: center;
2410
+ b {
2411
+ margin-left: $CAP_SPACE_08;
2412
+ }
2413
+ }
2414
+
2415
+ .webpush-fullscreen-close-icon {
2416
+ width: $CAP_SPACE_24;
2417
+ height: $CAP_SPACE_24;
2418
+ }
2419
+ }
2420
+ }
2421
+ }
2422
+
2353
2423
  // Responsive adjustments
2354
2424
  @media (max-width: 85.714rem) {
2355
2425
  .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,40 @@ 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 webPushInner = webPushOuter?.content || {};
211
+ const webPushExpandable = webPushInner?.expandableDetails || {};
212
+ const webPushMedia = webPushExpandable?.media || [];
213
+ const webPushCtas = webPushExpandable?.ctas || [];
214
+ const webPushButtons = webPushCtas.map((cta) => ({
215
+ text: cta?.title || '',
216
+ url: cta?.actionLink || '',
217
+ type: cta?.type || '',
218
+ }));
219
+ return (
220
+ <WebPushPreviewContent
221
+ notificationTitle={webPushInner?.title || ''}
222
+ notificationBody={webPushInner?.message || ''}
223
+ imageSrc={webPushMedia[0]?.url || ''}
224
+ brandIconSrc={webPushInner?.iconImageUrl || ''}
225
+ buttons={webPushButtons}
226
+ url={webPushInner?.cta?.actionLink || ''}
227
+ isUpdating={isUpdating}
228
+ error={error}
229
+ isFullscreenOpen={isFullscreenOpen}
230
+ setIsFullscreenOpen={setIsFullscreenOpen}
231
+ selectedCustomer={selectedCustomer}
232
+ />
233
+ );
234
+ }
235
+
200
236
  default:
201
237
  return (
202
238
  <div className="channel-preview-placeholder">
@@ -222,9 +258,10 @@ const UnifiedPreview = ({
222
258
  <PreviewHeader
223
259
  selectedCustomer={selectedCustomer}
224
260
  device={device}
225
- showDeviceToggle={showDeviceToggle}
261
+ showDeviceToggle={channel !== CHANNELS.WEBPUSH && showDeviceToggle}
226
262
  onDeviceChange={onDeviceChange}
227
263
  channel={channel}
264
+ setIsFullscreenOpen={setIsFullscreenOpen}
228
265
  />
229
266
  )}
230
267
  <CapRow className="preview-loading-container">
@@ -247,9 +284,10 @@ const UnifiedPreview = ({
247
284
  <PreviewHeader
248
285
  selectedCustomer={selectedCustomer}
249
286
  device={device}
250
- showDeviceToggle={showDeviceToggle}
287
+ showDeviceToggle={channel !== CHANNELS.WEBPUSH && showDeviceToggle}
251
288
  onDeviceChange={onDeviceChange}
252
289
  channel={channel}
290
+ setIsFullscreenOpen={setIsFullscreenOpen}
253
291
  />
254
292
  )}
255
293
  <CapRow className="preview-error-container">
@@ -275,15 +313,16 @@ const UnifiedPreview = ({
275
313
  <PreviewHeader
276
314
  selectedCustomer={selectedCustomer}
277
315
  device={device}
278
- showDeviceToggle={showDeviceToggle}
316
+ showDeviceToggle={channel !== CHANNELS.WEBPUSH && showDeviceToggle}
279
317
  onDeviceChange={onDeviceChange}
280
318
  channel={channel}
319
+ setIsFullscreenOpen={setIsFullscreenOpen}
281
320
  />
282
321
  )}
283
322
 
284
323
  {/* Channel-specific preview content */}
285
324
  <CapRow 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) ? (
325
+ {[CHANNELS.EMAIL, CHANNELS.SMS, CHANNELS.WHATSAPP, CHANNELS.RCS, CHANNELS.INAPP, CHANNELS.MOBILEPUSH, CHANNELS.VIBER, CHANNELS.WEBPUSH].includes(channel) ? (
287
326
  renderChannelContent()
288
327
  ) : (
289
328
  <DeviceFrame device={device || DESKTOP}>
@@ -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 (channel === CHANNELS.WHATSAPP || channel === CHANNELS.WEBPUSH) {
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;
@@ -514,4 +514,163 @@ describe('PreviewHeader', () => {
514
514
  consoleSpy.mockRestore();
515
515
  });
516
516
  });
517
+
518
+ describe('WEBPUSH channel - Fullscreen expander', () => {
519
+ it('should render expander icon for WEBPUSH channel', () => {
520
+ const setIsFullscreenOpen = jest.fn();
521
+ const props = {
522
+ ...defaultProps,
523
+ channel: CHANNELS.WEBPUSH,
524
+ setIsFullscreenOpen,
525
+ };
526
+
527
+ const { container } = render(
528
+ <TestWrapper>
529
+ <PreviewHeader {...props} />
530
+ </TestWrapper>
531
+ );
532
+
533
+ // expander icon should be present for WEBPUSH
534
+ const expanderIcon = container.querySelector('[class*="expander"], [aria-label*="expander"], .anticon');
535
+ expect(expanderIcon).toBeTruthy();
536
+ });
537
+
538
+ it('should NOT render expander icon for non-WEBPUSH channels', () => {
539
+ const setIsFullscreenOpen = jest.fn();
540
+ const props = {
541
+ ...defaultProps,
542
+ channel: CHANNELS.EMAIL,
543
+ setIsFullscreenOpen,
544
+ showDeviceToggle: false,
545
+ };
546
+
547
+ const { container } = render(
548
+ <TestWrapper>
549
+ <PreviewHeader {...props} />
550
+ </TestWrapper>
551
+ );
552
+
553
+ // No expander icon for EMAIL channel (only device toggle icons may appear)
554
+ const expanderIcon = container.querySelector('[class*="expander"]');
555
+ expect(expanderIcon).toBeNull();
556
+ });
557
+
558
+ it('should call setIsFullscreenOpen(true) when expander icon is clicked', () => {
559
+ const setIsFullscreenOpen = jest.fn();
560
+ const props = {
561
+ ...defaultProps,
562
+ channel: CHANNELS.WEBPUSH,
563
+ setIsFullscreenOpen,
564
+ };
565
+
566
+ const { container } = render(
567
+ <TestWrapper>
568
+ <PreviewHeader {...props} />
569
+ </TestWrapper>
570
+ );
571
+
572
+ const expanderIcon = container.querySelector('[class*="expander"]') ||
573
+ container.querySelector('.anticon');
574
+ if (expanderIcon) {
575
+ fireEvent.click(expanderIcon);
576
+ expect(setIsFullscreenOpen).toHaveBeenCalledWith(true);
577
+ }
578
+ });
579
+
580
+ it('should render expander icon with ACTIVE class when device is MOBILE', () => {
581
+ const setIsFullscreenOpen = jest.fn();
582
+ const props = {
583
+ ...defaultProps,
584
+ channel: CHANNELS.WEBPUSH,
585
+ device: MOBILE,
586
+ setIsFullscreenOpen,
587
+ };
588
+
589
+ const { container } = render(
590
+ <TestWrapper>
591
+ <PreviewHeader {...props} />
592
+ </TestWrapper>
593
+ );
594
+
595
+ // MOBILE device should give ACTIVE class to expander
596
+ const activeEl = container.querySelector('.active');
597
+ expect(activeEl).toBeTruthy();
598
+ });
599
+
600
+ it('should render expander icon without ACTIVE class when device is DESKTOP', () => {
601
+ const setIsFullscreenOpen = jest.fn();
602
+ const props = {
603
+ ...defaultProps,
604
+ channel: CHANNELS.WEBPUSH,
605
+ device: DESKTOP,
606
+ setIsFullscreenOpen,
607
+ };
608
+
609
+ const { container } = render(
610
+ <TestWrapper>
611
+ <PreviewHeader {...props} />
612
+ </TestWrapper>
613
+ );
614
+
615
+ // DESKTOP device should NOT give ACTIVE class to expander
616
+ const activeEl = container.querySelector('.active');
617
+ expect(activeEl).toBeNull();
618
+ });
619
+
620
+ it('should render expander icon alongside device content for WEBPUSH', () => {
621
+ // Note: suppression of showDeviceToggle for WEBPUSH is done in UnifiedPreview (parent),
622
+ // not in PreviewHeader itself. PreviewHeader only adds the expander icon for WEBPUSH.
623
+ const setIsFullscreenOpen = jest.fn();
624
+ const props = {
625
+ ...defaultProps,
626
+ channel: CHANNELS.WEBPUSH,
627
+ setIsFullscreenOpen,
628
+ showDeviceToggle: false,
629
+ };
630
+
631
+ const { container } = render(
632
+ <TestWrapper>
633
+ <PreviewHeader {...props} />
634
+ </TestWrapper>
635
+ );
636
+
637
+ // expander icon should be present for WEBPUSH
638
+ const expanderIcon = container.querySelector('[class*="expander"]') ||
639
+ container.querySelector('.anticon');
640
+ expect(expanderIcon).toBeTruthy();
641
+ });
642
+
643
+ it('should accept isFullscreenOpen and setIsFullscreenOpen as props', () => {
644
+ const setIsFullscreenOpen = jest.fn();
645
+ const props = {
646
+ ...defaultProps,
647
+ channel: CHANNELS.WEBPUSH,
648
+ isFullscreenOpen: false,
649
+ setIsFullscreenOpen,
650
+ };
651
+
652
+ expect(() =>
653
+ render(
654
+ <TestWrapper>
655
+ <PreviewHeader {...props} />
656
+ </TestWrapper>
657
+ )
658
+ ).not.toThrow();
659
+ });
660
+
661
+ it('should use default setIsFullscreenOpen when not provided', () => {
662
+ const props = {
663
+ ...defaultProps,
664
+ channel: CHANNELS.WEBPUSH,
665
+ };
666
+
667
+ expect(() =>
668
+ render(
669
+ <TestWrapper>
670
+ <PreviewHeader {...props} />
671
+ </TestWrapper>
672
+ )
673
+ ).not.toThrow();
674
+ });
675
+ });
517
676
  });