@capillarytech/creatives-library 8.0.272 → 8.0.274
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/tests/imageUrlUpload.test.js +298 -0
- package/v2Containers/Email/index.js +34 -1
- 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
|
@@ -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
|
+
});
|
|
@@ -26,7 +26,7 @@ import * as globalActions from '../Cap/actions';
|
|
|
26
26
|
import './_email.scss';
|
|
27
27
|
import {getMessageObject} from '../../utils/messageUtils';
|
|
28
28
|
import EmailPreview from '../../v2Components/EmailPreview';
|
|
29
|
-
import { getDecodedFileName ,
|
|
29
|
+
import { getDecodedFileName, hasLiquidSupportFeature, hasSupportCKEditor } from '../../utils/common';
|
|
30
30
|
import Pagination from '../../v2Components/Pagination';
|
|
31
31
|
import * as creativesContainerActions from '../CreativesContainer/actions';
|
|
32
32
|
import withCreatives from '../../hoc/withCreatives';
|
|
@@ -325,6 +325,7 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
|
|
|
325
325
|
}
|
|
326
326
|
if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && _.isEmpty(this.state.schema)) {
|
|
327
327
|
const newSchema = this.injectEvents(nextProps.metaEntities.layouts[0].definition);
|
|
328
|
+
this.applyTabOptionIconVisibility(newSchema);
|
|
328
329
|
|
|
329
330
|
this.setState({schema: newSchema, loadingStatus: this.state.loadingStatus + 1});
|
|
330
331
|
if (this.props.location.query.module !== 'library' || (this.props.location.query.module === 'library' && this.props.getDefaultTags)) {
|
|
@@ -1752,6 +1753,16 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
|
|
|
1752
1753
|
newTabPopoverSection.cols[0].style.display = isDragDrop ? "" : "none";
|
|
1753
1754
|
}
|
|
1754
1755
|
});
|
|
1756
|
+
// Hide tab-option-icon (switch editor trigger) when SUPPORT_CK_EDITOR is disabled
|
|
1757
|
+
if (containerInputFieldCol.value && containerInputFieldCol.value.sections) {
|
|
1758
|
+
_.forEach(containerInputFieldCol.value.sections[0].inputFields, (valueInputField) => {
|
|
1759
|
+
_.forEach(valueInputField.cols, (valueCol) => {
|
|
1760
|
+
if (valueCol.id === 'tab-option-icon') {
|
|
1761
|
+
valueCol.colStyle = { ...valueCol.colStyle, display: hasSupportCKEditor() ? 'flex' : 'none' };
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1755
1766
|
}
|
|
1756
1767
|
});
|
|
1757
1768
|
}
|
|
@@ -1760,6 +1771,28 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
|
|
|
1760
1771
|
this.setState({schema, isSchemaChanged: true});
|
|
1761
1772
|
}
|
|
1762
1773
|
|
|
1774
|
+
/**
|
|
1775
|
+
* Hides the tab-option-icon (switch editor trigger) in schema when SUPPORT_CK_EDITOR is disabled,
|
|
1776
|
+
* so users cannot switch editor if the feature is not enabled.
|
|
1777
|
+
*/
|
|
1778
|
+
applyTabOptionIconVisibility = (schema) => {
|
|
1779
|
+
if (!schema || !schema.containers) return;
|
|
1780
|
+
_.forEach(schema.containers, (container) => {
|
|
1781
|
+
if (!container.isActive || !container.tabBarExtraContent?.sections?.[0]?.inputFields?.[0]?.cols) return;
|
|
1782
|
+
_.forEach(container.tabBarExtraContent.sections[0].inputFields[0].cols, (col) => {
|
|
1783
|
+
if (col.id === 'tab-options-popover' && col.value?.sections?.[0]?.inputFields) {
|
|
1784
|
+
_.forEach(col.value.sections[0].inputFields, (inputField) => {
|
|
1785
|
+
_.forEach(inputField.cols, (c) => {
|
|
1786
|
+
if (c.id === 'tab-option-icon') {
|
|
1787
|
+
c.colStyle = { ...(c.colStyle || {}), display: hasSupportCKEditor() ? 'flex' : 'none' };
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
});
|
|
1794
|
+
};
|
|
1795
|
+
|
|
1763
1796
|
showInsertImageButton = (passedSchema) => {
|
|
1764
1797
|
const schema = passedSchema || _.cloneDeep(this.state.schema);
|
|
1765
1798
|
_.forEach(schema.containers, (container) => {
|
|
@@ -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
|