@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.
Files changed (136) hide show
  1. package/assets/Android.png +0 -0
  2. package/assets/iOS.png +0 -0
  3. package/constants/unified.js +2 -1
  4. package/initialReducer.js +2 -0
  5. package/package.json +1 -1
  6. package/services/api.js +10 -0
  7. package/services/tests/api.test.js +18 -0
  8. package/utils/common.js +5 -0
  9. package/utils/commonUtils.js +28 -5
  10. package/utils/tests/commonUtil.test.js +224 -0
  11. package/utils/transformTemplateConfig.js +0 -10
  12. package/v2Components/CapDeviceContent/index.js +61 -56
  13. package/v2Components/CapTagList/index.js +6 -1
  14. package/v2Components/CapTagListWithInput/index.js +5 -1
  15. package/v2Components/CapTagListWithInput/messages.js +1 -1
  16. package/v2Components/CapWhatsappCTA/tests/index.test.js +5 -0
  17. package/v2Components/ErrorInfoNote/index.js +452 -72
  18. package/v2Components/ErrorInfoNote/messages.js +22 -0
  19. package/v2Components/ErrorInfoNote/style.scss +280 -4
  20. package/v2Components/FormBuilder/tests/index.test.js +13 -4
  21. package/v2Components/HtmlEditor/HTMLEditor.js +640 -94
  22. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +874 -0
  23. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1167 -133
  24. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +27 -16
  25. package/v2Components/HtmlEditor/_htmlEditor.scss +108 -45
  26. package/v2Components/HtmlEditor/_index.lazy.scss +1 -1
  27. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +13 -101
  28. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +148 -139
  29. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +2 -1
  30. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
  31. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +9 -0
  32. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +1 -1
  33. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +22 -0
  34. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +4 -7
  35. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +35 -45
  36. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +1 -3
  37. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
  38. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +7 -6
  39. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +3 -6
  40. package/v2Components/HtmlEditor/components/PreviewPane/index.js +11 -13
  41. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
  42. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +49 -31
  43. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +68 -39
  44. package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +254 -0
  45. package/v2Components/HtmlEditor/components/ValidationTabs/index.js +391 -0
  46. package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +51 -0
  47. package/v2Components/HtmlEditor/constants.js +42 -20
  48. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +373 -16
  49. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.apiErrors.test.js +795 -0
  50. package/v2Components/HtmlEditor/hooks/useEditorContent.js +5 -2
  51. package/v2Components/HtmlEditor/hooks/useInAppContent.js +88 -146
  52. package/v2Components/HtmlEditor/hooks/useValidation.js +189 -53
  53. package/v2Components/HtmlEditor/index.js +1 -1
  54. package/v2Components/HtmlEditor/messages.js +95 -85
  55. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +94 -45
  56. package/v2Components/HtmlEditor/utils/contentSanitizer.js +40 -41
  57. package/v2Components/HtmlEditor/utils/htmlValidator.js +71 -72
  58. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +134 -102
  59. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +23 -25
  60. package/v2Components/HtmlEditor/utils/validationAdapter.js +66 -41
  61. package/v2Components/MobilePushPreviewV2/index.js +32 -7
  62. package/v2Components/TemplatePreview/_templatePreview.scss +44 -24
  63. package/v2Components/TemplatePreview/index.js +47 -32
  64. package/v2Components/TemplatePreview/messages.js +4 -0
  65. package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +1 -0
  66. package/v2Containers/BeeEditor/index.js +172 -90
  67. package/v2Containers/BeePopupEditor/constants.js +10 -0
  68. package/v2Containers/BeePopupEditor/index.js +193 -0
  69. package/v2Containers/BeePopupEditor/tests/index.test.js +627 -0
  70. package/v2Containers/CreativesContainer/SlideBoxContent.js +127 -51
  71. package/v2Containers/CreativesContainer/SlideBoxFooter.js +163 -13
  72. package/v2Containers/CreativesContainer/SlideBoxHeader.js +2 -1
  73. package/v2Containers/CreativesContainer/constants.js +1 -0
  74. package/v2Containers/CreativesContainer/index.js +239 -46
  75. package/v2Containers/CreativesContainer/messages.js +8 -0
  76. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +11 -2
  77. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +38 -50
  78. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +106 -0
  79. package/v2Containers/Email/actions.js +7 -0
  80. package/v2Containers/Email/constants.js +5 -1
  81. package/v2Containers/Email/index.js +222 -27
  82. package/v2Containers/Email/messages.js +32 -0
  83. package/v2Containers/Email/reducer.js +12 -1
  84. package/v2Containers/Email/sagas.js +61 -7
  85. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -0
  86. package/v2Containers/Email/tests/sagas.test.js +320 -29
  87. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1321 -0
  88. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +210 -15
  89. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +40 -74
  90. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +1749 -0
  91. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +520 -0
  92. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +2 -67
  93. package/v2Containers/EmailWrapper/constants.js +2 -0
  94. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +629 -77
  95. package/v2Containers/EmailWrapper/index.js +103 -23
  96. package/v2Containers/EmailWrapper/messages.js +61 -1
  97. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +643 -0
  98. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +594 -77
  99. package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +376 -0
  100. package/v2Containers/InApp/__tests__/sagas.test.js +363 -0
  101. package/v2Containers/InApp/actions.js +7 -0
  102. package/v2Containers/InApp/constants.js +20 -4
  103. package/v2Containers/InApp/index.js +802 -359
  104. package/v2Containers/InApp/index.scss +4 -3
  105. package/v2Containers/InApp/messages.js +7 -3
  106. package/v2Containers/InApp/reducer.js +21 -3
  107. package/v2Containers/InApp/sagas.js +29 -9
  108. package/v2Containers/InApp/selectors.js +25 -5
  109. package/v2Containers/InApp/tests/index.test.js +154 -50
  110. package/v2Containers/InApp/tests/reducer.test.js +34 -0
  111. package/v2Containers/InApp/tests/sagas.test.js +61 -9
  112. package/v2Containers/InApp/tests/selectors.test.js +612 -0
  113. package/v2Containers/InAppWrapper/components/InAppWrapperView.js +162 -0
  114. package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +267 -0
  115. package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +9 -0
  116. package/v2Containers/InAppWrapper/constants.js +16 -0
  117. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +473 -0
  118. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +198 -0
  119. package/v2Containers/InAppWrapper/index.js +148 -0
  120. package/v2Containers/InAppWrapper/messages.js +49 -0
  121. package/v2Containers/InappAdvance/index.js +1099 -0
  122. package/v2Containers/InappAdvance/index.scss +10 -0
  123. package/v2Containers/InappAdvance/tests/index.test.js +448 -0
  124. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
  125. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
  126. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +2 -0
  127. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +9 -0
  128. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +12 -0
  129. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4 -0
  130. package/v2Containers/TagList/index.js +62 -19
  131. package/v2Containers/Templates/_templates.scss +60 -1
  132. package/v2Containers/Templates/index.js +89 -4
  133. package/v2Containers/Templates/messages.js +4 -0
  134. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -0
  135. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +0 -152
  136. 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
+ });