@capillarytech/creatives-library 8.0.246-alpha.0 → 8.0.246

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