@capillarytech/creatives-library 8.0.276 → 8.0.278

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 (26) hide show
  1. package/package.json +1 -1
  2. package/utils/tests/imageUrlUpload.test.js +298 -0
  3. package/v2Containers/CreativesContainer/SlideBoxContent.js +2 -0
  4. package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -2
  5. package/v2Containers/CreativesContainer/index.js +10 -6
  6. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +165 -41
  7. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +6 -0
  8. package/v2Containers/Email/index.js +75 -9
  9. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +14 -8
  10. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +6 -2
  11. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +189 -6
  12. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +137 -0
  13. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +7 -3
  14. package/v2Containers/EmailWrapper/index.js +3 -0
  15. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +26 -0
  16. package/v2Containers/Facebook/Advertisement/index.js +1 -1
  17. package/v2Containers/Line/Container/_lineCreate.scss +1 -0
  18. package/v2Containers/Line/Container/style.js +1 -1
  19. package/v2Containers/MobilePush/Edit/index.js +6 -5
  20. package/v2Containers/SmsTrai/Create/index.scss +1 -1
  21. package/v2Containers/SmsTrai/Edit/index.js +9 -3
  22. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +11682 -86
  23. package/v2Containers/SmsTrai/Edit/tests/index.test.js +5 -0
  24. package/v2Containers/Viber/index.js +7 -0
  25. package/v2Containers/Viber/index.scss +4 -1
  26. package/v2Containers/Viber/style.js +0 -2
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.276",
4
+ "version": "8.0.278",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Tests for imageUrlUpload utils: fetchImageFromUrl and uploadImageFromUrlHelper
3
+ */
4
+
5
+ import { fetchImageFromUrl, uploadImageFromUrlHelper } from '../imageUrlUpload';
6
+
7
+ describe('imageUrlUpload', () => {
8
+ const formatMessage = jest.fn((msg) => msg?.id ?? msg?.defaultMessage ?? 'formatted');
9
+ const messages = {
10
+ imageTypeInvalid: { id: 'imageTypeInvalid' },
11
+ imageSizeInvalid: { id: 'imageSizeInvalid' },
12
+ imageLoadError: { id: 'imageLoadError' },
13
+ };
14
+
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+ formatMessage.mockImplementation((msg) => (msg && typeof msg === 'object' && msg.id) ? msg.id : 'formatted');
18
+ });
19
+
20
+ describe('fetchImageFromUrl', () => {
21
+ const validUrl = 'https://example.com/image.jpg';
22
+
23
+ it('throws when url is missing', async () => {
24
+ await expect(fetchImageFromUrl()).rejects.toThrow('URL is required');
25
+ await expect(fetchImageFromUrl(null)).rejects.toThrow('URL is required');
26
+ });
27
+
28
+ it('throws when url is empty string', async () => {
29
+ await expect(fetchImageFromUrl('')).rejects.toThrow('URL is required');
30
+ });
31
+
32
+ it('throws when url is only whitespace', async () => {
33
+ await expect(fetchImageFromUrl(' ')).rejects.toThrow('URL is required');
34
+ });
35
+
36
+ it('trims url before fetching', async () => {
37
+ const mockResponse = { ok: true, headers: new Headers() };
38
+ global.fetch = jest.fn().mockResolvedValue(mockResponse);
39
+
40
+ await fetchImageFromUrl(' https://example.com/img.png ');
41
+ expect(global.fetch).toHaveBeenCalledWith(
42
+ 'https://example.com/img.png',
43
+ expect.objectContaining({ method: 'GET', redirect: 'follow', mode: 'cors' })
44
+ );
45
+ });
46
+
47
+ it('returns response when fetch is ok', async () => {
48
+ const mockResponse = { ok: true, status: 200, headers: new Headers() };
49
+ global.fetch = jest.fn().mockResolvedValue(mockResponse);
50
+
51
+ const result = await fetchImageFromUrl(validUrl);
52
+ expect(result).toBe(mockResponse);
53
+ expect(global.fetch).toHaveBeenCalledWith(
54
+ validUrl,
55
+ expect.objectContaining({ method: 'GET', redirect: 'follow', mode: 'cors' })
56
+ );
57
+ });
58
+
59
+ it('throws when response is not ok', async () => {
60
+ const mockResponse = { ok: false, status: 404, statusText: 'Not Found' };
61
+ global.fetch = jest.fn().mockResolvedValue(mockResponse);
62
+
63
+ await expect(fetchImageFromUrl(validUrl)).rejects.toThrow(
64
+ 'Failed to fetch image: 404 Not Found'
65
+ );
66
+ });
67
+
68
+ it('throws on network or CORS error', async () => {
69
+ global.fetch = jest.fn().mockRejectedValue(new Error('Network error'));
70
+
71
+ await expect(fetchImageFromUrl(validUrl)).rejects.toThrow('Network error');
72
+ });
73
+ });
74
+
75
+ describe('uploadImageFromUrlHelper', () => {
76
+ const uploadAssetFn = jest.fn();
77
+ const fileNamePrefix = 'test-image';
78
+ const maxSize = 5000000;
79
+
80
+ it('returns error when content type is not allowed', async () => {
81
+ const response = {
82
+ ok: true,
83
+ headers: new Headers({ 'Content-Type': 'text/html' }),
84
+ blob: jest.fn().mockResolvedValue(new Blob()),
85
+ };
86
+ global.fetch = jest.fn().mockResolvedValue(response);
87
+
88
+ const result = await uploadImageFromUrlHelper(
89
+ 'https://example.com/page',
90
+ formatMessage,
91
+ messages,
92
+ uploadAssetFn,
93
+ fileNamePrefix,
94
+ maxSize,
95
+ ['image/jpeg', 'image/png']
96
+ );
97
+
98
+ expect(result).toEqual({ success: false, error: 'imageTypeInvalid' });
99
+ expect(formatMessage).toHaveBeenCalledWith(messages.imageTypeInvalid);
100
+ expect(uploadAssetFn).not.toHaveBeenCalled();
101
+ });
102
+
103
+ it('normalizes content type by stripping charset and trimming', async () => {
104
+ const response = {
105
+ ok: true,
106
+ headers: new Headers({ 'Content-Type': ' image/PNG; charset=utf-8 ' }),
107
+ blob: jest.fn().mockResolvedValue(new Blob(['x'], { type: 'image/png' })),
108
+ };
109
+ global.fetch = jest.fn().mockResolvedValue(response);
110
+
111
+ const OriginalImage = global.Image;
112
+ global.Image = class MockImage {
113
+ constructor() {
114
+ this.width = 10;
115
+ this.height = 10;
116
+ setTimeout(() => { if (this.onload) this.onload(); }, 0);
117
+ }
118
+
119
+ get src() { return this._src; }
120
+
121
+ set src(v) { this._src = v; }
122
+ };
123
+
124
+ const result = await uploadImageFromUrlHelper(
125
+ 'https://example.com/img.png',
126
+ formatMessage,
127
+ messages,
128
+ uploadAssetFn,
129
+ fileNamePrefix,
130
+ maxSize,
131
+ ['image/png']
132
+ );
133
+
134
+ global.Image = OriginalImage;
135
+
136
+ expect(result.success).toBe(true);
137
+ expect(uploadAssetFn).toHaveBeenCalledWith(
138
+ expect.any(File),
139
+ 'image',
140
+ expect.objectContaining({ width: 10, height: 10, error: false })
141
+ );
142
+ const file = uploadAssetFn.mock.calls[0][0];
143
+ expect(file.name).toMatch(/\.png$/);
144
+ });
145
+
146
+ it('returns error when blob size exceeds maxSize', async () => {
147
+ const largeBlob = new Blob([new ArrayBuffer(maxSize + 1)]);
148
+ const response = {
149
+ ok: true,
150
+ headers: new Headers({ 'Content-Type': 'image/jpeg' }),
151
+ blob: jest.fn().mockResolvedValue(largeBlob),
152
+ };
153
+ global.fetch = jest.fn().mockResolvedValue(response);
154
+
155
+ const result = await uploadImageFromUrlHelper(
156
+ 'https://example.com/large.jpg',
157
+ formatMessage,
158
+ messages,
159
+ uploadAssetFn,
160
+ fileNamePrefix,
161
+ maxSize
162
+ );
163
+
164
+ expect(result).toEqual({ success: false, error: 'imageSizeInvalid' });
165
+ expect(formatMessage).toHaveBeenCalledWith(messages.imageSizeInvalid);
166
+ expect(uploadAssetFn).not.toHaveBeenCalled();
167
+ });
168
+
169
+ it('returns error when image fails to load (invalid image data)', async () => {
170
+ const blob = new Blob(['not-an-image'], { type: 'image/jpeg' });
171
+ const response = {
172
+ ok: true,
173
+ headers: new Headers({ 'Content-Type': 'image/jpeg' }),
174
+ blob: jest.fn().mockResolvedValue(blob),
175
+ };
176
+ global.fetch = jest.fn().mockResolvedValue(response);
177
+
178
+ const OriginalImage = global.Image;
179
+ global.Image = class MockImage {
180
+ constructor() {
181
+ setTimeout(() => {
182
+ if (this.onerror) this.onerror(new Event('error'));
183
+ }, 0);
184
+ }
185
+
186
+ get src() { return this._src; }
187
+
188
+ set src(v) { this._src = v; }
189
+ };
190
+
191
+ const result = await uploadImageFromUrlHelper(
192
+ 'https://example.com/bad.jpg',
193
+ formatMessage,
194
+ messages,
195
+ uploadAssetFn,
196
+ fileNamePrefix,
197
+ maxSize
198
+ );
199
+
200
+ global.Image = OriginalImage;
201
+
202
+ expect(result.success).toBe(false);
203
+ expect(result.error).toBe('imageLoadError');
204
+ expect(formatMessage).toHaveBeenCalledWith(messages.imageLoadError);
205
+ expect(uploadAssetFn).not.toHaveBeenCalled();
206
+ });
207
+
208
+ it('returns error on fetch failure and uses imageLoadError message', async () => {
209
+ global.fetch = jest.fn().mockRejectedValue(new Error('CORS'));
210
+
211
+ const result = await uploadImageFromUrlHelper(
212
+ 'https://example.com/image.jpg',
213
+ formatMessage,
214
+ messages,
215
+ uploadAssetFn,
216
+ fileNamePrefix,
217
+ maxSize
218
+ );
219
+
220
+ expect(result).toEqual({ success: false, error: 'imageLoadError' });
221
+ expect(formatMessage).toHaveBeenCalledWith(messages.imageLoadError);
222
+ expect(uploadAssetFn).not.toHaveBeenCalled();
223
+ });
224
+
225
+ it('trims url before processing', async () => {
226
+ const response = {
227
+ ok: true,
228
+ headers: new Headers({ 'Content-Type': 'image/jpeg' }),
229
+ blob: jest.fn().mockResolvedValue(new Blob()),
230
+ };
231
+ global.fetch = jest.fn().mockResolvedValue(response);
232
+
233
+ await uploadImageFromUrlHelper(
234
+ ' https://example.com/img.jpg ',
235
+ formatMessage,
236
+ messages,
237
+ uploadAssetFn,
238
+ fileNamePrefix,
239
+ maxSize
240
+ );
241
+
242
+ expect(global.fetch).toHaveBeenCalledWith(
243
+ 'https://example.com/img.jpg',
244
+ expect.any(Object)
245
+ );
246
+ });
247
+
248
+ it('calls uploadAssetFn with file, type and fileParams on success', async () => {
249
+ const blob = new Blob([new ArrayBuffer(100)], { type: 'image/png' });
250
+ const response = {
251
+ ok: true,
252
+ headers: new Headers({ 'Content-Type': 'image/png' }),
253
+ blob: jest.fn().mockResolvedValue(blob),
254
+ };
255
+ global.fetch = jest.fn().mockResolvedValue(response);
256
+
257
+ const OriginalImage = global.Image;
258
+ global.Image = class MockImage {
259
+ constructor() {
260
+ this.width = 100;
261
+ this.height = 80;
262
+ setTimeout(() => {
263
+ if (this.onload) this.onload();
264
+ }, 0);
265
+ }
266
+
267
+ get src() { return this._src; }
268
+
269
+ set src(v) { this._src = v; }
270
+ };
271
+
272
+ const result = await uploadImageFromUrlHelper(
273
+ 'https://example.com/valid.png',
274
+ formatMessage,
275
+ messages,
276
+ uploadAssetFn,
277
+ fileNamePrefix,
278
+ maxSize
279
+ );
280
+
281
+ global.Image = OriginalImage;
282
+
283
+ expect(result).toEqual({ success: true, error: '' });
284
+ expect(uploadAssetFn).toHaveBeenCalledTimes(1);
285
+ const [file, type, fileParams] = uploadAssetFn.mock.calls[0];
286
+ expect(file).toBeInstanceOf(File);
287
+ expect(file.name).toMatch(new RegExp(`^${fileNamePrefix}\\.png$`));
288
+ expect(type).toBe('image');
289
+ expect(fileParams).toEqual(
290
+ expect.objectContaining({
291
+ width: 100,
292
+ height: 80,
293
+ error: false,
294
+ })
295
+ );
296
+ });
297
+ });
298
+ });
@@ -643,6 +643,7 @@ export function SlideBoxContent(props) {
643
643
  {isEmailCreate && (
644
644
  <EmailWrapper
645
645
  key="creatives-email-wrapper"
646
+ isEditEmail={false}
646
647
  date={new Date().getMilliseconds()}
647
648
  setIsLoadingContent={setIsLoadingContent}
648
649
  onEmailModeChange={onEmailModeChange}
@@ -723,6 +724,7 @@ export function SlideBoxContent(props) {
723
724
  return (
724
725
  <EmailWrapper
725
726
  key="cretives-container-email-edit-wrapper"
727
+ isEditEmail
726
728
  setIsLoadingContent={setIsLoadingContent}
727
729
  onEmailModeChange={onEmailModeChange}
728
730
  emailCreateMode="editor"
@@ -8,6 +8,7 @@ import messages from './messages';
8
8
  import ErrorInfoNote from '../../v2Components/ErrorInfoNote';
9
9
  import { PREVIEW } from './constants';
10
10
  import { EMAIL_CREATE_MODES } from '../EmailWrapper/constants';
11
+ import { hasSupportCKEditor } from '../../utils/common';
11
12
 
12
13
  function getFullModeSaveBtn(slidBoxContent, isCreatingTemplate) {
13
14
  if (isCreatingTemplate) {
@@ -72,8 +73,10 @@ function SlideBoxFooter(props) {
72
73
  const isBEEEditor = selectedEmailCreateMode === EMAIL_CREATE_MODES.DRAG_DROP
73
74
  || (emailCreateMode === EMAIL_CREATE_MODES.EDITOR && !isHTMLEditorMode)
74
75
  || (isEditMode && !isHtmlEditorValidationStateActive);
76
+ const isSupportCKEditor = hasSupportCKEditor();
75
77
  // Only check validation for HTML Editor mode, not for BEE/DragDrop editor
76
- const shouldCheckValidation = isEmailChannel && htmlEditorValidationState && isHTMLEditorMode && !isBEEEditor;
78
+ // In upload mode the legacy Email (CK) component does not report validation state, so do not disable buttons
79
+ const shouldCheckValidation = isEmailChannel && htmlEditorValidationState && isHTMLEditorMode && !isBEEEditor && !isSupportCKEditor;
77
80
  const isContentEmpty = shouldCheckValidation ? (htmlEditorValidationState?.isContentEmpty ?? true) : false;
78
81
  // Check if validation has completed
79
82
  const validationComplete = shouldCheckValidation ? (htmlEditorValidationState?.validationComplete ?? false) : true;
@@ -123,7 +126,7 @@ function SlideBoxFooter(props) {
123
126
  const isBEEEditorMode = isBEEEditorModeInEdit || isBEEEditorModeInCreate;
124
127
  const hasBEEEditorErrors = isEmailChannel && isBEEEditorMode && (hasStandardErrors || hasLiquidErrors) && (!htmlEditorValidationState || !htmlEditorHasErrors);
125
128
 
126
- const shouldShowErrorInfoNote = hasBEEEditorErrors;
129
+ const shouldShowErrorInfoNote = hasBEEEditorErrors || isSupportCKEditor;
127
130
  return (
128
131
  <div className="template-footer-width">
129
132
  {shouldShowErrorInfoNote && (
@@ -1781,16 +1781,20 @@ export class Creatives extends React.Component {
1781
1781
  const {
1782
1782
  slidBoxContent, templateStep, currentChannel, emailCreateMode, mobilePushCreateMode, inAppEditorType, weChatTemplateType,
1783
1783
  } = this.state;
1784
+ const { isFullMode } = this.props;
1785
+ const channel = currentChannel?.toUpperCase?.() || '';
1786
+ // In library/embedded mode show Continue only for EMAIL and MOBILE_PUSH; hide for other channels
1787
+ if (!isFullMode && channel !== constants.EMAIL && channel !== constants.MOBILE_PUSH) {
1788
+ return false;
1789
+ }
1784
1790
  let isShowContinueFooter = false;
1785
1791
  const currentStep = this.creativesTemplateSteps[templateStep];
1786
- const channel = currentChannel.toUpperCase();
1787
1792
  // Check if supportCKEditor is false (new flow)
1788
1793
  const supportCKEditor = commonUtil.hasSupportCKEditor(); // Default to legacy flow
1789
1794
  if (channel === constants.EMAIL || channel === constants.SMS) {
1790
- // New flow: Show Continue button when supportCKEditor is false and in modeSelection
1791
- // Always show it (even if disabled) - visibility is separate from enabled state
1795
+ // New flow: Show Continue button when supportCKEditor is false and in modeSelection (full mode only)
1792
1796
  if (!supportCKEditor && currentStep === 'modeSelection' && slidBoxContent === 'createTemplate') {
1793
- return true; // Return early to ensure visibility
1797
+ return true;
1794
1798
  }
1795
1799
 
1796
1800
  // Legacy flow: Original logic (only when supportCKEditor is true)
@@ -1799,9 +1803,9 @@ export class Creatives extends React.Component {
1799
1803
  isEmailCreate = currentChannel.toUpperCase() === constants.EMAIL && ((emailCreateMode === "upload" && currentStep !== 'createTemplateContent') || (emailCreateMode === "editor" && currentStep !== 'createTemplateContent' && currentStep !== "templateSelection"));
1800
1804
  isShowContinueFooter = isEmailCreate && emailCreateMode;
1801
1805
  }
1802
- } else if (currentChannel.toUpperCase() === constants.MOBILE_PUSH) {
1806
+ } else if (channel === constants.MOBILE_PUSH) {
1803
1807
  isShowContinueFooter = !isEmpty(mobilePushCreateMode) && currentStep === "modeSelection";
1804
- } else if (currentChannel.toUpperCase() === constants.WECHAT) {
1808
+ } else if (channel === constants.WECHAT) {
1805
1809
  isShowContinueFooter = !isEmpty(weChatTemplateType) && currentStep === "modeSelection";
1806
1810
  }
1807
1811
 
@@ -11,6 +11,11 @@ import {
11
11
  } from '../../../utils/test-utils';
12
12
  import SlideBoxFooter from "../SlideBoxFooter";
13
13
 
14
+ jest.mock('../../../utils/common', () => ({
15
+ ...jest.requireActual('../../../utils/common'),
16
+ hasSupportCKEditor: jest.fn(),
17
+ }));
18
+
14
19
  const ComponentToRender = injectIntl(SlideBoxFooter);
15
20
  const renderComponent = props => {
16
21
  const store = configureStore({}, initialReducer, history);
@@ -21,49 +26,168 @@ const renderComponent = props => {
21
26
  );
22
27
  };
23
28
 
29
+ const baseFooterProps = {
30
+ shouldShowDoneFooter: () => true,
31
+ shouldShowContinueFooter: () => false,
32
+ shouldShowFooter: () => true,
33
+ shouldShowHeader: () => true,
34
+ shouldShowTemplateName: () => true,
35
+ onSave: jest.fn(),
36
+ isFullMode: true,
37
+ messages: {},
38
+ slidBoxContent: 'editTemplate',
39
+ templateStep: 'modeSelection',
40
+ isTemplateNameEmpty: false,
41
+ isCreatingTemplate: false,
42
+ };
43
+
24
44
  describe("test for empty email empty template name", () => {
25
45
  it("check the error message and disabled button", async () => {
26
- const shouldShowDoneFooter = jest.fn();
27
- shouldShowDoneFooter.mockReturnValue(true);
46
+ const { hasSupportCKEditor } = require('../../../utils/common');
47
+ hasSupportCKEditor.mockReturnValue(true);
48
+ const shouldShowDoneFooter = jest.fn().mockReturnValue(true);
28
49
  const shouldShowContinueFooter = jest.fn();
29
- const shouldShowFooter = jest.fn();
30
- const shouldShowHeader = jest.fn();
31
- const shouldShowTemplateName = jest.fn();
32
- const onSave = jest.fn();
33
- const props={
34
- shouldShowDoneFooter,
35
- shouldShowContinueFooter,
36
- shouldShowFooter,
37
- shouldShowHeader,
38
- shouldShowTemplateName,
39
- onSave,
40
- isFullMode: true,
41
- messages: {
42
- },
43
- slidBoxContent: "editTemplate",
44
- currentChannel: "EMAIL",
45
- templateStep: "modeSelection",
46
- isTemplateNameEmpty: true,
47
- htmlEditorValidationState: {
48
- isContentEmpty: false,
49
- issueCounts: { html: 0, label: 0, liquid: 0, total: 0 },
50
- },
51
- isCreatingTemplate: false,
52
- }
53
- renderComponent(props);
54
- const errorMessage = await screen.findByText(/template name cannot be empty/i);
55
- expect(errorMessage).toBeInTheDocument();
56
- const updateBtn = screen.getByRole('button',{name:/update/i});
57
- expect(updateBtn).toBeDisabled();
58
- renderComponent({
59
- ...props,
60
- isTemplateNameEmpty: false,
61
- htmlEditorValidationState: {
62
- isContentEmpty: false,
63
- issueCounts: { html: 0, label: 0, liquid: 0, total: 0 },
64
- },
65
- })
66
- const updateBtns = screen.getAllByRole('button',{name:/update/i});
67
- expect(updateBtns[1]).toBeEnabled();
50
+ const props = {
51
+ ...baseFooterProps,
52
+ shouldShowDoneFooter,
53
+ shouldShowContinueFooter,
54
+ currentChannel: "EMAIL",
55
+ isTemplateNameEmpty: true,
56
+ htmlEditorValidationState: {
57
+ isContentEmpty: false,
58
+ issueCounts: { html: 0, label: 0, liquid: 0, total: 0 },
59
+ },
60
+ };
61
+ renderComponent(props);
62
+ const errorMessage = await screen.findByText(/template name cannot be empty/i);
63
+ expect(errorMessage).toBeInTheDocument();
64
+ const updateBtn = screen.getByRole('button', { name: /update/i });
65
+ expect(updateBtn).toBeDisabled();
66
+ renderComponent({
67
+ ...props,
68
+ isTemplateNameEmpty: false,
69
+ htmlEditorValidationState: {
70
+ isContentEmpty: false,
71
+ issueCounts: { html: 0, label: 0, liquid: 0, total: 0 },
72
+ },
73
+ });
74
+ const updateBtns = screen.getAllByRole('button', { name: /update/i });
75
+ expect(updateBtns[1]).toBeEnabled();
76
+ });
77
+ });
78
+
79
+ describe('shouldCheckValidation (line 79)', () => {
80
+ beforeEach(() => {
81
+ jest.clearAllMocks();
82
+ });
83
+
84
+ it('disables Save when shouldCheckValidation is true and validation has errors not acknowledged (HTML Editor, EMAIL, no CK)', () => {
85
+ const { hasSupportCKEditor } = require('../../../utils/common');
86
+ hasSupportCKEditor.mockReturnValue(false);
87
+ renderComponent({
88
+ ...baseFooterProps,
89
+ currentChannel: 'EMAIL',
90
+ slidBoxContent: 'editTemplate',
91
+ htmlEditorValidationState: {
92
+ validationComplete: true,
93
+ hasErrors: true,
94
+ errorsAcknowledged: false,
95
+ isContentEmpty: false,
96
+ issueCounts: { html: 1, label: 0, liquid: 0, total: 1 },
97
+ },
98
+ });
99
+ const updateBtn = screen.getByRole('button', { name: /update/i });
100
+ expect(updateBtn).toBeDisabled();
101
+ });
102
+
103
+ it('enables Save when shouldCheckValidation is true and validation complete with no blocking errors', () => {
104
+ const { hasSupportCKEditor } = require('../../../utils/common');
105
+ hasSupportCKEditor.mockReturnValue(false);
106
+ renderComponent({
107
+ ...baseFooterProps,
108
+ currentChannel: 'EMAIL',
109
+ slidBoxContent: 'editTemplate',
110
+ htmlEditorValidationState: {
111
+ validationComplete: true,
112
+ hasErrors: false,
113
+ errorsAcknowledged: false,
114
+ isContentEmpty: false,
115
+ issueCounts: { html: 0, label: 0, liquid: 0, total: 0 },
116
+ },
117
+ });
118
+ const updateBtn = screen.getByRole('button', { name: /update/i });
119
+ expect(updateBtn).toBeEnabled();
120
+ });
121
+
122
+ it('does not disable Save based on validation when currentChannel is not EMAIL (shouldCheckValidation false)', () => {
123
+ const { hasSupportCKEditor } = require('../../../utils/common');
124
+ hasSupportCKEditor.mockReturnValue(false);
125
+ renderComponent({
126
+ ...baseFooterProps,
127
+ currentChannel: 'SMS',
128
+ slidBoxContent: 'editTemplate',
129
+ htmlEditorValidationState: {
130
+ validationComplete: true,
131
+ hasErrors: true,
132
+ errorsAcknowledged: false,
133
+ isContentEmpty: false,
134
+ issueCounts: { html: 1, label: 0, liquid: 0, total: 1 },
135
+ },
136
+ });
137
+ const updateBtn = screen.getByRole('button', { name: /update/i });
138
+ expect(updateBtn).toBeEnabled();
139
+ });
140
+
141
+ it('does not disable Save based on validation when htmlEditorValidationState is not provided (shouldCheckValidation false)', () => {
142
+ const { hasSupportCKEditor } = require('../../../utils/common');
143
+ hasSupportCKEditor.mockReturnValue(false);
144
+ renderComponent({
145
+ ...baseFooterProps,
146
+ currentChannel: 'EMAIL',
147
+ slidBoxContent: 'editTemplate',
148
+ htmlEditorValidationState: null,
149
+ });
150
+ const updateBtn = screen.getByRole('button', { name: /update/i });
151
+ expect(updateBtn).toBeEnabled();
152
+ });
153
+
154
+ it('does not disable Save based on validation when hasSupportCKEditor is true (shouldCheckValidation false)', () => {
155
+ const { hasSupportCKEditor } = require('../../../utils/common');
156
+ hasSupportCKEditor.mockReturnValue(true);
157
+ renderComponent({
158
+ ...baseFooterProps,
159
+ currentChannel: 'EMAIL',
160
+ slidBoxContent: 'editTemplate',
161
+ htmlEditorValidationState: {
162
+ validationComplete: true,
163
+ hasErrors: true,
164
+ errorsAcknowledged: false,
165
+ isContentEmpty: false,
166
+ issueCounts: { html: 1, label: 0, liquid: 0, total: 1 },
167
+ },
168
+ });
169
+ const updateBtn = screen.getByRole('button', { name: /update/i });
170
+ expect(updateBtn).toBeEnabled();
171
+ });
172
+
173
+ it('disables Save when shouldCheckValidation true and content empty in create mode', () => {
174
+ const { hasSupportCKEditor } = require('../../../utils/common');
175
+ hasSupportCKEditor.mockReturnValue(false);
176
+ renderComponent({
177
+ ...baseFooterProps,
178
+ currentChannel: 'EMAIL',
179
+ slidBoxContent: 'createTemplate',
180
+ emailCreateMode: 'html_editor',
181
+ selectedEmailCreateMode: 'html_editor',
182
+ htmlEditorValidationState: {
183
+ validationComplete: true,
184
+ hasErrors: false,
185
+ errorsAcknowledged: false,
186
+ isContentEmpty: true,
187
+ issueCounts: { html: 0, label: 0, liquid: 0, total: 0 },
188
+ },
68
189
  });
190
+ const saveBtn = screen.getByRole('button', { name: /create/i });
191
+ expect(saveBtn).toBeDisabled();
192
+ });
69
193
  });
@@ -10,6 +10,7 @@ exports[`Test SlideBoxContent container Email component isTestAndPreviewMode IIF
10
10
  getCmsTemplatesInProgress={false}
11
11
  handleCloseTestAndPreview={[MockFunction]}
12
12
  handleTestAndPreview={[MockFunction]}
13
+ isEditEmail={true}
13
14
  isLoyaltyModule={false}
14
15
  isTestAndPreviewMode={true}
15
16
  key="cretives-container-email-edit-wrapper"
@@ -60,6 +61,7 @@ exports[`Test SlideBoxContent container Email component isTestAndPreviewMode IIF
60
61
  getCmsTemplatesInProgress={false}
61
62
  handleCloseTestAndPreview={[MockFunction]}
62
63
  handleTestAndPreview={[MockFunction]}
64
+ isEditEmail={true}
63
65
  isLoyaltyModule={false}
64
66
  isTestAndPreviewMode={false}
65
67
  key="cretives-container-email-edit-wrapper"
@@ -110,6 +112,7 @@ exports[`Test SlideBoxContent container Email component isTestAndPreviewMode IIF
110
112
  getCmsTemplatesInProgress={false}
111
113
  handleCloseTestAndPreview={[MockFunction]}
112
114
  handleTestAndPreview={[MockFunction]}
115
+ isEditEmail={true}
113
116
  isLoyaltyModule={false}
114
117
  isTestAndPreviewMode={true}
115
118
  key="cretives-container-email-edit-wrapper"
@@ -159,6 +162,7 @@ exports[`Test SlideBoxContent container Email component isTestAndPreviewMode IIF
159
162
  getCmsTemplatesInProgress={false}
160
163
  handleCloseTestAndPreview={[MockFunction]}
161
164
  handleTestAndPreview={[MockFunction]}
165
+ isEditEmail={true}
162
166
  isLoyaltyModule={false}
163
167
  isTestAndPreviewMode={false}
164
168
  key="cretives-container-email-edit-wrapper"
@@ -1277,6 +1281,7 @@ exports[`Test SlideBoxContent container Should render correctly with isTestAndPr
1277
1281
  <Connect(UserIsAuthenticated(Component))
1278
1282
  date={0}
1279
1283
  getCmsTemplatesInProgress={false}
1284
+ isEditEmail={false}
1280
1285
  isTestAndPreviewMode={false}
1281
1286
  key="creatives-email-wrapper"
1282
1287
  templateData={
@@ -1352,6 +1357,7 @@ exports[`Test SlideBoxContent container Should render correctly with isTestAndPr
1352
1357
  <Connect(UserIsAuthenticated(Component))
1353
1358
  date={0}
1354
1359
  getCmsTemplatesInProgress={false}
1360
+ isEditEmail={false}
1355
1361
  isTestAndPreviewMode={true}
1356
1362
  key="creatives-email-wrapper"
1357
1363
  templateData={