@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.
- package/package.json +1 -1
- package/v2Components/CommonTestAndPreview/UnifiedPreview/PreviewHeader.js +17 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/WebPushPreviewContent.js +169 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +70 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +44 -5
- package/v2Components/CommonTestAndPreview/constants.js +2 -0
- package/v2Components/CommonTestAndPreview/index.js +51 -2
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/PreviewHeader.test.js +159 -0
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/WebPushPreviewContent.test.js +522 -0
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +255 -0
- package/v2Components/CommonTestAndPreview/tests/constants.test.js +2 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +194 -0
- package/v2Components/FormBuilder/index.js +10 -48
- package/v2Components/TestAndPreviewSlidebox/index.js +2 -2
- package/v2Containers/App/constants.js +3 -0
- package/v2Containers/App/tests/constants.test.js +61 -0
- package/v2Containers/CreativesContainer/index.js +25 -61
- package/v2Containers/Email/index.js +2 -33
- package/v2Containers/Templates/index.js +68 -2
- package/v2Containers/Templates/tests/webpush.test.js +375 -0
- package/v2Containers/WebPush/Create/index.js +91 -8
- package/v2Containers/WebPush/Create/index.scss +7 -0
- package/v2Containers/WebPush/Create/tests/getTemplateContent.test.js +338 -0
- package/v2Containers/WebPush/Create/tests/testAndPreviewIntegration.test.js +325 -0
|
@@ -1,43 +1,4 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
3
|
-
// Isolated input for the email template name field.
|
|
4
|
-
// Manages its own value in local state so keystrokes only re-render this
|
|
5
|
-
// small component, not the entire CreativesContainer → Email → FormBuilder tree.
|
|
6
|
-
class TemplateNameInputField extends React.Component {
|
|
7
|
-
constructor(props) {
|
|
8
|
-
super(props);
|
|
9
|
-
this.state = { localValue: props.initialValue || '' };
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
componentDidUpdate(prevProps) {
|
|
13
|
-
// Sync from props only when the external value changed AND the user hasn't
|
|
14
|
-
// diverged from the previous prop value. This handles async data-load in edit
|
|
15
|
-
// mode without overwriting what the user is actively typing.
|
|
16
|
-
if (
|
|
17
|
-
prevProps.initialValue !== this.props.initialValue &&
|
|
18
|
-
this.state.localValue === (prevProps.initialValue || '')
|
|
19
|
-
) {
|
|
20
|
-
this.setState({ localValue: this.props.initialValue || '' });
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
handleChange = (ev) => {
|
|
25
|
-
const { value } = ev.currentTarget;
|
|
26
|
-
this.setState({ localValue: value });
|
|
27
|
-
this.props.onChange(value);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
render() {
|
|
31
|
-
const { onChange: _onChange, initialValue: _initialValue, ...rest } = this.props;
|
|
32
|
-
return (
|
|
33
|
-
<CapInput
|
|
34
|
-
{...rest}
|
|
35
|
-
value={this.state.localValue}
|
|
36
|
-
onChange={this.handleChange}
|
|
37
|
-
/>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
2
|
import PropTypes from 'prop-types';
|
|
42
3
|
import {
|
|
43
4
|
CAP_SPACE_16, CAP_SPACE_32, CAP_SPACE_56, CAP_SPACE_64,
|
|
@@ -230,10 +191,7 @@ export class Creatives extends React.Component {
|
|
|
230
191
|
// Performance optimized template name update
|
|
231
192
|
performTemplateNameUpdate = (value, formData, onFormDataChange) => {
|
|
232
193
|
const isEmptyTemplateName = !value.trim();
|
|
233
|
-
|
|
234
|
-
// standalone field changed, enabling the fast-path cache in getFormDataForBuilder
|
|
235
|
-
// and skipping the expensive FormBuilder re-render + validateForm cascade.
|
|
236
|
-
const newFormData = { ...formData, 'template-name': value, 'isTemplateNameEdited': true, _highFreqField: 'template-name' };
|
|
194
|
+
const newFormData = { ...formData, 'template-name': value, 'isTemplateNameEdited': true };
|
|
237
195
|
|
|
238
196
|
this.setState({ isTemplateNameEmpty: isEmptyTemplateName });
|
|
239
197
|
onFormDataChange(newFormData);
|
|
@@ -1795,24 +1753,30 @@ export class Creatives extends React.Component {
|
|
|
1795
1753
|
} />
|
|
1796
1754
|
)
|
|
1797
1755
|
|
|
1798
|
-
templateNameComponentInput = ({ formData, onFormDataChange, name }) =>
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1756
|
+
templateNameComponentInput = ({ formData, onFormDataChange, name }) => {
|
|
1757
|
+
// Use local state for immediate UI feedback, fallback to prop value
|
|
1758
|
+
const displayValue = this.state.localTemplateName !== '' ? this.state.localTemplateName : name;
|
|
1759
|
+
|
|
1760
|
+
return (
|
|
1761
|
+
<CapInput
|
|
1762
|
+
value={displayValue}
|
|
1763
|
+
suffix={<span />}
|
|
1764
|
+
onBlur={() => {
|
|
1765
|
+
this.setState({
|
|
1766
|
+
isEditName: false,
|
|
1767
|
+
localTemplateName: '', // Clear local state on blur
|
|
1768
|
+
}, () => {
|
|
1769
|
+
this.showTemplateName({ formData, onFormDataChange });
|
|
1770
|
+
});
|
|
1771
|
+
}}
|
|
1772
|
+
onChange={(ev) => {
|
|
1773
|
+
const { value } = ev.currentTarget;
|
|
1774
|
+
// Use optimized update for better performance
|
|
1775
|
+
this.updateTemplateNameImmediately(value, formData, onFormDataChange);
|
|
1776
|
+
}}
|
|
1777
|
+
/>
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1816
1780
|
|
|
1817
1781
|
showTemplateName = ({ formData, onFormDataChange }) => { //gets called from email/index after template data is fetched
|
|
1818
1782
|
const {
|
|
@@ -795,16 +795,9 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
|
|
|
795
795
|
delete window?.[CREATIVES_S3_ASSET_FILESIZES];
|
|
796
796
|
}
|
|
797
797
|
|
|
798
|
-
|
|
799
|
-
// CreativesContainer.performTemplateNameUpdate passes _highFreqField on the formData object.
|
|
800
|
-
// Both paths set _highFreqUpdate so getFormDataForBuilder can use the fast-path cache.
|
|
801
|
-
onFormDataChange = (updatedFormData, tabCount, currentTab, val) => {
|
|
798
|
+
onFormDataChange = (updatedFormData, tabCount, currentTab) => {
|
|
802
799
|
// this.transformFormData(formData);
|
|
803
800
|
const formData = {...updatedFormData};
|
|
804
|
-
// Consume and clean up the CC-path signal before storing in state
|
|
805
|
-
const highFreqField = (val && val.id) || updatedFormData._highFreqField;
|
|
806
|
-
delete formData._highFreqField;
|
|
807
|
-
|
|
808
801
|
const {defaultData = {}, isFullMode, showTemplateName} = this.props;
|
|
809
802
|
const templateName = formData['template-name'];
|
|
810
803
|
const defaultTemplateName = _.get(defaultData, 'template-name', "");
|
|
@@ -816,9 +809,6 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
|
|
|
816
809
|
formData['template-name'] = templateName;
|
|
817
810
|
}
|
|
818
811
|
|
|
819
|
-
// Must be set before setState so getFormDataForBuilder reads it during the triggered re-render.
|
|
820
|
-
const HIGH_FREQ_FIELDS = ['template-name', 'template-subject'];
|
|
821
|
-
this._highFreqUpdate = !!(highFreqField && HIGH_FREQ_FIELDS.includes(highFreqField));
|
|
822
812
|
|
|
823
813
|
this.setState({formData, tabCount, isSchemaChanged: false}, () => {
|
|
824
814
|
if (this.props.isFullMode && showTemplateName) {
|
|
@@ -831,27 +821,6 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
|
|
|
831
821
|
//this.resetCkEditorInstance(currentTab, formData);
|
|
832
822
|
}
|
|
833
823
|
|
|
834
|
-
// Returns a formData object safe to pass to FormBuilder.
|
|
835
|
-
// For high-frequency field updates (template-name / template-subject) patches only
|
|
836
|
-
// those fields into the existing cache, avoiding an expensive _.cloneDeep of the
|
|
837
|
-
// entire email formData (HTML content, tabs, language variants) on every keystroke.
|
|
838
|
-
// All other operations (tab changes, language add/delete, etc.) still get a full clone.
|
|
839
|
-
getFormDataForBuilder = () => {
|
|
840
|
-
const formData = this.state.formData;
|
|
841
|
-
if (this._highFreqUpdate && this._formDataBuilderCache) {
|
|
842
|
-
this._formDataBuilderCache = {
|
|
843
|
-
...this._formDataBuilderCache,
|
|
844
|
-
'template-name': formData['template-name'],
|
|
845
|
-
'template-subject': formData['template-subject'],
|
|
846
|
-
'isTemplateNameEdited': formData['isTemplateNameEdited'],
|
|
847
|
-
};
|
|
848
|
-
} else {
|
|
849
|
-
this._formDataBuilderCache = _.cloneDeep(formData);
|
|
850
|
-
}
|
|
851
|
-
this._highFreqUpdate = false;
|
|
852
|
-
return this._formDataBuilderCache;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
824
|
onChange = (evt) => {
|
|
856
825
|
const {isFullMode, showTemplateName} = this.props;
|
|
857
826
|
const formData = _.cloneDeep(this.state.formData);
|
|
@@ -3160,7 +3129,7 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
|
|
|
3160
3129
|
onChange={this.onFormDataChange}
|
|
3161
3130
|
currentTab={this.state.currentTab}
|
|
3162
3131
|
parent={this}
|
|
3163
|
-
formData={this.
|
|
3132
|
+
formData={_.cloneDeep(this.state.formData)}
|
|
3164
3133
|
location={this.props.location}
|
|
3165
3134
|
tabKey={this.state.tabKey}
|
|
3166
3135
|
tags={tags}
|
|
@@ -104,6 +104,9 @@ import {
|
|
|
104
104
|
VIBER as VIBER_CHANNEL,
|
|
105
105
|
FACEBOOK as FACEBOOK_CHANNEL,
|
|
106
106
|
CREATE,
|
|
107
|
+
EXTERNAL_URL,
|
|
108
|
+
URL,
|
|
109
|
+
SITE_URL,
|
|
107
110
|
} from '../App/constants';
|
|
108
111
|
import {MAX_WHATSAPP_TEMPLATES, WARNING_WHATSAPP_TEMPLATES , ACCOUNT_MAPPING_ON_CHANNEL, noFilteredWhatsappZaloTemplatesTitle, noFilteredWhatsappZaloTemplatesDesc, noApprovedWhatsappZaloTemplatesTitle, noApprovedWhatsappTemplatesDesc, zaloDescIllustration, noApprovedRcsTemplatesTitle, noApprovedRcsTemplatesDesc, ARCHIVE_STATUS_ACTIVE, ARCHIVE_STATUS_ARCHIVED, ARCHIVE_REFRESH_TYPE_ARCHIVE, ARCHIVE_REFRESH_TYPE_UNARCHIVE} from './constants';
|
|
109
112
|
import { COPY_OF, EMBEDDED } from '../../constants/unified';
|
|
@@ -1399,6 +1402,69 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1399
1402
|
};
|
|
1400
1403
|
}
|
|
1401
1404
|
|
|
1405
|
+
case WEBPUSH: {
|
|
1406
|
+
// WebPush content is stored in creatives format (brandIcon, onClickAction, ctas)
|
|
1407
|
+
// Must be transformed to campaign/test-message format matching getTemplateContent() in WebPush/Create/index.js
|
|
1408
|
+
const webpushContent = get(baseContent, 'content.webpush', {});
|
|
1409
|
+
const title = webpushContent?.title || '';
|
|
1410
|
+
const message = webpushContent?.message || '';
|
|
1411
|
+
const accountId = get(template, 'definition.accountId', null);
|
|
1412
|
+
const templateName = template?.name || '';
|
|
1413
|
+
|
|
1414
|
+
// iconImageUrl stored as brandIcon in creatives format
|
|
1415
|
+
const iconImageUrl = webpushContent?.brandIcon || webpushContent?.iconImageUrl || undefined;
|
|
1416
|
+
|
|
1417
|
+
// cta stored as onClickAction in creatives format (type: URL|SITE_URL, url)
|
|
1418
|
+
// or already as cta in campaign format (type: EXTERNAL_URL|SITE_URL, actionLink)
|
|
1419
|
+
let cta = null;
|
|
1420
|
+
const onClickAction = webpushContent?.onClickAction;
|
|
1421
|
+
const existingCta = webpushContent?.cta;
|
|
1422
|
+
if (onClickAction) {
|
|
1423
|
+
const ctaType = onClickAction.type === URL ? EXTERNAL_URL : (onClickAction.type || SITE_URL);
|
|
1424
|
+
cta = { type: ctaType, actionLink: onClickAction.url || '' };
|
|
1425
|
+
} else if (existingCta) {
|
|
1426
|
+
cta = { type: existingCta.type || EXTERNAL_URL, actionLink: existingCta.actionLink || '' };
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// expandableDetails: image → media[], ctas[] → mapped ctas
|
|
1430
|
+
const image = webpushContent?.image;
|
|
1431
|
+
const rawCtas = webpushContent?.ctas;
|
|
1432
|
+
const existingExpandable = webpushContent?.expandableDetails;
|
|
1433
|
+
let expandableDetails = null;
|
|
1434
|
+
const hasImage = !!image;
|
|
1435
|
+
const hasCtas = Array.isArray(rawCtas) && rawCtas.length > 0;
|
|
1436
|
+
if (hasImage || hasCtas) {
|
|
1437
|
+
expandableDetails = {
|
|
1438
|
+
media: hasImage ? [{ url: image, type: IMAGE }] : [],
|
|
1439
|
+
ctas: hasCtas ? rawCtas.map((ctaItem) => ({
|
|
1440
|
+
type: ctaItem?.type === URL ? EXTERNAL_URL : (ctaItem?.type || EXTERNAL_URL),
|
|
1441
|
+
action: ctaItem?.action || '',
|
|
1442
|
+
title: ctaItem?.actionText || ctaItem?.title || '',
|
|
1443
|
+
actionLink: ctaItem?.actionLink || '',
|
|
1444
|
+
})) : [],
|
|
1445
|
+
};
|
|
1446
|
+
} else if (existingExpandable) {
|
|
1447
|
+
expandableDetails = {
|
|
1448
|
+
media: existingExpandable?.media || [],
|
|
1449
|
+
ctas: existingExpandable?.ctas || [],
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
return {
|
|
1454
|
+
channel: WEBPUSH,
|
|
1455
|
+
accountId,
|
|
1456
|
+
content: {
|
|
1457
|
+
title,
|
|
1458
|
+
message,
|
|
1459
|
+
...(iconImageUrl ? { iconImageUrl } : {}),
|
|
1460
|
+
...(cta ? { cta } : {}),
|
|
1461
|
+
...(expandableDetails ? { expandableDetails } : {}),
|
|
1462
|
+
},
|
|
1463
|
+
messageSubject: templateName || title,
|
|
1464
|
+
offers: [],
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1402
1468
|
default:
|
|
1403
1469
|
console.warn(`Unsupported channel for content extraction: ${channelUpper}`);
|
|
1404
1470
|
return null;
|
|
@@ -1411,7 +1477,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1411
1477
|
* @returns {Boolean} - True if channel supports Test and Preview
|
|
1412
1478
|
*/
|
|
1413
1479
|
isTestAndPreviewSupported = () => {
|
|
1414
|
-
const supportedChannels = [EMAIL, SMS, INAPP, MOBILE_PUSH, VIBER, ZALO];
|
|
1480
|
+
const supportedChannels = [EMAIL, SMS, INAPP, MOBILE_PUSH, VIBER, ZALO, WEBPUSH];
|
|
1415
1481
|
return supportedChannels.includes(this.state.channel.toUpperCase());
|
|
1416
1482
|
}
|
|
1417
1483
|
|
|
@@ -1971,7 +2037,7 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1971
2037
|
// Show preview icon only for channels that don't support Test and Preview
|
|
1972
2038
|
(() => {
|
|
1973
2039
|
// Channels that have Test and Preview integrated
|
|
1974
|
-
const testAndPreviewChannels = [EMAIL, SMS, WHATSAPP, RCS, INAPP, MOBILE_PUSH, VIBER, ZALO];
|
|
2040
|
+
const testAndPreviewChannels = [EMAIL, SMS, WHATSAPP, RCS, INAPP, MOBILE_PUSH, VIBER, ZALO, WEBPUSH];
|
|
1975
2041
|
const isTestAndPreviewSupported = testAndPreviewChannels.includes(currentChannel.toUpperCase());
|
|
1976
2042
|
|
|
1977
2043
|
// Don't show preview icon if channel supports Test and Preview
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Templates container - WEBPUSH channel additions
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - extractTemplateContentForPreview for WEBPUSH channel
|
|
6
|
+
* - isTestAndPreviewSupported with WEBPUSH channel
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import { shallowWithIntl } from '../../../helpers/intl-enzym-test-helpers';
|
|
11
|
+
import { Templates } from '../index';
|
|
12
|
+
|
|
13
|
+
jest.mock('../../CreativesContainer', () => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: (props) => (
|
|
16
|
+
<div className="creatives-container-mock" {...props}>
|
|
17
|
+
CreativesContainer
|
|
18
|
+
</div>
|
|
19
|
+
),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
// Helper: build a valid template for WEBPUSH
|
|
23
|
+
// extractTemplateContentForPreview reads:
|
|
24
|
+
// - template.versions.base.content.webpush → webpushContent
|
|
25
|
+
// - template.definition.accountId → accountId
|
|
26
|
+
// - template.name → templateName
|
|
27
|
+
const makeTemplate = (webpush = {}, extra = {}) => ({
|
|
28
|
+
name: 'My Template',
|
|
29
|
+
definition: { accountId: 'acc-123' },
|
|
30
|
+
versions: {
|
|
31
|
+
base: {
|
|
32
|
+
content: { webpush },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
...extra,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('Templates - WEBPUSH channel', () => {
|
|
39
|
+
const mockActions = {
|
|
40
|
+
getWeCrmAccounts: jest.fn(),
|
|
41
|
+
setChannelAccount: jest.fn(),
|
|
42
|
+
getAllTemplates: jest.fn(),
|
|
43
|
+
getUserList: jest.fn(),
|
|
44
|
+
getSenderDetails: jest.fn(),
|
|
45
|
+
resetTemplate: jest.fn(),
|
|
46
|
+
setArchivedMode: jest.fn(),
|
|
47
|
+
clearTemplateSelection: jest.fn(),
|
|
48
|
+
toggleTemplateSelection: jest.fn(),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const baseProps = {
|
|
52
|
+
route: { name: 'webpush' },
|
|
53
|
+
Templates: { templates: [] },
|
|
54
|
+
actions: mockActions,
|
|
55
|
+
location: { pathname: '/webpush', query: {}, search: '' },
|
|
56
|
+
EmailCreate: { duplicateTemplateInProgress: false },
|
|
57
|
+
isFullMode: false,
|
|
58
|
+
intl: { formatMessage: jest.fn((msg) => msg.defaultMessage || '') },
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
jest.clearAllMocks();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const renderComponent = (channel = 'webpush') => {
|
|
66
|
+
const props = { ...baseProps, route: { name: channel } };
|
|
67
|
+
return shallowWithIntl(<Templates {...props} />);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
71
|
+
describe('isTestAndPreviewSupported', () => {
|
|
72
|
+
it('should return true for WEBPUSH channel', () => {
|
|
73
|
+
const component = renderComponent('webpush');
|
|
74
|
+
component.setState({ channel: 'webpush' });
|
|
75
|
+
expect(component.instance().isTestAndPreviewSupported()).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should return true for WEBPUSH in uppercase state', () => {
|
|
79
|
+
const component = renderComponent('webpush');
|
|
80
|
+
component.setState({ channel: 'WEBPUSH' });
|
|
81
|
+
expect(component.instance().isTestAndPreviewSupported()).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should return true for EMAIL channel', () => {
|
|
85
|
+
const component = renderComponent('webpush');
|
|
86
|
+
component.setState({ channel: 'email' });
|
|
87
|
+
expect(component.instance().isTestAndPreviewSupported()).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should return true for SMS channel', () => {
|
|
91
|
+
const component = renderComponent('webpush');
|
|
92
|
+
component.setState({ channel: 'sms' });
|
|
93
|
+
expect(component.instance().isTestAndPreviewSupported()).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return true for INAPP channel', () => {
|
|
97
|
+
const component = renderComponent('webpush');
|
|
98
|
+
component.setState({ channel: 'inapp' });
|
|
99
|
+
expect(component.instance().isTestAndPreviewSupported()).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should return true for VIBER channel', () => {
|
|
103
|
+
const component = renderComponent('webpush');
|
|
104
|
+
component.setState({ channel: 'viber' });
|
|
105
|
+
expect(component.instance().isTestAndPreviewSupported()).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return true for ZALO channel', () => {
|
|
109
|
+
const component = renderComponent('webpush');
|
|
110
|
+
component.setState({ channel: 'zalo' });
|
|
111
|
+
expect(component.instance().isTestAndPreviewSupported()).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should return false for WHATSAPP channel', () => {
|
|
115
|
+
const component = renderComponent('webpush');
|
|
116
|
+
component.setState({ channel: 'whatsapp' });
|
|
117
|
+
expect(component.instance().isTestAndPreviewSupported()).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should return false for RCS channel', () => {
|
|
121
|
+
const component = renderComponent('webpush');
|
|
122
|
+
component.setState({ channel: 'rcs' });
|
|
123
|
+
expect(component.instance().isTestAndPreviewSupported()).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
128
|
+
describe('extractTemplateContentForPreview - WEBPUSH channel', () => {
|
|
129
|
+
it('should return null when template has no versions.base', () => {
|
|
130
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
131
|
+
const component = renderComponent('webpush');
|
|
132
|
+
const result = component.instance().extractTemplateContentForPreview(
|
|
133
|
+
{ name: 'T', definition: {} },
|
|
134
|
+
'WEBPUSH'
|
|
135
|
+
);
|
|
136
|
+
expect(result).toBeNull();
|
|
137
|
+
consoleSpy.mockRestore();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should extract title and message from versions.base.content.webpush', () => {
|
|
141
|
+
const component = renderComponent('webpush');
|
|
142
|
+
const template = makeTemplate({ title: 'Push Title', message: 'Push body' });
|
|
143
|
+
|
|
144
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
145
|
+
expect(result).toBeDefined();
|
|
146
|
+
expect(result.channel).toBe('WEBPUSH');
|
|
147
|
+
expect(result.accountId).toBe('acc-123');
|
|
148
|
+
expect(result.content.title).toBe('Push Title');
|
|
149
|
+
expect(result.content.message).toBe('Push body');
|
|
150
|
+
expect(result.messageSubject).toBe('My Template');
|
|
151
|
+
expect(result.offers).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should fall back to title for messageSubject when template name is empty', () => {
|
|
155
|
+
const component = renderComponent('webpush');
|
|
156
|
+
const template = makeTemplate({ title: 'Title Only', message: 'M' }, { name: '' });
|
|
157
|
+
|
|
158
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
159
|
+
expect(result.messageSubject).toBe('Title Only');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should extract brandIcon as iconImageUrl', () => {
|
|
163
|
+
const component = renderComponent('webpush');
|
|
164
|
+
const template = makeTemplate({
|
|
165
|
+
title: 'T',
|
|
166
|
+
message: 'M',
|
|
167
|
+
brandIcon: 'https://example.com/brand.png',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
171
|
+
expect(result.content.iconImageUrl).toBe('https://example.com/brand.png');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should use iconImageUrl field when brandIcon is absent', () => {
|
|
175
|
+
const component = renderComponent('webpush');
|
|
176
|
+
const template = makeTemplate({
|
|
177
|
+
title: 'T',
|
|
178
|
+
message: 'M',
|
|
179
|
+
iconImageUrl: 'https://example.com/icon.png',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
183
|
+
expect(result.content.iconImageUrl).toBe('https://example.com/icon.png');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should NOT include iconImageUrl when neither brandIcon nor iconImageUrl present', () => {
|
|
187
|
+
const component = renderComponent('webpush');
|
|
188
|
+
const template = makeTemplate({ title: 'T', message: 'M' });
|
|
189
|
+
|
|
190
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
191
|
+
expect(result.content.iconImageUrl).toBeUndefined();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should convert onClickAction type URL to EXTERNAL_URL cta', () => {
|
|
195
|
+
const component = renderComponent('webpush');
|
|
196
|
+
const template = makeTemplate({
|
|
197
|
+
title: 'T',
|
|
198
|
+
message: 'M',
|
|
199
|
+
onClickAction: { type: 'URL', url: 'https://example.com' },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
203
|
+
expect(result.content.cta).toEqual({ type: 'EXTERNAL_URL', actionLink: 'https://example.com' });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should preserve onClickAction type SITE_URL as-is', () => {
|
|
207
|
+
const component = renderComponent('webpush');
|
|
208
|
+
const template = makeTemplate({
|
|
209
|
+
title: 'T',
|
|
210
|
+
message: 'M',
|
|
211
|
+
onClickAction: { type: 'SITE_URL', url: 'https://site.com' },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
215
|
+
expect(result.content.cta).toEqual({ type: 'SITE_URL', actionLink: 'https://site.com' });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should use existingCta when onClickAction is absent', () => {
|
|
219
|
+
const component = renderComponent('webpush');
|
|
220
|
+
const template = makeTemplate({
|
|
221
|
+
title: 'T',
|
|
222
|
+
message: 'M',
|
|
223
|
+
cta: { type: 'EXTERNAL_URL', actionLink: 'https://existing.com' },
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
227
|
+
expect(result.content.cta).toEqual({ type: 'EXTERNAL_URL', actionLink: 'https://existing.com' });
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should default existingCta type to EXTERNAL_URL when type is missing', () => {
|
|
231
|
+
const component = renderComponent('webpush');
|
|
232
|
+
const template = makeTemplate({
|
|
233
|
+
title: 'T',
|
|
234
|
+
message: 'M',
|
|
235
|
+
cta: { actionLink: 'https://example.com' },
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
239
|
+
expect(result.content.cta.type).toBe('EXTERNAL_URL');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should NOT include cta when neither onClickAction nor cta present', () => {
|
|
243
|
+
const component = renderComponent('webpush');
|
|
244
|
+
const template = makeTemplate({ title: 'T', message: 'M' });
|
|
245
|
+
|
|
246
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
247
|
+
expect(result.content.cta).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should build expandableDetails from image and ctas', () => {
|
|
251
|
+
const component = renderComponent('webpush');
|
|
252
|
+
const template = makeTemplate({
|
|
253
|
+
title: 'T',
|
|
254
|
+
message: 'M',
|
|
255
|
+
image: 'https://example.com/image.jpg',
|
|
256
|
+
ctas: [{ type: 'URL', actionText: 'Click', actionLink: 'https://a.com', action: '' }],
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
260
|
+
expect(result.content.expandableDetails).toBeDefined();
|
|
261
|
+
expect(result.content.expandableDetails.media).toEqual([
|
|
262
|
+
{ url: 'https://example.com/image.jpg', type: 'IMAGE' },
|
|
263
|
+
]);
|
|
264
|
+
expect(result.content.expandableDetails.ctas[0].title).toBe('Click');
|
|
265
|
+
expect(result.content.expandableDetails.ctas[0].type).toBe('EXTERNAL_URL');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should convert CTA type URL → EXTERNAL_URL in expandableDetails.ctas', () => {
|
|
269
|
+
const component = renderComponent('webpush');
|
|
270
|
+
const template = makeTemplate({
|
|
271
|
+
title: 'T',
|
|
272
|
+
message: 'M',
|
|
273
|
+
ctas: [{ type: 'URL', actionText: 'Go', actionLink: 'https://x.com' }],
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
277
|
+
expect(result.content.expandableDetails.ctas[0].type).toBe('EXTERNAL_URL');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should keep EXTERNAL_URL type unchanged in expandableDetails.ctas', () => {
|
|
281
|
+
const component = renderComponent('webpush');
|
|
282
|
+
const template = makeTemplate({
|
|
283
|
+
title: 'T',
|
|
284
|
+
message: 'M',
|
|
285
|
+
ctas: [{ type: 'EXTERNAL_URL', title: 'Go', actionLink: 'https://x.com' }],
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
289
|
+
expect(result.content.expandableDetails.ctas[0].type).toBe('EXTERNAL_URL');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should build image-only expandableDetails when no ctas', () => {
|
|
293
|
+
const component = renderComponent('webpush');
|
|
294
|
+
const template = makeTemplate({
|
|
295
|
+
title: 'T',
|
|
296
|
+
message: 'M',
|
|
297
|
+
image: 'https://example.com/img.jpg',
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
301
|
+
expect(result.content.expandableDetails.media).toHaveLength(1);
|
|
302
|
+
expect(result.content.expandableDetails.ctas).toEqual([]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should build ctas-only expandableDetails when no image', () => {
|
|
306
|
+
const component = renderComponent('webpush');
|
|
307
|
+
const template = makeTemplate({
|
|
308
|
+
title: 'T',
|
|
309
|
+
message: 'M',
|
|
310
|
+
ctas: [{ type: 'EXTERNAL_URL', actionText: 'Btn', actionLink: 'https://b.com' }],
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
314
|
+
expect(result.content.expandableDetails.media).toEqual([]);
|
|
315
|
+
expect(result.content.expandableDetails.ctas).toHaveLength(1);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should use existingExpandable when no image and no ctas', () => {
|
|
319
|
+
const component = renderComponent('webpush');
|
|
320
|
+
const existingExpandable = {
|
|
321
|
+
media: [{ url: 'https://example.com/existing.jpg', type: 'IMAGE' }],
|
|
322
|
+
ctas: [{ title: 'Existing', actionLink: 'https://existing.com', type: 'EXTERNAL_URL' }],
|
|
323
|
+
};
|
|
324
|
+
const template = makeTemplate({
|
|
325
|
+
title: 'T',
|
|
326
|
+
message: 'M',
|
|
327
|
+
expandableDetails: existingExpandable,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
331
|
+
expect(result.content.expandableDetails).toEqual(existingExpandable);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should NOT include expandableDetails when none present', () => {
|
|
335
|
+
const component = renderComponent('webpush');
|
|
336
|
+
const template = makeTemplate({ title: 'T', message: 'M' });
|
|
337
|
+
|
|
338
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
339
|
+
expect(result.content.expandableDetails).toBeUndefined();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should handle empty webpush content gracefully', () => {
|
|
343
|
+
const component = renderComponent('webpush');
|
|
344
|
+
const template = makeTemplate({}, { name: '' });
|
|
345
|
+
|
|
346
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
347
|
+
expect(result).toBeDefined();
|
|
348
|
+
expect(result.content.title).toBe('');
|
|
349
|
+
expect(result.content.message).toBe('');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should handle missing definition.accountId gracefully (returns null)', () => {
|
|
353
|
+
const component = renderComponent('webpush');
|
|
354
|
+
const template = {
|
|
355
|
+
name: 'T',
|
|
356
|
+
versions: { base: { content: { webpush: { title: 'T', message: 'M' } } } },
|
|
357
|
+
// no definition
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'WEBPUSH');
|
|
361
|
+
expect(result).toBeDefined();
|
|
362
|
+
expect(result.accountId).toBeNull();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should return null for unsupported channel', () => {
|
|
366
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
367
|
+
const component = renderComponent('webpush');
|
|
368
|
+
const template = makeTemplate({ title: 'T', message: 'M' });
|
|
369
|
+
|
|
370
|
+
const result = component.instance().extractTemplateContentForPreview(template, 'UNSUPPORTED_CHANNEL');
|
|
371
|
+
expect(result).toBeNull();
|
|
372
|
+
consoleSpy.mockRestore();
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|