@capillarytech/creatives-library 8.0.249 → 8.0.250-alpha.0
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/assets/Android.png +0 -0
- package/assets/iOS.png +0 -0
- package/constants/unified.js +2 -1
- package/initialReducer.js +2 -0
- package/package.json +1 -1
- package/services/api.js +10 -0
- package/services/tests/api.test.js +18 -0
- package/utils/common.js +5 -0
- package/utils/commonUtils.js +28 -5
- package/utils/tests/commonUtil.test.js +224 -0
- package/utils/transformTemplateConfig.js +0 -10
- package/v2Components/CapDeviceContent/index.js +61 -56
- package/v2Components/CapTagList/index.js +6 -1
- package/v2Components/CapTagListWithInput/index.js +5 -1
- package/v2Components/CapTagListWithInput/messages.js +1 -1
- package/v2Components/CapWhatsappCTA/tests/index.test.js +5 -0
- package/v2Components/ErrorInfoNote/index.js +452 -72
- package/v2Components/ErrorInfoNote/messages.js +22 -0
- package/v2Components/ErrorInfoNote/style.scss +280 -4
- package/v2Components/FormBuilder/tests/index.test.js +13 -4
- package/v2Components/HtmlEditor/HTMLEditor.js +640 -94
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +874 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1167 -133
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +27 -16
- package/v2Components/HtmlEditor/_htmlEditor.scss +108 -45
- package/v2Components/HtmlEditor/_index.lazy.scss +1 -1
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +13 -101
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +148 -139
- package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +2 -1
- package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
- package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +9 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/index.js +1 -1
- package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +22 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +4 -7
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +35 -45
- package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +1 -3
- package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
- package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +7 -6
- package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +3 -6
- package/v2Components/HtmlEditor/components/PreviewPane/index.js +11 -13
- package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +49 -31
- package/v2Components/HtmlEditor/components/ValidationPanel/index.js +68 -39
- package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +254 -0
- package/v2Components/HtmlEditor/components/ValidationTabs/index.js +391 -0
- package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +51 -0
- package/v2Components/HtmlEditor/constants.js +42 -20
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +373 -16
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.apiErrors.test.js +795 -0
- package/v2Components/HtmlEditor/hooks/useEditorContent.js +5 -2
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +88 -146
- package/v2Components/HtmlEditor/hooks/useValidation.js +189 -53
- package/v2Components/HtmlEditor/index.js +1 -1
- package/v2Components/HtmlEditor/messages.js +95 -85
- package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +94 -45
- package/v2Components/HtmlEditor/utils/contentSanitizer.js +40 -41
- package/v2Components/HtmlEditor/utils/htmlValidator.js +71 -72
- package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +134 -102
- package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +23 -25
- package/v2Components/HtmlEditor/utils/validationAdapter.js +66 -41
- package/v2Components/MobilePushPreviewV2/index.js +32 -7
- package/v2Components/TemplatePreview/_templatePreview.scss +44 -24
- package/v2Components/TemplatePreview/index.js +47 -32
- package/v2Components/TemplatePreview/messages.js +4 -0
- package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +1 -0
- package/v2Containers/BeeEditor/index.js +172 -90
- package/v2Containers/BeePopupEditor/constants.js +10 -0
- package/v2Containers/BeePopupEditor/index.js +193 -0
- package/v2Containers/BeePopupEditor/tests/index.test.js +627 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +127 -51
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +163 -13
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +2 -1
- package/v2Containers/CreativesContainer/constants.js +1 -0
- package/v2Containers/CreativesContainer/index.js +239 -46
- package/v2Containers/CreativesContainer/messages.js +8 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +11 -2
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +38 -50
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +106 -0
- package/v2Containers/Email/actions.js +7 -0
- package/v2Containers/Email/constants.js +5 -1
- package/v2Containers/Email/index.js +222 -27
- package/v2Containers/Email/messages.js +32 -0
- package/v2Containers/Email/reducer.js +12 -1
- package/v2Containers/Email/sagas.js +61 -7
- package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -0
- package/v2Containers/Email/tests/sagas.test.js +320 -29
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1321 -0
- package/v2Containers/EmailWrapper/components/EmailWrapperView.js +210 -15
- package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +40 -74
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +1749 -0
- package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +520 -0
- package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +2 -67
- package/v2Containers/EmailWrapper/constants.js +2 -0
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +629 -77
- package/v2Containers/EmailWrapper/index.js +103 -23
- package/v2Containers/EmailWrapper/messages.js +61 -1
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +643 -0
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +594 -77
- package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +376 -0
- package/v2Containers/InApp/__tests__/sagas.test.js +363 -0
- package/v2Containers/InApp/actions.js +7 -0
- package/v2Containers/InApp/constants.js +20 -4
- package/v2Containers/InApp/index.js +802 -359
- package/v2Containers/InApp/index.scss +4 -3
- package/v2Containers/InApp/messages.js +7 -3
- package/v2Containers/InApp/reducer.js +21 -3
- package/v2Containers/InApp/sagas.js +29 -9
- package/v2Containers/InApp/selectors.js +25 -5
- package/v2Containers/InApp/tests/index.test.js +154 -50
- package/v2Containers/InApp/tests/reducer.test.js +34 -0
- package/v2Containers/InApp/tests/sagas.test.js +61 -9
- package/v2Containers/InApp/tests/selectors.test.js +612 -0
- package/v2Containers/InAppWrapper/components/InAppWrapperView.js +162 -0
- package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +267 -0
- package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +9 -0
- package/v2Containers/InAppWrapper/constants.js +16 -0
- package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +473 -0
- package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +198 -0
- package/v2Containers/InAppWrapper/index.js +148 -0
- package/v2Containers/InAppWrapper/messages.js +49 -0
- package/v2Containers/InappAdvance/index.js +1099 -0
- package/v2Containers/InappAdvance/index.scss +10 -0
- package/v2Containers/InappAdvance/tests/index.test.js +448 -0
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
- package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +2 -0
- package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +9 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +12 -0
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4 -0
- package/v2Containers/TagList/index.js +62 -19
- package/v2Containers/Templates/_templates.scss +60 -1
- package/v2Containers/Templates/index.js +89 -4
- package/v2Containers/Templates/messages.js +4 -0
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +0 -152
- package/v2Containers/EmailWrapper/tests/EmailWrapperView.test.js +0 -214
|
@@ -0,0 +1,1749 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmailHTMLEditor Component Tests
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive tests to cover all uncovered lines in EmailHTMLEditor component
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import {
|
|
9
|
+
render, screen, fireEvent, waitFor, act,
|
|
10
|
+
} from '@testing-library/react';
|
|
11
|
+
import '@testing-library/jest-dom';
|
|
12
|
+
import { IntlProvider } from 'react-intl';
|
|
13
|
+
import EmailHTMLEditor from '../EmailHTMLEditor';
|
|
14
|
+
import { validateLiquidTemplateContent } from '../../../../utils/commonUtils';
|
|
15
|
+
import { validateTags } from '../../../../utils/tagValidations';
|
|
16
|
+
import { isEmailUnsubscribeTagMandatory } from '../../../../utils/common';
|
|
17
|
+
|
|
18
|
+
// Mock dependencies
|
|
19
|
+
jest.mock('../../../../utils/commonUtils', () => ({
|
|
20
|
+
validateLiquidTemplateContent: jest.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
jest.mock('../../../../utils/tagValidations', () => ({
|
|
24
|
+
validateTags: jest.fn(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// Create mutable mock for hasLiquidSupportFeature
|
|
28
|
+
let mockHasLiquidSupportFeature = jest.fn(() => true);
|
|
29
|
+
jest.mock('../../../../utils/common', () => ({
|
|
30
|
+
hasLiquidSupportFeature: (...args) => mockHasLiquidSupportFeature(...args),
|
|
31
|
+
isEmailUnsubscribeTagMandatory: jest.fn(() => false),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
jest.mock('../../../../utils/history', () => ({
|
|
35
|
+
push: jest.fn(),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
jest.mock('@capillarytech/cap-ui-library/CapNotification', () => ({
|
|
39
|
+
error: jest.fn(),
|
|
40
|
+
success: jest.fn(),
|
|
41
|
+
warning: jest.fn(),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// Create mutable mock functions that can be controlled in tests
|
|
45
|
+
let mockGetAllIssues = jest.fn(() => []);
|
|
46
|
+
let mockGetValidationState = jest.fn(() => ({
|
|
47
|
+
isValidating: false,
|
|
48
|
+
hasErrors: false,
|
|
49
|
+
issueCounts: {
|
|
50
|
+
html: 0, label: 0, liquid: 0, total: 0,
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
// Mock HtmlEditor - it exports a lazy-loaded component by default
|
|
55
|
+
jest.mock('../../../../v2Components/HtmlEditor/index.lazy', () => {
|
|
56
|
+
const React = require('react');
|
|
57
|
+
const MockHTMLEditor = React.forwardRef((props, ref) => {
|
|
58
|
+
React.useImperativeHandle(ref, () => ({
|
|
59
|
+
getAllIssues: () => mockGetAllIssues(),
|
|
60
|
+
getValidationState: () => mockGetValidationState(),
|
|
61
|
+
getValidation: () => mockGetValidationState(),
|
|
62
|
+
isContentEmpty: () => !props.initialContent || (typeof props.initialContent === 'string' && props.initialContent.trim() === ''),
|
|
63
|
+
getIssueCounts: () => {
|
|
64
|
+
const issues = mockGetAllIssues();
|
|
65
|
+
return {
|
|
66
|
+
html: issues.filter(i => i.source === 'htmlhint' || i.source === 'css-validator').length,
|
|
67
|
+
label: issues.filter(i => i.rule === 'tag-pair' || i.message?.includes('tag must be paired')).length,
|
|
68
|
+
liquid: issues.filter(i => i.source === 'liquid-validator').length,
|
|
69
|
+
total: issues.length,
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div data-testid="html-editor">
|
|
76
|
+
<button
|
|
77
|
+
onClick={() => props.onContentChange && props.onContentChange('<p>New content</p>')}
|
|
78
|
+
data-testid="trigger-content-change"
|
|
79
|
+
>
|
|
80
|
+
Change Content
|
|
81
|
+
</button>
|
|
82
|
+
<button
|
|
83
|
+
onClick={() => props.onSave && props.onSave()}
|
|
84
|
+
data-testid="trigger-save"
|
|
85
|
+
>
|
|
86
|
+
Save
|
|
87
|
+
</button>
|
|
88
|
+
<button
|
|
89
|
+
onClick={() => props.onErrorAcknowledged && props.onErrorAcknowledged()}
|
|
90
|
+
data-testid="trigger-error-acknowledged"
|
|
91
|
+
>
|
|
92
|
+
Acknowledge Error
|
|
93
|
+
</button>
|
|
94
|
+
<button
|
|
95
|
+
onClick={() => props.onValidationChange && props.onValidationChange({
|
|
96
|
+
isContentEmpty: false,
|
|
97
|
+
issueCounts: {
|
|
98
|
+
html: 0, label: 0, liquid: 0, total: 0,
|
|
99
|
+
},
|
|
100
|
+
validationComplete: true,
|
|
101
|
+
hasErrors: false,
|
|
102
|
+
})}
|
|
103
|
+
data-testid="trigger-validation-change"
|
|
104
|
+
>
|
|
105
|
+
Validation Change
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
MockHTMLEditor.displayName = 'MockHTMLEditor';
|
|
111
|
+
return MockHTMLEditor;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Also mock the main index.js which exports the lazy version
|
|
115
|
+
jest.mock('../../../../v2Components/HtmlEditor', () => {
|
|
116
|
+
const React = require('react');
|
|
117
|
+
const MockHTMLEditor = React.forwardRef((props, ref) => {
|
|
118
|
+
React.useImperativeHandle(ref, () => ({
|
|
119
|
+
getAllIssues: () => mockGetAllIssues(),
|
|
120
|
+
getValidationState: () => mockGetValidationState(),
|
|
121
|
+
getValidation: () => mockGetValidationState(),
|
|
122
|
+
isContentEmpty: () => !props.initialContent || (typeof props.initialContent === 'string' && props.initialContent.trim() === ''),
|
|
123
|
+
getIssueCounts: () => {
|
|
124
|
+
const issues = mockGetAllIssues();
|
|
125
|
+
return {
|
|
126
|
+
html: issues.filter(i => i.source === 'htmlhint' || i.source === 'css-validator').length,
|
|
127
|
+
label: issues.filter(i => i.rule === 'tag-pair' || i.message?.includes('tag must be paired')).length,
|
|
128
|
+
liquid: issues.filter(i => i.source === 'liquid-validator').length,
|
|
129
|
+
total: issues.length,
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div data-testid="html-editor">
|
|
136
|
+
<button
|
|
137
|
+
onClick={() => props.onContentChange && props.onContentChange('<p>New content</p>')}
|
|
138
|
+
data-testid="trigger-content-change"
|
|
139
|
+
>
|
|
140
|
+
Change Content
|
|
141
|
+
</button>
|
|
142
|
+
<button
|
|
143
|
+
onClick={() => props.onSave && props.onSave()}
|
|
144
|
+
data-testid="trigger-save"
|
|
145
|
+
>
|
|
146
|
+
Save
|
|
147
|
+
</button>
|
|
148
|
+
<button
|
|
149
|
+
onClick={() => props.onErrorAcknowledged && props.onErrorAcknowledged()}
|
|
150
|
+
data-testid="trigger-error-acknowledged"
|
|
151
|
+
>
|
|
152
|
+
Acknowledge Error
|
|
153
|
+
</button>
|
|
154
|
+
<button
|
|
155
|
+
onClick={() => props.onValidationChange && props.onValidationChange({
|
|
156
|
+
isContentEmpty: false,
|
|
157
|
+
issueCounts: {
|
|
158
|
+
html: 0, label: 0, liquid: 0, total: 0,
|
|
159
|
+
},
|
|
160
|
+
validationComplete: true,
|
|
161
|
+
hasErrors: false,
|
|
162
|
+
})}
|
|
163
|
+
data-testid="trigger-validation-change"
|
|
164
|
+
>
|
|
165
|
+
Validation Change
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
MockHTMLEditor.displayName = 'MockHTMLEditor';
|
|
171
|
+
return {
|
|
172
|
+
__esModule: true,
|
|
173
|
+
default: MockHTMLEditor,
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
jest.mock('../../../../v2Components/CapTagListWithInput', () => {
|
|
178
|
+
const React = require('react');
|
|
179
|
+
return function MockCapTagListWithInput(props) {
|
|
180
|
+
const [value, setValue] = React.useState(props.inputValue || '');
|
|
181
|
+
React.useEffect(() => {
|
|
182
|
+
setValue(props.inputValue || '');
|
|
183
|
+
}, [props.inputValue]);
|
|
184
|
+
return (
|
|
185
|
+
<div data-testid="cap-tag-list-input">
|
|
186
|
+
<input
|
|
187
|
+
id="template-subject"
|
|
188
|
+
data-testid="subject-input"
|
|
189
|
+
value={value}
|
|
190
|
+
onChange={(e) => {
|
|
191
|
+
setValue(e.target.value);
|
|
192
|
+
if (props.inputOnChange) {
|
|
193
|
+
props.inputOnChange(e);
|
|
194
|
+
}
|
|
195
|
+
}}
|
|
196
|
+
placeholder={props.inputPlaceholder}
|
|
197
|
+
/>
|
|
198
|
+
<button
|
|
199
|
+
onClick={() => props.onTagSelect && props.onTagSelect('customer.name')}
|
|
200
|
+
data-testid="trigger-tag-select"
|
|
201
|
+
>
|
|
202
|
+
Select Tag
|
|
203
|
+
</button>
|
|
204
|
+
<button
|
|
205
|
+
onClick={() => props.onContextChange && props.onContextChange('default')}
|
|
206
|
+
data-testid="trigger-context-change"
|
|
207
|
+
>
|
|
208
|
+
Change Context
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Mock browser APIs
|
|
216
|
+
global.IntersectionObserver = class IntersectionObserver {
|
|
217
|
+
constructor() { }
|
|
218
|
+
|
|
219
|
+
disconnect() { }
|
|
220
|
+
|
|
221
|
+
observe() { }
|
|
222
|
+
|
|
223
|
+
unobserve() { }
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
global.ResizeObserver = class ResizeObserver {
|
|
227
|
+
constructor() { }
|
|
228
|
+
|
|
229
|
+
disconnect() { }
|
|
230
|
+
|
|
231
|
+
observe() { }
|
|
232
|
+
|
|
233
|
+
unobserve() { }
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Mock useLayoutEffect to behave like useEffect in tests
|
|
237
|
+
React.useLayoutEffect = React.useEffect;
|
|
238
|
+
|
|
239
|
+
// Mock browser APIs
|
|
240
|
+
global.IntersectionObserver = class IntersectionObserver {
|
|
241
|
+
constructor() { }
|
|
242
|
+
disconnect() { }
|
|
243
|
+
observe() { }
|
|
244
|
+
unobserve() { }
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
global.ResizeObserver = class ResizeObserver {
|
|
248
|
+
constructor() { }
|
|
249
|
+
disconnect() { }
|
|
250
|
+
observe() { }
|
|
251
|
+
unobserve() { }
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Mock useLayoutEffect to behave like useEffect in tests
|
|
255
|
+
React.useLayoutEffect = React.useEffect;
|
|
256
|
+
|
|
257
|
+
// Mock browser APIs
|
|
258
|
+
global.IntersectionObserver = class IntersectionObserver {
|
|
259
|
+
constructor() { }
|
|
260
|
+
disconnect() { }
|
|
261
|
+
observe() { }
|
|
262
|
+
unobserve() { }
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
global.ResizeObserver = class ResizeObserver {
|
|
266
|
+
constructor() { }
|
|
267
|
+
disconnect() { }
|
|
268
|
+
observe() { }
|
|
269
|
+
unobserve() { }
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Setup window.matchMedia mock
|
|
273
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
274
|
+
writable: true,
|
|
275
|
+
value: jest.fn().mockImplementation((query) => ({
|
|
276
|
+
matches: false,
|
|
277
|
+
media: query,
|
|
278
|
+
onchange: null,
|
|
279
|
+
addListener: jest.fn(),
|
|
280
|
+
removeListener: jest.fn(),
|
|
281
|
+
addEventListener: jest.fn(),
|
|
282
|
+
removeEventListener: jest.fn(),
|
|
283
|
+
dispatchEvent: jest.fn(),
|
|
284
|
+
})),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Mock enquire.js for Ant Design responsive behavior
|
|
288
|
+
jest.mock('enquire.js', () => ({
|
|
289
|
+
register: jest.fn(),
|
|
290
|
+
unregister: jest.fn(),
|
|
291
|
+
}));
|
|
292
|
+
|
|
293
|
+
// Mock Ant Design's responsive observer
|
|
294
|
+
jest.mock('antd/lib/_util/responsiveObserve', () => ({
|
|
295
|
+
subscribe: jest.fn(() => jest.fn()), // Return unsubscribe function
|
|
296
|
+
unsubscribe: jest.fn(),
|
|
297
|
+
responsiveMap: {
|
|
298
|
+
xs: '(max-width: 575px)',
|
|
299
|
+
sm: '(min-width: 576px)',
|
|
300
|
+
md: '(min-width: 768px)',
|
|
301
|
+
lg: '(min-width: 992px)',
|
|
302
|
+
xl: '(min-width: 1200px)',
|
|
303
|
+
xxl: '(min-width: 1600px)',
|
|
304
|
+
},
|
|
305
|
+
}));
|
|
306
|
+
|
|
307
|
+
// Mock enquire.js for Ant Design responsive behavior
|
|
308
|
+
jest.mock('enquire.js', () => ({
|
|
309
|
+
register: jest.fn(),
|
|
310
|
+
unregister: jest.fn(),
|
|
311
|
+
}));
|
|
312
|
+
|
|
313
|
+
// Mock Ant Design's responsive observer
|
|
314
|
+
jest.mock('antd/lib/_util/responsiveObserve', () => ({
|
|
315
|
+
subscribe: jest.fn(() => jest.fn()), // Return unsubscribe function
|
|
316
|
+
unsubscribe: jest.fn(),
|
|
317
|
+
responsiveMap: {
|
|
318
|
+
xs: '(max-width: 575px)',
|
|
319
|
+
sm: '(min-width: 576px)',
|
|
320
|
+
md: '(min-width: 768px)',
|
|
321
|
+
lg: '(min-width: 992px)',
|
|
322
|
+
xl: '(min-width: 1200px)',
|
|
323
|
+
xxl: '(min-width: 1600px)',
|
|
324
|
+
},
|
|
325
|
+
}));
|
|
326
|
+
|
|
327
|
+
// Setup window.matchMedia mock (duplicate - keeping for compatibility)
|
|
328
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
329
|
+
writable: true,
|
|
330
|
+
value: jest.fn().mockImplementation((query) => ({
|
|
331
|
+
matches: false,
|
|
332
|
+
media: query,
|
|
333
|
+
onchange: null,
|
|
334
|
+
addListener: jest.fn(),
|
|
335
|
+
removeListener: jest.fn(),
|
|
336
|
+
addEventListener: jest.fn(),
|
|
337
|
+
removeEventListener: jest.fn(),
|
|
338
|
+
dispatchEvent: jest.fn(),
|
|
339
|
+
})),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const defaultProps = {
|
|
343
|
+
intl: {
|
|
344
|
+
formatMessage: jest.fn((msg) => msg.defaultMessage || msg.id || ''),
|
|
345
|
+
locale: 'en',
|
|
346
|
+
},
|
|
347
|
+
location: { query: {} },
|
|
348
|
+
params: {},
|
|
349
|
+
getDefaultTags: 'default',
|
|
350
|
+
supportedTags: [],
|
|
351
|
+
metaEntities: {},
|
|
352
|
+
injectedTags: {},
|
|
353
|
+
globalActions: {
|
|
354
|
+
getLiquidTags: jest.fn(),
|
|
355
|
+
fetchSchemaForEntity: jest.fn(),
|
|
356
|
+
},
|
|
357
|
+
loadingTags: false,
|
|
358
|
+
eventContextTags: [],
|
|
359
|
+
forwardedTags: {},
|
|
360
|
+
selectedOfferDetails: [],
|
|
361
|
+
currentOrgDetails: {
|
|
362
|
+
basic_details: {
|
|
363
|
+
base_language: 'en',
|
|
364
|
+
languages: {
|
|
365
|
+
en: { lang_id: '1', language: 'English' },
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
isReadOnly: false,
|
|
370
|
+
fetchingLiquidTags: false,
|
|
371
|
+
createTemplateInProgress: false,
|
|
372
|
+
fetchingCmsData: false,
|
|
373
|
+
Email: {},
|
|
374
|
+
emailActions: {
|
|
375
|
+
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
376
|
+
createTemplate: jest.fn((obj, callback) => callback({ templateId: { _id: '123', versions: {} } })),
|
|
377
|
+
getTemplateDetails: jest.fn(),
|
|
378
|
+
clearAllValues: jest.fn(),
|
|
379
|
+
},
|
|
380
|
+
isFullMode: true,
|
|
381
|
+
templateName: 'Test Template',
|
|
382
|
+
showTemplateName: jest.fn(),
|
|
383
|
+
onFormDataChange: jest.fn(),
|
|
384
|
+
isGetFormData: false,
|
|
385
|
+
getFormdata: jest.fn(),
|
|
386
|
+
templateData: null,
|
|
387
|
+
EmailLayout: null,
|
|
388
|
+
getLiquidTags: jest.fn((content, callback) => callback({ askAiraResponse: { data: [] }, isError: false })),
|
|
389
|
+
showLiquidErrorInFooter: jest.fn(),
|
|
390
|
+
onValidationFail: jest.fn(),
|
|
391
|
+
setIsLoadingContent: jest.fn(),
|
|
392
|
+
forwardedRef: null,
|
|
393
|
+
moduleType: null,
|
|
394
|
+
onHtmlEditorValidationStateChange: jest.fn(),
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const renderWithIntl = (props = {}) => {
|
|
398
|
+
const mergedProps = { ...defaultProps, ...props };
|
|
399
|
+
return render(
|
|
400
|
+
<IntlProvider locale="en" messages={{}}>
|
|
401
|
+
<EmailHTMLEditor {...mergedProps} />
|
|
402
|
+
</IntlProvider>
|
|
403
|
+
);
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
describe('EmailHTMLEditor', () => {
|
|
407
|
+
beforeEach(() => {
|
|
408
|
+
jest.clearAllMocks();
|
|
409
|
+
validateLiquidTemplateContent.mockResolvedValue(true);
|
|
410
|
+
validateTags.mockReturnValue({ valid: true });
|
|
411
|
+
isEmailUnsubscribeTagMandatory.mockReturnValue(false);
|
|
412
|
+
// Reset mock functions
|
|
413
|
+
mockGetAllIssues.mockReturnValue([]);
|
|
414
|
+
mockGetValidationState.mockReturnValue({
|
|
415
|
+
isValidating: false,
|
|
416
|
+
hasErrors: false,
|
|
417
|
+
issueCounts: {
|
|
418
|
+
html: 0, label: 0, liquid: 0, total: 0,
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
// Reset hasLiquidSupportFeature mock to return true by default
|
|
422
|
+
mockHasLiquidSupportFeature.mockReturnValue(true);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe('Component Rendering', () => {
|
|
426
|
+
it('renders without crashing', () => {
|
|
427
|
+
renderWithIntl();
|
|
428
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('renders subject input field', () => {
|
|
432
|
+
renderWithIntl();
|
|
433
|
+
expect(screen.getByTestId('subject-input')).toBeInTheDocument();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('renders with loading state', () => {
|
|
437
|
+
renderWithIntl({ loadingTags: true });
|
|
438
|
+
// Component should render even when loading
|
|
439
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe('Content Initialization', () => {
|
|
444
|
+
it('initializes with empty content in create mode', () => {
|
|
445
|
+
renderWithIntl({ isGetFormData: false });
|
|
446
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('initializes with EmailLayout content in create mode', () => {
|
|
450
|
+
const EmailLayout = '<p>Uploaded content</p>';
|
|
451
|
+
renderWithIntl({ EmailLayout, isGetFormData: false });
|
|
452
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('initializes with EmailLayout object content', () => {
|
|
456
|
+
const EmailLayout = { html: '<p>Object content</p>' };
|
|
457
|
+
renderWithIntl({ EmailLayout, isGetFormData: false });
|
|
458
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('handles template data prop in edit mode', () => {
|
|
462
|
+
const templateData = {
|
|
463
|
+
base: {
|
|
464
|
+
'template-content': '<p>Template content</p>',
|
|
465
|
+
"subject": 'Test Subject',
|
|
466
|
+
},
|
|
467
|
+
name: 'Test Template',
|
|
468
|
+
};
|
|
469
|
+
renderWithIntl({
|
|
470
|
+
templateData,
|
|
471
|
+
params: { id: '123' },
|
|
472
|
+
Email: { templateDetails: { _id: '123' } },
|
|
473
|
+
});
|
|
474
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('handles template data with versions structure', () => {
|
|
478
|
+
const templateData = {
|
|
479
|
+
versions: {
|
|
480
|
+
base: {
|
|
481
|
+
activeTab: 'en',
|
|
482
|
+
en: {
|
|
483
|
+
'template-content': '<p>Version content</p>',
|
|
484
|
+
},
|
|
485
|
+
subject: 'Version Subject',
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
name: 'Version Template',
|
|
489
|
+
};
|
|
490
|
+
renderWithIntl({
|
|
491
|
+
templateData,
|
|
492
|
+
params: { id: '123' },
|
|
493
|
+
Email: { templateDetails: { _id: '123' } },
|
|
494
|
+
});
|
|
495
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('handles Redux template data in edit mode', () => {
|
|
499
|
+
const Email = {
|
|
500
|
+
templateDetails: {
|
|
501
|
+
_id: '123',
|
|
502
|
+
name: 'Redux Template',
|
|
503
|
+
versions: {
|
|
504
|
+
base: {
|
|
505
|
+
activeTab: 'en',
|
|
506
|
+
en: {
|
|
507
|
+
'template-content': '<p>Redux content</p>',
|
|
508
|
+
},
|
|
509
|
+
subject: 'Redux Subject',
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
getTemplateDetailsInProgress: false,
|
|
514
|
+
fetchingCmsData: false,
|
|
515
|
+
};
|
|
516
|
+
renderWithIntl({
|
|
517
|
+
Email,
|
|
518
|
+
params: { id: '123' },
|
|
519
|
+
});
|
|
520
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('handles BEETemplate from Redux', () => {
|
|
524
|
+
const Email = {
|
|
525
|
+
BEETemplate: {
|
|
526
|
+
_id: '123',
|
|
527
|
+
name: 'BEE Template',
|
|
528
|
+
base: {
|
|
529
|
+
'template-content': '<p>BEE content</p>',
|
|
530
|
+
"subject": 'BEE Subject',
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
getTemplateDetailsInProgress: false,
|
|
534
|
+
fetchingCmsData: false,
|
|
535
|
+
};
|
|
536
|
+
renderWithIntl({
|
|
537
|
+
Email,
|
|
538
|
+
params: { id: '123' },
|
|
539
|
+
});
|
|
540
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('fetches template details when template ID changes', () => {
|
|
544
|
+
const emailActions = {
|
|
545
|
+
...defaultProps.emailActions,
|
|
546
|
+
getTemplateDetails: jest.fn(),
|
|
547
|
+
};
|
|
548
|
+
const { rerender } = renderWithIntl({
|
|
549
|
+
Email: { templateDetails: null, getTemplateDetailsInProgress: false, fetchingCmsData: false },
|
|
550
|
+
params: { id: '123' },
|
|
551
|
+
emailActions,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
rerender(
|
|
555
|
+
<IntlProvider locale="en" messages={{}}>
|
|
556
|
+
<EmailHTMLEditor
|
|
557
|
+
{...defaultProps}
|
|
558
|
+
Email={{ templateDetails: null, getTemplateDetailsInProgress: false, fetchingCmsData: false }}
|
|
559
|
+
params={{ id: '456' }}
|
|
560
|
+
emailActions={emailActions}
|
|
561
|
+
/>
|
|
562
|
+
</IntlProvider>
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
// Should trigger fetch for new template ID
|
|
566
|
+
expect(emailActions.getTemplateDetails).toHaveBeenCalled();
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
describe('Subject Handling', () => {
|
|
571
|
+
it('updates subject on input change', () => {
|
|
572
|
+
renderWithIntl();
|
|
573
|
+
const input = screen.getByTestId('subject-input');
|
|
574
|
+
fireEvent.change(input, { target: { value: 'New Subject' } });
|
|
575
|
+
expect(input.value).toBe('New Subject');
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('clears subject error when subject is entered', () => {
|
|
579
|
+
renderWithIntl();
|
|
580
|
+
const input = screen.getByTestId('subject-input');
|
|
581
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
582
|
+
// Error should be cleared
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('inserts tag into subject field', () => {
|
|
586
|
+
renderWithIntl({ subject: 'Hello ' });
|
|
587
|
+
const tagButton = screen.getByTestId('trigger-tag-select');
|
|
588
|
+
|
|
589
|
+
// Mock input element with selection
|
|
590
|
+
const input = document.getElementById('template-subject');
|
|
591
|
+
if (input) {
|
|
592
|
+
Object.defineProperty(input, 'selectionStart', { value: 6, writable: true });
|
|
593
|
+
Object.defineProperty(input, 'selectionEnd', { value: 6, writable: true });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
fireEvent.click(tagButton);
|
|
597
|
+
// Tag should be inserted
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('handles tag insertion when input has no selection', () => {
|
|
601
|
+
renderWithIntl({ subject: 'Hello' });
|
|
602
|
+
const tagButton = screen.getByTestId('trigger-tag-select');
|
|
603
|
+
|
|
604
|
+
const input = document.getElementById('template-subject');
|
|
605
|
+
if (input) {
|
|
606
|
+
Object.defineProperty(input, 'selectionStart', { value: undefined, writable: true });
|
|
607
|
+
Object.defineProperty(input, 'selectionEnd', { value: undefined, writable: true });
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
fireEvent.click(tagButton);
|
|
611
|
+
// Tag should be appended
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('handles tag insertion when input element is not found', () => {
|
|
615
|
+
renderWithIntl({ subject: 'Hello' });
|
|
616
|
+
const tagButton = screen.getByTestId('trigger-tag-select');
|
|
617
|
+
|
|
618
|
+
// Remove input element
|
|
619
|
+
const input = document.getElementById('template-subject');
|
|
620
|
+
if (input) {
|
|
621
|
+
input.remove();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
fireEvent.click(tagButton);
|
|
625
|
+
// Should handle gracefully
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
describe('Content Change Handling', () => {
|
|
630
|
+
it('updates content when HTMLEditor changes', () => {
|
|
631
|
+
renderWithIntl();
|
|
632
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
633
|
+
fireEvent.click(changeButton);
|
|
634
|
+
// Content should be updated
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('validates tags on content change', () => {
|
|
638
|
+
validateTags.mockClear();
|
|
639
|
+
// Need to provide tags via metaEntities or supportedTags
|
|
640
|
+
renderWithIntl({
|
|
641
|
+
metaEntities: {
|
|
642
|
+
tags: {
|
|
643
|
+
standard: [{ name: 'customer.name' }],
|
|
644
|
+
},
|
|
645
|
+
},
|
|
646
|
+
});
|
|
647
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
648
|
+
fireEvent.click(changeButton);
|
|
649
|
+
// validateTags is called in handleContentChange when tags.length > 0
|
|
650
|
+
expect(validateTags).toHaveBeenCalledWith(
|
|
651
|
+
expect.objectContaining({
|
|
652
|
+
content: '<p>New content</p>',
|
|
653
|
+
tagsParam: [{ name: 'customer.name' }],
|
|
654
|
+
})
|
|
655
|
+
);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('sets tag validation error when validation fails', () => {
|
|
659
|
+
validateTags.mockReturnValue({ valid: false, missingTags: ['tag1'] });
|
|
660
|
+
validateTags.mockClear();
|
|
661
|
+
renderWithIntl({
|
|
662
|
+
metaEntities: {
|
|
663
|
+
tags: {
|
|
664
|
+
standard: [{ name: 'customer.name' }],
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
669
|
+
fireEvent.click(changeButton);
|
|
670
|
+
// validateTags should be called
|
|
671
|
+
expect(validateTags).toHaveBeenCalled();
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it('clears tag validation error when validation passes', () => {
|
|
675
|
+
validateTags.mockReturnValue({ valid: true });
|
|
676
|
+
validateTags.mockClear();
|
|
677
|
+
renderWithIntl({
|
|
678
|
+
metaEntities: {
|
|
679
|
+
tags: {
|
|
680
|
+
standard: [{ name: 'customer.name' }],
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
685
|
+
fireEvent.click(changeButton);
|
|
686
|
+
// validateTags should be called
|
|
687
|
+
expect(validateTags).toHaveBeenCalled();
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
describe('Validation State Handling', () => {
|
|
692
|
+
it('handles validation state change from HTMLEditor', () => {
|
|
693
|
+
const onHtmlEditorValidationStateChange = jest.fn();
|
|
694
|
+
renderWithIntl({ onHtmlEditorValidationStateChange });
|
|
695
|
+
const validationButton = screen.getByTestId('trigger-validation-change');
|
|
696
|
+
fireEvent.click(validationButton);
|
|
697
|
+
expect(onHtmlEditorValidationStateChange).toHaveBeenCalled();
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('handles error acknowledgment', () => {
|
|
701
|
+
const onHtmlEditorValidationStateChange = jest.fn();
|
|
702
|
+
renderWithIntl({ onHtmlEditorValidationStateChange });
|
|
703
|
+
const ackButton = screen.getByTestId('trigger-error-acknowledged');
|
|
704
|
+
fireEvent.click(ackButton);
|
|
705
|
+
// Error should be acknowledged
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('resets error acknowledgment when new errors appear', () => {
|
|
709
|
+
const onHtmlEditorValidationStateChange = jest.fn();
|
|
710
|
+
renderWithIntl({ onHtmlEditorValidationStateChange });
|
|
711
|
+
const validationButton = screen.getByTestId('trigger-validation-change');
|
|
712
|
+
|
|
713
|
+
// First, set validation with errors
|
|
714
|
+
fireEvent.click(validationButton);
|
|
715
|
+
|
|
716
|
+
// Then trigger validation change with errors
|
|
717
|
+
const htmlEditor = screen.getByTestId('html-editor');
|
|
718
|
+
const triggerValidationWithErrors = htmlEditor.querySelector('[data-testid="trigger-validation-change"]');
|
|
719
|
+
if (triggerValidationWithErrors) {
|
|
720
|
+
// Mock validation change with errors
|
|
721
|
+
const mockValidationChange = jest.fn((state) => {
|
|
722
|
+
if (state.hasErrors) {
|
|
723
|
+
onHtmlEditorValidationStateChange({
|
|
724
|
+
...state,
|
|
725
|
+
errorsAcknowledged: false,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
// This would be called by HTMLEditor internally
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('prevents duplicate validation state updates', () => {
|
|
734
|
+
const onHtmlEditorValidationStateChange = jest.fn();
|
|
735
|
+
renderWithIntl({ onHtmlEditorValidationStateChange });
|
|
736
|
+
const validationButton = screen.getByTestId('trigger-validation-change');
|
|
737
|
+
|
|
738
|
+
// Click multiple times with same state
|
|
739
|
+
fireEvent.click(validationButton);
|
|
740
|
+
fireEvent.click(validationButton);
|
|
741
|
+
fireEvent.click(validationButton);
|
|
742
|
+
|
|
743
|
+
// Should only notify once for same state
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
describe('Save Functionality', () => {
|
|
748
|
+
it('blocks save when subject is empty', async () => {
|
|
749
|
+
const onValidationFail = jest.fn();
|
|
750
|
+
// Component initializes with empty subject state, so when isGetFormData becomes true, save should be blocked
|
|
751
|
+
renderWithIntl({ onValidationFail, isGetFormData: true });
|
|
752
|
+
await waitFor(() => {
|
|
753
|
+
expect(onValidationFail).toHaveBeenCalled();
|
|
754
|
+
}, { timeout: 3000 });
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it('blocks save when subject is only whitespace', async () => {
|
|
758
|
+
const onValidationFail = jest.fn();
|
|
759
|
+
// Component uses internal state, so we need to set subject via input change first
|
|
760
|
+
const { rerender } = renderWithIntl({ onValidationFail, isGetFormData: false });
|
|
761
|
+
const input = screen.getByTestId('subject-input');
|
|
762
|
+
fireEvent.change(input, { target: { value: ' ' } });
|
|
763
|
+
// Now trigger save
|
|
764
|
+
rerender(
|
|
765
|
+
<IntlProvider locale="en" messages={{}}>
|
|
766
|
+
<EmailHTMLEditor {...defaultProps} onValidationFail={onValidationFail} isGetFormData={true} />
|
|
767
|
+
</IntlProvider>
|
|
768
|
+
);
|
|
769
|
+
await waitFor(() => {
|
|
770
|
+
expect(onValidationFail).toHaveBeenCalled();
|
|
771
|
+
}, { timeout: 3000 });
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it('blocks save when there are HTML validation errors', async () => {
|
|
775
|
+
const onValidationFail = jest.fn();
|
|
776
|
+
// Mock getAllIssues to return BLOCKING errors (sanitizer errors or client-side Liquid errors)
|
|
777
|
+
// HTML/label errors are now warnings and don't block save
|
|
778
|
+
mockGetAllIssues.mockReturnValue([{
|
|
779
|
+
type: 'error',
|
|
780
|
+
message: 'Sanitization failed',
|
|
781
|
+
line: 1,
|
|
782
|
+
column: 1,
|
|
783
|
+
rule: 'sanitizer.sanitizationFailed',
|
|
784
|
+
severity: 'error',
|
|
785
|
+
source: 'sanitizer',
|
|
786
|
+
}]);
|
|
787
|
+
// Also update getValidationState to return hasErrors: true (this is what handleSave checks)
|
|
788
|
+
mockGetValidationState.mockReturnValue({
|
|
789
|
+
isValidating: false,
|
|
790
|
+
hasErrors: true,
|
|
791
|
+
issueCounts: {
|
|
792
|
+
html: 0, label: 0, liquid: 0, total: 1,
|
|
793
|
+
},
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// Set subject and content via component interactions
|
|
797
|
+
const { rerender } = renderWithIntl({
|
|
798
|
+
onValidationFail,
|
|
799
|
+
isGetFormData: false,
|
|
800
|
+
});
|
|
801
|
+
const input = screen.getByTestId('subject-input');
|
|
802
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
803
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
804
|
+
fireEvent.click(changeButton);
|
|
805
|
+
// Now trigger save
|
|
806
|
+
rerender(
|
|
807
|
+
<IntlProvider locale="en" messages={{}}>
|
|
808
|
+
<EmailHTMLEditor {...defaultProps} onValidationFail={onValidationFail} isGetFormData={true} />
|
|
809
|
+
</IntlProvider>
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
await waitFor(() => {
|
|
813
|
+
expect(onValidationFail).toHaveBeenCalled();
|
|
814
|
+
}, { timeout: 3000 });
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it('blocks save when there are label validation errors', async () => {
|
|
818
|
+
const onValidationFail = jest.fn();
|
|
819
|
+
// Mock getAllIssues to return BLOCKING errors (sanitizer errors or client-side Liquid errors)
|
|
820
|
+
// Label errors (tag-pair) are now warnings and don't block save
|
|
821
|
+
mockGetAllIssues.mockReturnValue([{
|
|
822
|
+
type: 'error',
|
|
823
|
+
message: 'Invalid input detected',
|
|
824
|
+
line: 1,
|
|
825
|
+
column: 1,
|
|
826
|
+
rule: 'sanitizer.invalidInput',
|
|
827
|
+
severity: 'error',
|
|
828
|
+
source: 'sanitizer',
|
|
829
|
+
}]);
|
|
830
|
+
// Also update getValidationState to return hasErrors: true (this is what handleSave checks)
|
|
831
|
+
mockGetValidationState.mockReturnValue({
|
|
832
|
+
isValidating: false,
|
|
833
|
+
hasErrors: true,
|
|
834
|
+
issueCounts: {
|
|
835
|
+
html: 0, label: 0, liquid: 0, total: 1,
|
|
836
|
+
},
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
// Set subject and content via component interactions
|
|
840
|
+
const { rerender } = renderWithIntl({
|
|
841
|
+
onValidationFail,
|
|
842
|
+
isGetFormData: false,
|
|
843
|
+
});
|
|
844
|
+
const input = screen.getByTestId('subject-input');
|
|
845
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
846
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
847
|
+
fireEvent.click(changeButton);
|
|
848
|
+
// Now trigger save
|
|
849
|
+
rerender(
|
|
850
|
+
<IntlProvider locale="en" messages={{}}>
|
|
851
|
+
<EmailHTMLEditor {...defaultProps} onValidationFail={onValidationFail} isGetFormData={true} />
|
|
852
|
+
</IntlProvider>
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
await waitFor(() => {
|
|
856
|
+
expect(onValidationFail).toHaveBeenCalled();
|
|
857
|
+
}, { timeout: 3000 });
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
it('blocks save when there are liquid validation errors', async () => {
|
|
861
|
+
const onValidationFail = jest.fn();
|
|
862
|
+
// Mock getAllIssues to return client-side Liquid errors (these ARE blocking)
|
|
863
|
+
mockGetAllIssues.mockReturnValue([{
|
|
864
|
+
type: 'error',
|
|
865
|
+
message: 'Unclosed Liquid tag',
|
|
866
|
+
line: 1,
|
|
867
|
+
column: 1,
|
|
868
|
+
rule: 'liquid-syntax',
|
|
869
|
+
severity: 'error',
|
|
870
|
+
source: 'liquid-validator',
|
|
871
|
+
}]);
|
|
872
|
+
// Also update getValidationState to return hasErrors: true (this is what handleSave checks)
|
|
873
|
+
mockGetValidationState.mockReturnValue({
|
|
874
|
+
isValidating: false,
|
|
875
|
+
hasErrors: true,
|
|
876
|
+
issueCounts: {
|
|
877
|
+
html: 0, label: 0, liquid: 1, total: 1,
|
|
878
|
+
},
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
// Set subject and content via component interactions
|
|
882
|
+
const { rerender } = renderWithIntl({
|
|
883
|
+
onValidationFail,
|
|
884
|
+
isGetFormData: false,
|
|
885
|
+
});
|
|
886
|
+
const input = screen.getByTestId('subject-input');
|
|
887
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
888
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
889
|
+
fireEvent.click(changeButton);
|
|
890
|
+
// Now trigger save
|
|
891
|
+
rerender(
|
|
892
|
+
<IntlProvider locale="en" messages={{}}>
|
|
893
|
+
<EmailHTMLEditor {...defaultProps} onValidationFail={onValidationFail} isGetFormData={true} />
|
|
894
|
+
</IntlProvider>
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
await waitFor(() => {
|
|
898
|
+
expect(onValidationFail).toHaveBeenCalled();
|
|
899
|
+
}, { timeout: 3000 });
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
it('blocks save when unsubscribe tag is mandatory and missing', async () => {
|
|
903
|
+
isEmailUnsubscribeTagMandatory.mockReturnValue(true);
|
|
904
|
+
const onValidationFail = jest.fn();
|
|
905
|
+
const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
|
|
906
|
+
|
|
907
|
+
// Set subject via input and content via HTMLEditor mock
|
|
908
|
+
const { rerender } = renderWithIntl({
|
|
909
|
+
onValidationFail,
|
|
910
|
+
isGetFormData: false,
|
|
911
|
+
moduleType: 'OUTBOUND',
|
|
912
|
+
});
|
|
913
|
+
const input = screen.getByTestId('subject-input');
|
|
914
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
915
|
+
// Trigger content change to set htmlContent
|
|
916
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
917
|
+
fireEvent.click(changeButton);
|
|
918
|
+
// Now trigger save
|
|
919
|
+
rerender(
|
|
920
|
+
<IntlProvider locale="en" messages={{}}>
|
|
921
|
+
<EmailHTMLEditor {...defaultProps} onValidationFail={onValidationFail} isGetFormData={true} moduleType="OUTBOUND" />
|
|
922
|
+
</IntlProvider>
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
await waitFor(() => {
|
|
926
|
+
expect(CapNotification.error).toHaveBeenCalled();
|
|
927
|
+
expect(onValidationFail).toHaveBeenCalled();
|
|
928
|
+
}, { timeout: 3000 });
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
it('allows save when unsubscribe tag is present', () => {
|
|
932
|
+
isEmailUnsubscribeTagMandatory.mockReturnValue(true);
|
|
933
|
+
renderWithIntl({
|
|
934
|
+
isGetFormData: true,
|
|
935
|
+
subject: 'Valid Subject',
|
|
936
|
+
htmlContent: '<p>Content {{unsubscribe}}</p>',
|
|
937
|
+
moduleType: 'OUTBOUND',
|
|
938
|
+
});
|
|
939
|
+
// Should proceed with save
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
it('blocks save for non-liquid orgs when tag validation fails', async () => {
|
|
943
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
944
|
+
validateTags.mockReturnValue({
|
|
945
|
+
valid: false,
|
|
946
|
+
unsupportedTags: ['tag1'],
|
|
947
|
+
missingTags: ['tag2'],
|
|
948
|
+
});
|
|
949
|
+
const onValidationFail = jest.fn();
|
|
950
|
+
const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
|
|
951
|
+
|
|
952
|
+
// Set subject and content via component interactions
|
|
953
|
+
const { rerender } = renderWithIntl({
|
|
954
|
+
onValidationFail,
|
|
955
|
+
isGetFormData: false,
|
|
956
|
+
metaEntities: {
|
|
957
|
+
tags: {
|
|
958
|
+
standard: [{ name: 'customer.name' }],
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
getLiquidTags: null, // No liquid tags for non-liquid org
|
|
962
|
+
});
|
|
963
|
+
const input = screen.getByTestId('subject-input');
|
|
964
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
965
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
966
|
+
fireEvent.click(changeButton);
|
|
967
|
+
// Now trigger save
|
|
968
|
+
rerender(
|
|
969
|
+
<IntlProvider locale="en" messages={{}}>
|
|
970
|
+
<EmailHTMLEditor {...defaultProps} onValidationFail={onValidationFail} isGetFormData={true} metaEntities={{
|
|
971
|
+
tags: {
|
|
972
|
+
standard: [{ name: 'customer.name' }],
|
|
973
|
+
},
|
|
974
|
+
}} getLiquidTags={null} />
|
|
975
|
+
</IntlProvider>
|
|
976
|
+
);
|
|
977
|
+
|
|
978
|
+
await waitFor(() => {
|
|
979
|
+
expect(CapNotification.error).toHaveBeenCalled();
|
|
980
|
+
expect(onValidationFail).toHaveBeenCalled();
|
|
981
|
+
}, { timeout: 3000 });
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
it('allows save for liquid orgs even when tag validation fails', async () => {
|
|
985
|
+
validateTags.mockReturnValue({
|
|
986
|
+
valid: false,
|
|
987
|
+
unsupportedTags: ['tag1'],
|
|
988
|
+
});
|
|
989
|
+
const getLiquidTags = jest.fn((content, callback) => {
|
|
990
|
+
callback({ askAiraResponse: { data: [] }, isError: false });
|
|
991
|
+
});
|
|
992
|
+
validateLiquidTemplateContent.mockResolvedValue(true);
|
|
993
|
+
// Ensure no HTML/Label/Liquid errors from HtmlEditor
|
|
994
|
+
mockGetAllIssues.mockReturnValue([]);
|
|
995
|
+
|
|
996
|
+
// Set subject and content via component interactions
|
|
997
|
+
const { rerender } = renderWithIntl({
|
|
998
|
+
isGetFormData: false,
|
|
999
|
+
isFullMode: true,
|
|
1000
|
+
metaEntities: {
|
|
1001
|
+
tags: {
|
|
1002
|
+
standard: [{ name: 'customer.name' }],
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
1005
|
+
isLiquidEnabled: true,
|
|
1006
|
+
getLiquidTags,
|
|
1007
|
+
});
|
|
1008
|
+
const input = screen.getByTestId('subject-input');
|
|
1009
|
+
await act(async () => {
|
|
1010
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
1011
|
+
});
|
|
1012
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
1013
|
+
await act(async () => {
|
|
1014
|
+
fireEvent.click(changeButton);
|
|
1015
|
+
});
|
|
1016
|
+
// Wait a bit for state updates
|
|
1017
|
+
await act(async () => {
|
|
1018
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1019
|
+
});
|
|
1020
|
+
// Now trigger save
|
|
1021
|
+
await act(async () => {
|
|
1022
|
+
rerender(
|
|
1023
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1024
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData={true} isFullMode={true} metaEntities={{
|
|
1025
|
+
tags: {
|
|
1026
|
+
standard: [{ name: 'customer.name' }],
|
|
1027
|
+
},
|
|
1028
|
+
}} getLiquidTags={getLiquidTags} />
|
|
1029
|
+
</IntlProvider>
|
|
1030
|
+
);
|
|
1031
|
+
});
|
|
1032
|
+
// Should proceed to liquid validation (tag validation fails but liquid orgs continue)
|
|
1033
|
+
await waitFor(() => {
|
|
1034
|
+
expect(validateLiquidTemplateContent).toHaveBeenCalled();
|
|
1035
|
+
}, { timeout: 5000 });
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
it('validates liquid content before saving when liquid is enabled', async () => {
|
|
1039
|
+
validateLiquidTemplateContent.mockResolvedValue(true);
|
|
1040
|
+
const getLiquidTags = jest.fn((content, callback) => {
|
|
1041
|
+
callback({ askAiraResponse: { data: [] }, isError: false });
|
|
1042
|
+
});
|
|
1043
|
+
// Ensure no HTML/Label/Liquid errors from HtmlEditor
|
|
1044
|
+
mockGetAllIssues.mockReturnValue([]);
|
|
1045
|
+
|
|
1046
|
+
// Set subject and content via component interactions
|
|
1047
|
+
const { rerender } = renderWithIntl({
|
|
1048
|
+
isGetFormData: false,
|
|
1049
|
+
isFullMode: true,
|
|
1050
|
+
isLiquidEnabled: true,
|
|
1051
|
+
getLiquidTags,
|
|
1052
|
+
});
|
|
1053
|
+
const input = screen.getByTestId('subject-input');
|
|
1054
|
+
await act(async () => {
|
|
1055
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
1056
|
+
});
|
|
1057
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
1058
|
+
await act(async () => {
|
|
1059
|
+
fireEvent.click(changeButton);
|
|
1060
|
+
});
|
|
1061
|
+
// Wait a bit for state updates
|
|
1062
|
+
await act(async () => {
|
|
1063
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1064
|
+
});
|
|
1065
|
+
// Now trigger save
|
|
1066
|
+
await act(async () => {
|
|
1067
|
+
rerender(
|
|
1068
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1069
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData={true} isFullMode={true} getLiquidTags={getLiquidTags} />
|
|
1070
|
+
</IntlProvider>
|
|
1071
|
+
);
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
await waitFor(() => {
|
|
1075
|
+
expect(validateLiquidTemplateContent).toHaveBeenCalled();
|
|
1076
|
+
}, { timeout: 5000 });
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
it('handles liquid validation errors', async () => {
|
|
1080
|
+
validateLiquidTemplateContent.mockImplementation((content, options) => {
|
|
1081
|
+
options.onError({
|
|
1082
|
+
standardErrors: ['Standard error'],
|
|
1083
|
+
liquidErrors: ['Liquid error'],
|
|
1084
|
+
});
|
|
1085
|
+
return Promise.resolve(false);
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
const showLiquidErrorInFooter = jest.fn();
|
|
1089
|
+
const onValidationFail = jest.fn();
|
|
1090
|
+
const getLiquidTags = jest.fn((content, callback) => {
|
|
1091
|
+
callback({ askAiraResponse: { errors: [{ message: 'Error' }] }, isError: true });
|
|
1092
|
+
});
|
|
1093
|
+
// Ensure no HTML/Label/Liquid errors from HtmlEditor
|
|
1094
|
+
mockGetAllIssues.mockReturnValue([]);
|
|
1095
|
+
|
|
1096
|
+
// Set subject and content via component interactions
|
|
1097
|
+
const { rerender } = renderWithIntl({
|
|
1098
|
+
isGetFormData: false,
|
|
1099
|
+
isFullMode: true,
|
|
1100
|
+
isLiquidEnabled: true,
|
|
1101
|
+
getLiquidTags,
|
|
1102
|
+
showLiquidErrorInFooter,
|
|
1103
|
+
onValidationFail,
|
|
1104
|
+
});
|
|
1105
|
+
const input = screen.getByTestId('subject-input');
|
|
1106
|
+
await act(async () => {
|
|
1107
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
1108
|
+
});
|
|
1109
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
1110
|
+
await act(async () => {
|
|
1111
|
+
fireEvent.click(changeButton);
|
|
1112
|
+
});
|
|
1113
|
+
// Wait a bit for state updates
|
|
1114
|
+
await act(async () => {
|
|
1115
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1116
|
+
});
|
|
1117
|
+
// Now trigger save
|
|
1118
|
+
await act(async () => {
|
|
1119
|
+
rerender(
|
|
1120
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1121
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData={true} isFullMode={true} getLiquidTags={getLiquidTags} showLiquidErrorInFooter={showLiquidErrorInFooter} onValidationFail={onValidationFail} />
|
|
1122
|
+
</IntlProvider>
|
|
1123
|
+
);
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
await waitFor(() => {
|
|
1127
|
+
expect(validateLiquidTemplateContent).toHaveBeenCalled();
|
|
1128
|
+
}, { timeout: 5000 });
|
|
1129
|
+
await waitFor(() => {
|
|
1130
|
+
expect(showLiquidErrorInFooter).toHaveBeenCalled();
|
|
1131
|
+
expect(onValidationFail).toHaveBeenCalled();
|
|
1132
|
+
}, { timeout: 5000 });
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
it('proceeds with save after successful liquid validation', async () => {
|
|
1136
|
+
validateLiquidTemplateContent.mockImplementation((content, options) => {
|
|
1137
|
+
options.onSuccess();
|
|
1138
|
+
return Promise.resolve(true);
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
const emailActions = {
|
|
1142
|
+
...defaultProps.emailActions,
|
|
1143
|
+
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
1144
|
+
createTemplate: jest.fn((obj, callback) => {
|
|
1145
|
+
callback({ templateId: { _id: '123', versions: {} } });
|
|
1146
|
+
}),
|
|
1147
|
+
};
|
|
1148
|
+
const getLiquidTags = jest.fn((content, callback) => {
|
|
1149
|
+
callback({ askAiraResponse: { data: [] }, isError: false });
|
|
1150
|
+
});
|
|
1151
|
+
// Ensure no HTML/Label/Liquid errors from HtmlEditor
|
|
1152
|
+
mockGetAllIssues.mockReturnValue([]);
|
|
1153
|
+
|
|
1154
|
+
// Set subject and content via component interactions
|
|
1155
|
+
const { rerender } = renderWithIntl({
|
|
1156
|
+
isGetFormData: false,
|
|
1157
|
+
isFullMode: true,
|
|
1158
|
+
isLiquidEnabled: true,
|
|
1159
|
+
getLiquidTags,
|
|
1160
|
+
emailActions,
|
|
1161
|
+
templateName: 'New Template',
|
|
1162
|
+
});
|
|
1163
|
+
const input = screen.getByTestId('subject-input');
|
|
1164
|
+
await act(async () => {
|
|
1165
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
1166
|
+
});
|
|
1167
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
1168
|
+
await act(async () => {
|
|
1169
|
+
fireEvent.click(changeButton);
|
|
1170
|
+
});
|
|
1171
|
+
// Wait a bit for state updates
|
|
1172
|
+
await act(async () => {
|
|
1173
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1174
|
+
});
|
|
1175
|
+
// Now trigger save
|
|
1176
|
+
await act(async () => {
|
|
1177
|
+
rerender(
|
|
1178
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1179
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData={true} isFullMode={true} getLiquidTags={getLiquidTags} emailActions={emailActions} templateName="New Template" />
|
|
1180
|
+
</IntlProvider>
|
|
1181
|
+
);
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
await waitFor(() => {
|
|
1185
|
+
expect(validateLiquidTemplateContent).toHaveBeenCalled();
|
|
1186
|
+
}, { timeout: 5000 });
|
|
1187
|
+
|
|
1188
|
+
await waitFor(() => {
|
|
1189
|
+
expect(emailActions.createTemplate).toHaveBeenCalled();
|
|
1190
|
+
}, { timeout: 5000 });
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
it('saves in full mode with create template', async () => {
|
|
1194
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1195
|
+
const emailActions = {
|
|
1196
|
+
...defaultProps.emailActions,
|
|
1197
|
+
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
1198
|
+
createTemplate: jest.fn((obj, callback) => {
|
|
1199
|
+
callback({ templateId: { _id: '123', versions: {} } });
|
|
1200
|
+
}),
|
|
1201
|
+
};
|
|
1202
|
+
const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
|
|
1203
|
+
|
|
1204
|
+
// Set subject and content via component interactions
|
|
1205
|
+
const { rerender } = renderWithIntl({
|
|
1206
|
+
isGetFormData: false,
|
|
1207
|
+
templateName: 'New Template',
|
|
1208
|
+
emailActions,
|
|
1209
|
+
getLiquidTags: null, // No liquid tags for non-liquid org
|
|
1210
|
+
});
|
|
1211
|
+
const input = screen.getByTestId('subject-input');
|
|
1212
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
1213
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
1214
|
+
fireEvent.click(changeButton);
|
|
1215
|
+
// Now trigger save
|
|
1216
|
+
rerender(
|
|
1217
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1218
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData={true} templateName="New Template" emailActions={emailActions} getLiquidTags={null} />
|
|
1219
|
+
</IntlProvider>
|
|
1220
|
+
);
|
|
1221
|
+
|
|
1222
|
+
await waitFor(() => {
|
|
1223
|
+
expect(emailActions.createTemplate).toHaveBeenCalled();
|
|
1224
|
+
}, { timeout: 3000 });
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
it('saves in full mode with edit template', async () => {
|
|
1228
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1229
|
+
const emailActions = {
|
|
1230
|
+
...defaultProps.emailActions,
|
|
1231
|
+
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
1232
|
+
createTemplate: jest.fn((obj, callback) => {
|
|
1233
|
+
callback({ templateId: { _id: '123', versions: {} } });
|
|
1234
|
+
}),
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
// Set subject and content via component interactions
|
|
1238
|
+
const { rerender } = renderWithIntl({
|
|
1239
|
+
isGetFormData: false,
|
|
1240
|
+
params: { id: '123' },
|
|
1241
|
+
Email: {
|
|
1242
|
+
templateDetails: {
|
|
1243
|
+
_id: '123',
|
|
1244
|
+
name: 'Existing Template',
|
|
1245
|
+
versions: {
|
|
1246
|
+
base: {
|
|
1247
|
+
activeTab: 'en',
|
|
1248
|
+
en: { 'template-content': '<p>Old</p>' },
|
|
1249
|
+
tabKey: 'existing-key',
|
|
1250
|
+
},
|
|
1251
|
+
},
|
|
1252
|
+
},
|
|
1253
|
+
getTemplateDetailsInProgress: false,
|
|
1254
|
+
fetchingCmsData: false,
|
|
1255
|
+
},
|
|
1256
|
+
emailActions,
|
|
1257
|
+
getLiquidTags: null, // No liquid tags for non-liquid org
|
|
1258
|
+
});
|
|
1259
|
+
// Wait for content to load from template data
|
|
1260
|
+
await waitFor(() => {
|
|
1261
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
1262
|
+
});
|
|
1263
|
+
const input = screen.getByTestId('subject-input');
|
|
1264
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
1265
|
+
// Now trigger save
|
|
1266
|
+
rerender(
|
|
1267
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1268
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData={true} params={{ id: '123' }} Email={{
|
|
1269
|
+
templateDetails: {
|
|
1270
|
+
_id: '123',
|
|
1271
|
+
name: 'Existing Template',
|
|
1272
|
+
versions: {
|
|
1273
|
+
base: {
|
|
1274
|
+
activeTab: 'en',
|
|
1275
|
+
en: { 'template-content': '<p>Old</p>' },
|
|
1276
|
+
tabKey: 'existing-key',
|
|
1277
|
+
},
|
|
1278
|
+
},
|
|
1279
|
+
},
|
|
1280
|
+
getTemplateDetailsInProgress: false,
|
|
1281
|
+
fetchingCmsData: false,
|
|
1282
|
+
}} emailActions={emailActions} getLiquidTags={null} />
|
|
1283
|
+
</IntlProvider>
|
|
1284
|
+
);
|
|
1285
|
+
|
|
1286
|
+
await waitFor(() => {
|
|
1287
|
+
expect(emailActions.createTemplate).toHaveBeenCalled();
|
|
1288
|
+
}, { timeout: 3000 });
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
it('handles create template error response', async () => {
|
|
1292
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1293
|
+
const emailActions = {
|
|
1294
|
+
...defaultProps.emailActions,
|
|
1295
|
+
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
1296
|
+
createTemplate: jest.fn((obj, callback) => {
|
|
1297
|
+
callback({ error: 'Template name already exists' });
|
|
1298
|
+
}),
|
|
1299
|
+
};
|
|
1300
|
+
const onValidationFail = jest.fn();
|
|
1301
|
+
|
|
1302
|
+
// Set subject and content via component interactions
|
|
1303
|
+
const { rerender } = renderWithIntl({
|
|
1304
|
+
isGetFormData: false,
|
|
1305
|
+
templateName: 'New Template',
|
|
1306
|
+
emailActions,
|
|
1307
|
+
onValidationFail,
|
|
1308
|
+
getLiquidTags: null, // No liquid tags for non-liquid org
|
|
1309
|
+
});
|
|
1310
|
+
const input = screen.getByTestId('subject-input');
|
|
1311
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
1312
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
1313
|
+
fireEvent.click(changeButton);
|
|
1314
|
+
// Now trigger save
|
|
1315
|
+
rerender(
|
|
1316
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1317
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData={true} templateName="New Template" emailActions={emailActions} onValidationFail={onValidationFail} getLiquidTags={null} />
|
|
1318
|
+
</IntlProvider>
|
|
1319
|
+
);
|
|
1320
|
+
|
|
1321
|
+
await waitFor(() => {
|
|
1322
|
+
expect(onValidationFail).toHaveBeenCalled();
|
|
1323
|
+
}, { timeout: 3000 });
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
it('handles create template success with getFormdata', async () => {
|
|
1327
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1328
|
+
const emailActions = {
|
|
1329
|
+
...defaultProps.emailActions,
|
|
1330
|
+
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
1331
|
+
createTemplate: jest.fn((obj, callback) => {
|
|
1332
|
+
callback({ templateId: { _id: '123', versions: { base: {} } } });
|
|
1333
|
+
}),
|
|
1334
|
+
};
|
|
1335
|
+
const getFormdata = jest.fn();
|
|
1336
|
+
const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
|
|
1337
|
+
|
|
1338
|
+
// Set subject and content via component interactions
|
|
1339
|
+
const { rerender } = renderWithIntl({
|
|
1340
|
+
isGetFormData: false,
|
|
1341
|
+
templateName: 'New Template',
|
|
1342
|
+
emailActions,
|
|
1343
|
+
getFormdata,
|
|
1344
|
+
getLiquidTags: null, // No liquid tags for non-liquid org
|
|
1345
|
+
});
|
|
1346
|
+
const input = screen.getByTestId('subject-input');
|
|
1347
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
1348
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
1349
|
+
fireEvent.click(changeButton);
|
|
1350
|
+
// Now trigger save
|
|
1351
|
+
rerender(
|
|
1352
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1353
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData={true} templateName="New Template" emailActions={emailActions} getFormdata={getFormdata} getLiquidTags={null} />
|
|
1354
|
+
</IntlProvider>
|
|
1355
|
+
);
|
|
1356
|
+
|
|
1357
|
+
await waitFor(() => {
|
|
1358
|
+
expect(getFormdata).toHaveBeenCalled();
|
|
1359
|
+
expect(CapNotification.success).toHaveBeenCalled();
|
|
1360
|
+
}, { timeout: 3000 });
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
it('handles create template success without getFormdata', async () => {
|
|
1364
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1365
|
+
const emailActions = {
|
|
1366
|
+
...defaultProps.emailActions,
|
|
1367
|
+
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
1368
|
+
createTemplate: jest.fn((obj, callback) => {
|
|
1369
|
+
callback({ templateId: { _id: '123', versions: {} } });
|
|
1370
|
+
}),
|
|
1371
|
+
};
|
|
1372
|
+
const history = require('../../../../utils/history');
|
|
1373
|
+
|
|
1374
|
+
// Set subject and content via component interactions
|
|
1375
|
+
const { rerender } = renderWithIntl({
|
|
1376
|
+
isGetFormData: false,
|
|
1377
|
+
templateName: 'New Template',
|
|
1378
|
+
emailActions,
|
|
1379
|
+
getFormdata: null,
|
|
1380
|
+
location: { query: { module: 'default' } },
|
|
1381
|
+
getLiquidTags: null, // No liquid tags for non-liquid org
|
|
1382
|
+
});
|
|
1383
|
+
const input = screen.getByTestId('subject-input');
|
|
1384
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
1385
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
1386
|
+
fireEvent.click(changeButton);
|
|
1387
|
+
// Now trigger save
|
|
1388
|
+
rerender(
|
|
1389
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1390
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData={true} templateName="New Template" emailActions={emailActions} getFormdata={null} location={{ query: { module: 'default' } }} getLiquidTags={null} />
|
|
1391
|
+
</IntlProvider>
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
await waitFor(() => {
|
|
1395
|
+
expect(history.push).toHaveBeenCalled();
|
|
1396
|
+
}, { timeout: 3000 });
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
it('saves in library mode with getFormdata', async () => {
|
|
1400
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1401
|
+
const getFormdata = jest.fn();
|
|
1402
|
+
|
|
1403
|
+
// Set subject and content via component interactions
|
|
1404
|
+
const { rerender } = renderWithIntl({
|
|
1405
|
+
isGetFormData: false,
|
|
1406
|
+
isFullMode: false,
|
|
1407
|
+
getFormdata,
|
|
1408
|
+
location: { query: { module: 'library' } },
|
|
1409
|
+
getLiquidTags: null, // No liquid tags for non-liquid org
|
|
1410
|
+
});
|
|
1411
|
+
const input = screen.getByTestId('subject-input');
|
|
1412
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
1413
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
1414
|
+
fireEvent.click(changeButton);
|
|
1415
|
+
// Now trigger save
|
|
1416
|
+
rerender(
|
|
1417
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1418
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData={true} isFullMode={false} getFormdata={getFormdata} location={{ query: { module: 'library' } }} getLiquidTags={null} />
|
|
1419
|
+
</IntlProvider>
|
|
1420
|
+
);
|
|
1421
|
+
|
|
1422
|
+
await waitFor(() => {
|
|
1423
|
+
expect(getFormdata).toHaveBeenCalled();
|
|
1424
|
+
}, { timeout: 3000 });
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
it('saves in library mode without library module', async () => {
|
|
1428
|
+
mockHasLiquidSupportFeature.mockReturnValue(false); // Non-liquid org
|
|
1429
|
+
const getFormdata = jest.fn();
|
|
1430
|
+
|
|
1431
|
+
// Set subject and content via component interactions
|
|
1432
|
+
const { rerender } = renderWithIntl({
|
|
1433
|
+
isGetFormData: false,
|
|
1434
|
+
isFullMode: false,
|
|
1435
|
+
getFormdata,
|
|
1436
|
+
location: { query: { module: 'default' } },
|
|
1437
|
+
getLiquidTags: null, // No liquid tags for non-liquid org
|
|
1438
|
+
});
|
|
1439
|
+
const input = screen.getByTestId('subject-input');
|
|
1440
|
+
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
1441
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
1442
|
+
fireEvent.click(changeButton);
|
|
1443
|
+
// Now trigger save
|
|
1444
|
+
rerender(
|
|
1445
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1446
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData={true} isFullMode={false} getFormdata={getFormdata} location={{ query: { module: 'default' } }} getLiquidTags={null} />
|
|
1447
|
+
</IntlProvider>
|
|
1448
|
+
);
|
|
1449
|
+
|
|
1450
|
+
await waitFor(() => {
|
|
1451
|
+
expect(getFormdata).toHaveBeenCalled();
|
|
1452
|
+
}, { timeout: 3000 });
|
|
1453
|
+
});
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
describe('Tag Context Change', () => {
|
|
1457
|
+
it('handles tag context change', () => {
|
|
1458
|
+
const globalActions = {
|
|
1459
|
+
fetchSchemaForEntity: jest.fn(),
|
|
1460
|
+
};
|
|
1461
|
+
|
|
1462
|
+
renderWithIntl({ globalActions });
|
|
1463
|
+
const contextButton = screen.getByTestId('trigger-context-change');
|
|
1464
|
+
fireEvent.click(contextButton);
|
|
1465
|
+
expect(globalActions.fetchSchemaForEntity).toHaveBeenCalled();
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
it('handles embedded mode context change', () => {
|
|
1469
|
+
const globalActions = {
|
|
1470
|
+
fetchSchemaForEntity: jest.fn(),
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1473
|
+
renderWithIntl({
|
|
1474
|
+
globalActions,
|
|
1475
|
+
location: { query: { type: 'embedded', module: 'test' } },
|
|
1476
|
+
});
|
|
1477
|
+
const contextButton = screen.getByTestId('trigger-context-change');
|
|
1478
|
+
fireEvent.click(contextButton);
|
|
1479
|
+
expect(globalActions.fetchSchemaForEntity).toHaveBeenCalled();
|
|
1480
|
+
});
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
describe('Template Name Handling', () => {
|
|
1484
|
+
it('calls showTemplateName in create mode', () => {
|
|
1485
|
+
const showTemplateName = jest.fn();
|
|
1486
|
+
renderWithIntl({
|
|
1487
|
+
showTemplateName,
|
|
1488
|
+
templateName: 'New Template',
|
|
1489
|
+
isFullMode: true,
|
|
1490
|
+
});
|
|
1491
|
+
expect(showTemplateName).toHaveBeenCalled();
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
it('calls showTemplateName in edit mode', () => {
|
|
1495
|
+
const showTemplateName = jest.fn();
|
|
1496
|
+
renderWithIntl({
|
|
1497
|
+
showTemplateName,
|
|
1498
|
+
params: { id: '123' },
|
|
1499
|
+
Email: {
|
|
1500
|
+
templateDetails: {
|
|
1501
|
+
_id: '123',
|
|
1502
|
+
name: 'Existing Template',
|
|
1503
|
+
},
|
|
1504
|
+
},
|
|
1505
|
+
isFullMode: true,
|
|
1506
|
+
});
|
|
1507
|
+
expect(showTemplateName).toHaveBeenCalled();
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
it('handles form data change', () => {
|
|
1511
|
+
const onFormDataChange = jest.fn();
|
|
1512
|
+
const showTemplateName = jest.fn();
|
|
1513
|
+
renderWithIntl({
|
|
1514
|
+
onFormDataChange,
|
|
1515
|
+
showTemplateName,
|
|
1516
|
+
isFullMode: true,
|
|
1517
|
+
});
|
|
1518
|
+
// Form data change would be triggered by showTemplateName callback
|
|
1519
|
+
});
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
describe('Loading State Management', () => {
|
|
1523
|
+
it('manages loading state based on API calls', () => {
|
|
1524
|
+
const { rerender } = renderWithIntl({ loadingTags: true });
|
|
1525
|
+
|
|
1526
|
+
rerender(
|
|
1527
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1528
|
+
<EmailHTMLEditor {...defaultProps} loadingTags={false} />
|
|
1529
|
+
</IntlProvider>
|
|
1530
|
+
);
|
|
1531
|
+
// Loading should be updated
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
it('stops loading when all APIs complete', () => {
|
|
1535
|
+
const setIsLoadingContent = jest.fn();
|
|
1536
|
+
renderWithIntl({
|
|
1537
|
+
loadingTags: false,
|
|
1538
|
+
fetchingLiquidTags: false,
|
|
1539
|
+
createTemplateInProgress: false,
|
|
1540
|
+
fetchingCmsData: false,
|
|
1541
|
+
tags: [],
|
|
1542
|
+
setIsLoadingContent,
|
|
1543
|
+
});
|
|
1544
|
+
// Loading should stop
|
|
1545
|
+
});
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
describe('Edge Cases', () => {
|
|
1549
|
+
it('handles missing globalActions', () => {
|
|
1550
|
+
renderWithIntl({ globalActions: null });
|
|
1551
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
it('handles missing emailActions', () => {
|
|
1555
|
+
renderWithIntl({ emailActions: null });
|
|
1556
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
it('handles missing getLiquidTags with globalActions fallback', () => {
|
|
1560
|
+
const globalActions = {
|
|
1561
|
+
getLiquidTags: jest.fn((content, callback) => {
|
|
1562
|
+
callback({ askAiraResponse: { data: [] }, isError: false });
|
|
1563
|
+
}),
|
|
1564
|
+
};
|
|
1565
|
+
renderWithIntl({
|
|
1566
|
+
getLiquidTags: null,
|
|
1567
|
+
globalActions,
|
|
1568
|
+
isLiquidEnabled: true,
|
|
1569
|
+
isGetFormData: true,
|
|
1570
|
+
subject: 'Valid Subject',
|
|
1571
|
+
htmlContent: '<p>Content</p>',
|
|
1572
|
+
});
|
|
1573
|
+
// Should use globalActions.getLiquidTags
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
it('handles empty template data gracefully', () => {
|
|
1577
|
+
renderWithIntl({
|
|
1578
|
+
templateData: {},
|
|
1579
|
+
params: { id: '123' },
|
|
1580
|
+
});
|
|
1581
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1584
|
+
it('handles template switching', () => {
|
|
1585
|
+
const { rerender } = renderWithIntl({
|
|
1586
|
+
params: { id: '123' },
|
|
1587
|
+
Email: {
|
|
1588
|
+
templateDetails: { _id: '123', name: 'Template 1' },
|
|
1589
|
+
getTemplateDetailsInProgress: false,
|
|
1590
|
+
fetchingCmsData: false,
|
|
1591
|
+
},
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
rerender(
|
|
1595
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1596
|
+
<EmailHTMLEditor
|
|
1597
|
+
{...defaultProps}
|
|
1598
|
+
params={{ id: '456' }}
|
|
1599
|
+
Email={{
|
|
1600
|
+
templateDetails: { _id: '456', name: 'Template 2' },
|
|
1601
|
+
getTemplateDetailsInProgress: false,
|
|
1602
|
+
fetchingCmsData: false,
|
|
1603
|
+
}}
|
|
1604
|
+
/>
|
|
1605
|
+
</IntlProvider>
|
|
1606
|
+
);
|
|
1607
|
+
// Should handle template switch
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
it('handles isGetFormData trigger', () => {
|
|
1611
|
+
const onValidationFail = jest.fn();
|
|
1612
|
+
const { rerender } = renderWithIntl({
|
|
1613
|
+
isGetFormData: false,
|
|
1614
|
+
onValidationFail,
|
|
1615
|
+
subject: 'Valid Subject',
|
|
1616
|
+
htmlContent: '<p>Content</p>',
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
rerender(
|
|
1620
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1621
|
+
<EmailHTMLEditor
|
|
1622
|
+
{...defaultProps}
|
|
1623
|
+
isGetFormData
|
|
1624
|
+
onValidationFail={onValidationFail}
|
|
1625
|
+
subject="Valid Subject"
|
|
1626
|
+
htmlContent="<p>Content</p>"
|
|
1627
|
+
/>
|
|
1628
|
+
</IntlProvider>
|
|
1629
|
+
);
|
|
1630
|
+
// Should trigger save
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
it('handles isGetFormData reset', () => {
|
|
1634
|
+
const { rerender } = renderWithIntl({
|
|
1635
|
+
isGetFormData: true,
|
|
1636
|
+
subject: 'Valid Subject',
|
|
1637
|
+
htmlContent: '<p>Content</p>',
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
rerender(
|
|
1641
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1642
|
+
<EmailHTMLEditor
|
|
1643
|
+
{...defaultProps}
|
|
1644
|
+
isGetFormData={false}
|
|
1645
|
+
subject="Valid Subject"
|
|
1646
|
+
htmlContent="<p>Content</p>"
|
|
1647
|
+
/>
|
|
1648
|
+
</IntlProvider>
|
|
1649
|
+
);
|
|
1650
|
+
// Should reset ref
|
|
1651
|
+
});
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
describe('useImperativeHandle methods', () => {
|
|
1655
|
+
// Note: These methods are tested indirectly through component integration
|
|
1656
|
+
// Direct ref testing requires proper component mounting which can be complex in test environment
|
|
1657
|
+
// The methods are covered through integration tests and actual usage
|
|
1658
|
+
|
|
1659
|
+
it('should expose ref methods when component is mounted', async () => {
|
|
1660
|
+
const ref = React.createRef();
|
|
1661
|
+
const currentOrgDetails = {
|
|
1662
|
+
basic_details: {
|
|
1663
|
+
base_language: 'en',
|
|
1664
|
+
},
|
|
1665
|
+
};
|
|
1666
|
+
|
|
1667
|
+
render(
|
|
1668
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1669
|
+
<EmailHTMLEditor
|
|
1670
|
+
{...defaultProps}
|
|
1671
|
+
ref={ref}
|
|
1672
|
+
currentOrgDetails={currentOrgDetails}
|
|
1673
|
+
subject="Test Subject"
|
|
1674
|
+
htmlContent="<p>Test Content</p>"
|
|
1675
|
+
/>
|
|
1676
|
+
</IntlProvider>
|
|
1677
|
+
);
|
|
1678
|
+
|
|
1679
|
+
await waitFor(() => {
|
|
1680
|
+
expect(ref.current).toBeTruthy();
|
|
1681
|
+
}, { timeout: 3000 });
|
|
1682
|
+
|
|
1683
|
+
// Verify ref methods exist
|
|
1684
|
+
expect(typeof ref.current?.getFormDataForPreview).toBe('function');
|
|
1685
|
+
expect(typeof ref.current?.getContentForPreview).toBe('function');
|
|
1686
|
+
expect(typeof ref.current?.getValidationState).toBe('function');
|
|
1687
|
+
expect(typeof ref.current?.isContentEmpty).toBe('function');
|
|
1688
|
+
expect(typeof ref.current?.getIssueCounts).toBe('function');
|
|
1689
|
+
});
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
describe('Template data extraction', () => {
|
|
1693
|
+
// Note: Template data extraction is tested indirectly through component behavior
|
|
1694
|
+
// Direct testing requires complex effect timing and state management
|
|
1695
|
+
// The extraction logic is covered through integration tests
|
|
1696
|
+
|
|
1697
|
+
it('should handle templateDataProp in library mode', () => {
|
|
1698
|
+
const templateDataProp = {
|
|
1699
|
+
'template-content': '<p>Library Content</p>',
|
|
1700
|
+
emailSubject: 'Library Subject',
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
// Component should render without errors when templateData is provided
|
|
1704
|
+
renderWithIntl({
|
|
1705
|
+
isFullMode: false,
|
|
1706
|
+
templateData: templateDataProp,
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
// Verify component renders
|
|
1710
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
1711
|
+
});
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
describe('setIsLoadingContent callback', () => {
|
|
1715
|
+
it('should call setIsLoadingContent when uploaded content is available', async () => {
|
|
1716
|
+
const setIsLoadingContent = jest.fn();
|
|
1717
|
+
const EmailLayout = {
|
|
1718
|
+
'template-content': '<p>Uploaded Content</p>',
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
renderWithIntl({
|
|
1722
|
+
setIsLoadingContent,
|
|
1723
|
+
EmailLayout,
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
await waitFor(() => {
|
|
1727
|
+
expect(setIsLoadingContent).toHaveBeenCalledWith(false);
|
|
1728
|
+
});
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
it('should call setIsLoadingContent when template data is loaded', async () => {
|
|
1732
|
+
const setIsLoadingContent = jest.fn();
|
|
1733
|
+
const templateDataProp = {
|
|
1734
|
+
'template-content': '<p>Template Content</p>',
|
|
1735
|
+
emailSubject: 'Template Subject',
|
|
1736
|
+
};
|
|
1737
|
+
|
|
1738
|
+
renderWithIntl({
|
|
1739
|
+
setIsLoadingContent,
|
|
1740
|
+
isFullMode: false,
|
|
1741
|
+
templateData: templateDataProp,
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
await waitFor(() => {
|
|
1745
|
+
expect(setIsLoadingContent).toHaveBeenCalledWith(false);
|
|
1746
|
+
});
|
|
1747
|
+
});
|
|
1748
|
+
});
|
|
1749
|
+
});
|