@capillarytech/creatives-library 8.0.272 → 8.0.273
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/utils/imageUrlUpload.js +13 -14
- package/utils/tests/imageUrlUpload.test.js +298 -0
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +143 -1
- package/v2Containers/MobilePush/Create/index.js +1 -1
- package/v2Containers/MobilePush/Edit/index.js +10 -6
package/package.json
CHANGED
package/utils/imageUrlUpload.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Utility functions for uploading images from URLs
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* NOTE: CORS-limited; will be replaced with backend implementation.
|
|
5
5
|
* Flow currently hidden (not removed) from frontend.
|
|
6
6
|
*/
|
|
@@ -13,14 +13,14 @@ import {
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Fetches an image from a URL
|
|
16
|
-
*
|
|
16
|
+
*
|
|
17
17
|
* @param {string} url - The image URL to fetch
|
|
18
18
|
* @returns {Promise<Response>} - The fetch response object
|
|
19
19
|
* @throws {Error} - If the fetch fails (network/CORS error)
|
|
20
20
|
*/
|
|
21
21
|
export const fetchImageFromUrl = async (url) => {
|
|
22
22
|
const trimmedUrl = url?.trim() || '';
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
if (!trimmedUrl) {
|
|
25
25
|
throw new Error('URL is required');
|
|
26
26
|
}
|
|
@@ -42,7 +42,7 @@ export const fetchImageFromUrl = async (url) => {
|
|
|
42
42
|
/**
|
|
43
43
|
* Helper function to upload image from URL
|
|
44
44
|
* Fetches image, validates content type and size, converts to File, and uploads via uploadAsset
|
|
45
|
-
*
|
|
45
|
+
*
|
|
46
46
|
* @param {string} url - The image URL to upload
|
|
47
47
|
* @param {Function} formatMessage - React Intl formatMessage function
|
|
48
48
|
* @param {Object} messages - React Intl messages object
|
|
@@ -51,7 +51,7 @@ export const fetchImageFromUrl = async (url) => {
|
|
|
51
51
|
* @param {number} maxSize - Maximum file size in bytes
|
|
52
52
|
* @param {string[]} allowedContentTypes - Array of allowed MIME types (defaults to DEFAULT_ALLOWED_CONTENT_TYPES)
|
|
53
53
|
* @returns {Promise<{success: boolean, error: string}>} - Result object with success status and error message
|
|
54
|
-
*
|
|
54
|
+
*
|
|
55
55
|
* @example
|
|
56
56
|
* const result = await uploadImageFromUrlHelper(
|
|
57
57
|
* 'https://example.com/image.jpg',
|
|
@@ -73,14 +73,14 @@ export const uploadImageFromUrlHelper = async (
|
|
|
73
73
|
allowedContentTypes = DEFAULT_ALLOWED_CONTENT_TYPES,
|
|
74
74
|
) => {
|
|
75
75
|
const trimmedUrl = url?.trim() || '';
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
try {
|
|
78
78
|
const response = await fetchImageFromUrl(trimmedUrl);
|
|
79
79
|
|
|
80
80
|
// Validate Content-Type
|
|
81
81
|
const contentType = response.headers?.get('Content-Type') || '';
|
|
82
82
|
const normalizedContentType = contentType.split(';')[0].toLowerCase().trim();
|
|
83
|
-
|
|
83
|
+
|
|
84
84
|
if (!allowedContentTypes.includes(normalizedContentType)) {
|
|
85
85
|
return {
|
|
86
86
|
success: false,
|
|
@@ -89,7 +89,7 @@ export const uploadImageFromUrlHelper = async (
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
const blob = await response.blob();
|
|
92
|
-
|
|
92
|
+
|
|
93
93
|
if (blob.size > maxSize) {
|
|
94
94
|
return {
|
|
95
95
|
success: false,
|
|
@@ -101,7 +101,7 @@ export const uploadImageFromUrlHelper = async (
|
|
|
101
101
|
return new Promise((resolve) => {
|
|
102
102
|
const img = new Image();
|
|
103
103
|
const objectUrl = URL.createObjectURL(blob);
|
|
104
|
-
|
|
104
|
+
|
|
105
105
|
img.onload = () => {
|
|
106
106
|
const extension = MIME_TYPE_TO_EXTENSION[normalizedContentType] || DEFAULT_IMAGE_EXTENSION;
|
|
107
107
|
const fileName = `${fileNamePrefix}.${extension}`;
|
|
@@ -111,16 +111,16 @@ export const uploadImageFromUrlHelper = async (
|
|
|
111
111
|
height: img.height,
|
|
112
112
|
error: false,
|
|
113
113
|
};
|
|
114
|
-
|
|
114
|
+
|
|
115
115
|
uploadAssetFn(file, 'image', fileParams);
|
|
116
116
|
URL.revokeObjectURL(objectUrl);
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
resolve({
|
|
119
119
|
success: true,
|
|
120
120
|
error: '',
|
|
121
121
|
});
|
|
122
122
|
};
|
|
123
|
-
|
|
123
|
+
|
|
124
124
|
img.onerror = () => {
|
|
125
125
|
URL.revokeObjectURL(objectUrl);
|
|
126
126
|
resolve({
|
|
@@ -128,7 +128,7 @@ export const uploadImageFromUrlHelper = async (
|
|
|
128
128
|
error: formatMessage(messages.imageLoadError),
|
|
129
129
|
});
|
|
130
130
|
};
|
|
131
|
-
|
|
131
|
+
|
|
132
132
|
img.src = objectUrl;
|
|
133
133
|
});
|
|
134
134
|
} catch (error) {
|
|
@@ -138,4 +138,3 @@ export const uploadImageFromUrlHelper = async (
|
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
140
|
};
|
|
141
|
-
|
|
@@ -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
|
+
});
|
|
@@ -1897,6 +1897,117 @@ describe('EmailHTMLEditor', () => {
|
|
|
1897
1897
|
});
|
|
1898
1898
|
});
|
|
1899
1899
|
|
|
1900
|
+
describe('Template content extraction (lines 291-309)', () => {
|
|
1901
|
+
it('extracts content and subject from templateDataProp.base branch', async () => {
|
|
1902
|
+
const ref = React.createRef();
|
|
1903
|
+
const templateData = {
|
|
1904
|
+
base: {
|
|
1905
|
+
'template-content': '<p>Base branch content</p>',
|
|
1906
|
+
"subject": 'Base branch subject',
|
|
1907
|
+
},
|
|
1908
|
+
};
|
|
1909
|
+
render(
|
|
1910
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1911
|
+
<EmailHTMLEditor
|
|
1912
|
+
{...defaultProps}
|
|
1913
|
+
ref={ref}
|
|
1914
|
+
params={{ id: '123' }}
|
|
1915
|
+
templateData={templateData}
|
|
1916
|
+
Email={{
|
|
1917
|
+
templateDetails: { _id: '123' },
|
|
1918
|
+
getTemplateDetailsInProgress: false,
|
|
1919
|
+
fetchingCmsData: false,
|
|
1920
|
+
}}
|
|
1921
|
+
/>
|
|
1922
|
+
</IntlProvider>
|
|
1923
|
+
);
|
|
1924
|
+
await waitFor(() => {
|
|
1925
|
+
expect(ref.current).toBeTruthy();
|
|
1926
|
+
}, { timeout: 3000 });
|
|
1927
|
+
await waitFor(() => {
|
|
1928
|
+
const content = ref.current.getContentForPreview();
|
|
1929
|
+
const formData = ref.current.getFormDataForPreview();
|
|
1930
|
+
expect(content).toBe('<p>Base branch content</p>');
|
|
1931
|
+
expect(formData['template-subject']).toBe('Base branch subject');
|
|
1932
|
+
}, { timeout: 3000 });
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
it('extracts content and subject from templateDataProp.versions.base branch with activeTab', async () => {
|
|
1936
|
+
const ref = React.createRef();
|
|
1937
|
+
const templateData = {
|
|
1938
|
+
versions: {
|
|
1939
|
+
base: {
|
|
1940
|
+
activeTab: 'en',
|
|
1941
|
+
en: {
|
|
1942
|
+
'template-content': '<p>Versions base en content</p>',
|
|
1943
|
+
"html_content": '<p>fallback html_content</p>',
|
|
1944
|
+
},
|
|
1945
|
+
subject: 'Versions base subject',
|
|
1946
|
+
emailSubject: 'Fallback email subject',
|
|
1947
|
+
},
|
|
1948
|
+
},
|
|
1949
|
+
};
|
|
1950
|
+
render(
|
|
1951
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1952
|
+
<EmailHTMLEditor
|
|
1953
|
+
{...defaultProps}
|
|
1954
|
+
ref={ref}
|
|
1955
|
+
params={{ id: '123' }}
|
|
1956
|
+
templateData={templateData}
|
|
1957
|
+
Email={{
|
|
1958
|
+
templateDetails: { _id: '123' },
|
|
1959
|
+
getTemplateDetailsInProgress: false,
|
|
1960
|
+
fetchingCmsData: false,
|
|
1961
|
+
}}
|
|
1962
|
+
/>
|
|
1963
|
+
</IntlProvider>
|
|
1964
|
+
);
|
|
1965
|
+
await waitFor(() => {
|
|
1966
|
+
expect(ref.current).toBeTruthy();
|
|
1967
|
+
}, { timeout: 3000 });
|
|
1968
|
+
await waitFor(() => {
|
|
1969
|
+
const content = ref.current.getContentForPreview();
|
|
1970
|
+
const formData = ref.current.getFormDataForPreview();
|
|
1971
|
+
expect(content).toBe('<p>Versions base en content</p>');
|
|
1972
|
+
expect(formData['template-subject']).toBe('Versions base subject');
|
|
1973
|
+
}, { timeout: 3000 });
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
it('extracts content and subject from flat templateDataProp (else branch)', async () => {
|
|
1977
|
+
const ref = React.createRef();
|
|
1978
|
+
const templateData = {
|
|
1979
|
+
'template-content': '<p>Flat content</p>',
|
|
1980
|
+
"emailSubject": 'Flat email subject',
|
|
1981
|
+
"html_content": '<p>flat html_content</p>',
|
|
1982
|
+
"subject": 'Flat subject',
|
|
1983
|
+
};
|
|
1984
|
+
render(
|
|
1985
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1986
|
+
<EmailHTMLEditor
|
|
1987
|
+
{...defaultProps}
|
|
1988
|
+
ref={ref}
|
|
1989
|
+
params={{ id: '123' }}
|
|
1990
|
+
templateData={templateData}
|
|
1991
|
+
Email={{
|
|
1992
|
+
templateDetails: { _id: '123' },
|
|
1993
|
+
getTemplateDetailsInProgress: false,
|
|
1994
|
+
fetchingCmsData: false,
|
|
1995
|
+
}}
|
|
1996
|
+
/>
|
|
1997
|
+
</IntlProvider>
|
|
1998
|
+
);
|
|
1999
|
+
await waitFor(() => {
|
|
2000
|
+
expect(ref.current).toBeTruthy();
|
|
2001
|
+
}, { timeout: 3000 });
|
|
2002
|
+
await waitFor(() => {
|
|
2003
|
+
const content = ref.current.getContentForPreview();
|
|
2004
|
+
const formData = ref.current.getFormDataForPreview();
|
|
2005
|
+
expect(content).toBe('<p>Flat content</p>');
|
|
2006
|
+
expect(formData['template-subject']).toBe('Flat email subject');
|
|
2007
|
+
}, { timeout: 3000 });
|
|
2008
|
+
});
|
|
2009
|
+
});
|
|
2010
|
+
|
|
1900
2011
|
describe('setIsLoadingContent callback', () => {
|
|
1901
2012
|
it('should call setIsLoadingContent when uploaded content is available', async () => {
|
|
1902
2013
|
const setIsLoadingContent = jest.fn();
|
|
@@ -1933,6 +2044,37 @@ describe('EmailHTMLEditor', () => {
|
|
|
1933
2044
|
});
|
|
1934
2045
|
});
|
|
1935
2046
|
|
|
2047
|
+
describe('location.query and tagList (lines 127, 129, 185)', () => {
|
|
2048
|
+
it('should handle missing location.query (destructure from empty object)', () => {
|
|
2049
|
+
renderWithIntl({
|
|
2050
|
+
location: {},
|
|
2051
|
+
supportedTags: [],
|
|
2052
|
+
metaEntities: { tags: { standard: [{ name: 'standard.tag' }] } },
|
|
2053
|
+
});
|
|
2054
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
2055
|
+
});
|
|
2056
|
+
|
|
2057
|
+
it('should handle null location (query defaults to {})', () => {
|
|
2058
|
+
renderWithIntl({
|
|
2059
|
+
location: null,
|
|
2060
|
+
supportedTags: [],
|
|
2061
|
+
metaEntities: { tags: { standard: [] } },
|
|
2062
|
+
});
|
|
2063
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
it('should use tagList from supportedTags when type=embedded, module=library and no getDefaultTags (line 129)', () => {
|
|
2067
|
+
const supportedTags = [{ name: 'custom.a' }, { name: 'custom.b' }];
|
|
2068
|
+
renderWithIntl({
|
|
2069
|
+
location: { query: { type: 'embedded', module: 'library' } },
|
|
2070
|
+
getDefaultTags: null,
|
|
2071
|
+
supportedTags,
|
|
2072
|
+
metaEntities: { tags: { standard: [{ name: 'standard.only' }] } },
|
|
2073
|
+
});
|
|
2074
|
+
expect(screen.getByTestId('html-editor')).toBeInTheDocument();
|
|
2075
|
+
});
|
|
2076
|
+
});
|
|
2077
|
+
|
|
1936
2078
|
describe('tags useMemo (lines 125-132)', () => {
|
|
1937
2079
|
it('should use supportedTags when in EMBEDDED mode with LIBRARY module and no getDefaultTags', () => {
|
|
1938
2080
|
const supportedTags = [{ name: 'custom.tag1' }, { name: 'custom.tag2' }];
|
|
@@ -2050,7 +2192,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
2050
2192
|
expect(formData).toBeDefined();
|
|
2051
2193
|
expect(formData['0']).toBeDefined();
|
|
2052
2194
|
expect(formData['0'].fr).toBeDefined(); // Uses base_language 'fr'
|
|
2053
|
-
expect(formData['0'].fr
|
|
2195
|
+
expect(formData['0'].fr.is_drag_drop).toBe(false);
|
|
2054
2196
|
expect(formData['0'].activeTab).toBe('fr');
|
|
2055
2197
|
expect(formData['0'].selectedLanguages).toEqual(['fr']);
|
|
2056
2198
|
expect(formData['0'].base).toBe(true);
|
|
@@ -114,7 +114,7 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
114
114
|
if (nextProps.iosCtasData && isEmpty(this.state.templateCta)) {
|
|
115
115
|
this.setState({showIosCtaTable: true});
|
|
116
116
|
}
|
|
117
|
-
if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && isEmpty(this.state.schema)) {
|
|
117
|
+
if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && isEmpty(this.state.schema) && isEmpty(this.state.formData)) {
|
|
118
118
|
const schema = this.props.params.mode === "text" ? nextProps.metaEntities.layouts[0].definition.textSchema : nextProps.metaEntities.layouts[0].definition.imageSchema;
|
|
119
119
|
const isAndroidSupported = get(this, "props.Templates.selectedWeChatAccount.configs.android") === '1';
|
|
120
120
|
const isIosSupported = get(this, "props.Templates.selectedWeChatAccount.configs.ios") === '1';
|
|
@@ -167,8 +167,8 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
167
167
|
};
|
|
168
168
|
this.props.actions.getMobilepushTemplatesList('mobilepush', params);
|
|
169
169
|
}
|
|
170
|
-
if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && _.isEmpty(this.state.fullSchema)) {
|
|
171
|
-
this.setState({fullSchema: nextProps.metaEntities.layouts[0].definition, schema: (nextProps.location.query.module === 'loyalty') ? nextProps.metaEntities.layouts[0].definition.textSchema : {}}, () => {
|
|
170
|
+
if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && _.isEmpty(this.state.fullSchema) && _.isEmpty(this.state.formData)) {
|
|
171
|
+
this.setState({ fullSchema: nextProps.metaEntities.layouts[0].definition, schema: (nextProps.location.query.module === 'loyalty') ? nextProps.metaEntities.layouts[0].definition.textSchema : {} }, () => {
|
|
172
172
|
this.handleEditSchemaOnPropsChange(nextProps, selectedWeChatAccount);
|
|
173
173
|
const templateId = get(this, "props.params.id");
|
|
174
174
|
if (nextProps.location.query.module !== 'loyalty' && templateId && templateId !== 'temp') {
|
|
@@ -388,11 +388,11 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
388
388
|
}
|
|
389
389
|
});
|
|
390
390
|
} else {
|
|
391
|
-
selectedAccount = accounts[0];
|
|
391
|
+
selectedAccount = accounts?.[0];
|
|
392
392
|
formData['mobilepush-accounts'] = selectedAccount?.id;
|
|
393
393
|
}
|
|
394
394
|
this.props.actions.setWeChatAccount(selectedAccount);
|
|
395
|
-
this.setState({formData, accountsOptions: accounts
|
|
395
|
+
this.setState({ formData, accountsOptions: accounts?.map((acc) => ({ key: acc.id, label: acc.name, value: acc.id })) });
|
|
396
396
|
};
|
|
397
397
|
|
|
398
398
|
createDefinition = (account) => ({
|
|
@@ -2018,12 +2018,16 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2018
2018
|
if (this.props.supportedTags) {
|
|
2019
2019
|
tags = this.props.supportedTags;
|
|
2020
2020
|
}
|
|
2021
|
+
if (this.props.supportedTags) {
|
|
2022
|
+
tags = this.props.supportedTags;
|
|
2023
|
+
}
|
|
2021
2024
|
return (
|
|
2022
2025
|
<div className="creatives-mobilepush-edit mobilepush-wrapper">
|
|
2023
2026
|
<CapSpin spinning={spinning}>
|
|
2024
2027
|
<CapRow>
|
|
2025
2028
|
<CapColumn>
|
|
2026
|
-
|
|
2029
|
+
<FormBuilder
|
|
2030
|
+
key={!_.isEmpty(schema) ? 'has-schema' : 'no-schema'}
|
|
2027
2031
|
channel={MOBILE_PUSH}
|
|
2028
2032
|
schema={schema}
|
|
2029
2033
|
showLiquidErrorInFooter={this.props.showLiquidErrorInFooter}
|
|
@@ -2058,7 +2062,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2058
2062
|
hideTestAndPreviewBtn={this.props.hideTestAndPreviewBtn}
|
|
2059
2063
|
isFullMode={this.props.isFullMode}
|
|
2060
2064
|
eventContextTags={this.props?.eventContextTags}
|
|
2061
|
-
/>
|
|
2065
|
+
/>
|
|
2062
2066
|
</CapColumn>
|
|
2063
2067
|
{this.props.iosCtasData && this.state.showIosCtaTable &&
|
|
2064
2068
|
<CapSlideBox
|