@capillarytech/creatives-library 8.0.271 → 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/assets/Android.png +0 -0
- package/assets/iOS.png +0 -0
- package/constants/unified.js +2 -1
- package/initialReducer.js +2 -0
- package/package.json +1 -1
- package/services/api.js +10 -0
- package/services/tests/api.test.js +34 -0
- package/tests/integration/TemplateCreation/TemplateCreation.integration.test.js +17 -35
- package/tests/integration/TemplateCreation/api-response.js +31 -1
- package/tests/integration/TemplateCreation/msw-handler.js +2 -0
- package/utils/common.js +5 -0
- package/utils/commonUtils.js +28 -5
- package/utils/imageUrlUpload.js +13 -14
- package/utils/tests/commonUtil.test.js +224 -0
- package/utils/tests/imageUrlUpload.test.js +298 -0
- package/utils/transformTemplateConfig.js +0 -10
- package/v2Components/CapDeviceContent/index.js +61 -56
- package/v2Components/CapTagList/index.js +6 -1
- package/v2Components/CapTagListWithInput/index.js +5 -1
- package/v2Components/CapTagListWithInput/messages.js +1 -1
- package/v2Components/CapWhatsappCTA/tests/index.test.js +5 -0
- package/v2Components/ErrorInfoNote/constants.js +1 -0
- package/v2Components/ErrorInfoNote/index.js +402 -72
- package/v2Components/ErrorInfoNote/messages.js +32 -6
- package/v2Components/ErrorInfoNote/style.scss +278 -6
- package/v2Components/FormBuilder/tests/index.test.js +13 -4
- package/v2Components/HtmlEditor/HTMLEditor.js +418 -99
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +870 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1882 -133
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +27 -16
- package/v2Components/HtmlEditor/_htmlEditor.scss +108 -45
- package/v2Components/HtmlEditor/_index.lazy.scss +0 -1
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +23 -102
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +148 -140
- package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +2 -1
- package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
- package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +9 -1
- package/v2Components/HtmlEditor/components/EditorToolbar/index.js +31 -6
- package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +22 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +4 -7
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +35 -45
- package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +1 -3
- package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
- package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +7 -6
- package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +7 -10
- package/v2Components/HtmlEditor/components/PreviewPane/index.js +22 -43
- package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +18 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +36 -31
- package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +46 -34
- package/v2Components/HtmlEditor/components/ValidationPanel/constants.js +6 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/index.js +52 -46
- package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +277 -0
- package/v2Components/HtmlEditor/components/ValidationTabs/index.js +295 -0
- package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +51 -0
- package/v2Components/HtmlEditor/constants.js +45 -20
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +373 -16
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +351 -16
- package/v2Components/HtmlEditor/hooks/useEditorContent.js +5 -2
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +88 -146
- package/v2Components/HtmlEditor/hooks/useValidation.js +213 -56
- package/v2Components/HtmlEditor/index.js +1 -1
- package/v2Components/HtmlEditor/messages.js +102 -94
- package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +214 -45
- package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +134 -0
- package/v2Components/HtmlEditor/utils/contentSanitizer.js +40 -41
- package/v2Components/HtmlEditor/utils/htmlValidator.js +71 -72
- package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +158 -124
- package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +23 -25
- package/v2Components/HtmlEditor/utils/validationAdapter.js +66 -41
- package/v2Components/HtmlEditor/utils/validationConstants.js +38 -0
- package/v2Components/MobilePushPreviewV2/constants.js +6 -0
- package/v2Components/MobilePushPreviewV2/index.js +33 -7
- package/v2Components/TemplatePreview/_templatePreview.scss +55 -24
- package/v2Components/TemplatePreview/index.js +47 -32
- package/v2Components/TemplatePreview/messages.js +4 -0
- package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +1 -0
- package/v2Containers/BeeEditor/index.js +172 -90
- package/v2Containers/BeePopupEditor/_beePopupEditor.scss +14 -0
- package/v2Containers/BeePopupEditor/constants.js +10 -0
- package/v2Containers/BeePopupEditor/index.js +194 -0
- package/v2Containers/BeePopupEditor/tests/index.test.js +627 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +127 -51
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +156 -13
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +2 -1
- package/v2Containers/CreativesContainer/constants.js +1 -0
- package/v2Containers/CreativesContainer/index.js +251 -47
- package/v2Containers/CreativesContainer/messages.js +8 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +11 -2
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +38 -50
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +103 -0
- package/v2Containers/Email/actions.js +7 -0
- package/v2Containers/Email/constants.js +5 -1
- package/v2Containers/Email/index.js +234 -29
- package/v2Containers/Email/messages.js +32 -0
- package/v2Containers/Email/reducer.js +12 -1
- package/v2Containers/Email/sagas.js +61 -7
- package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -0
- package/v2Containers/Email/tests/reducer.test.js +46 -0
- package/v2Containers/Email/tests/sagas.test.js +320 -29
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1246 -0
- package/v2Containers/EmailWrapper/components/EmailWrapperView.js +212 -21
- package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +40 -74
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +2614 -0
- package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +520 -0
- package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +2 -67
- package/v2Containers/EmailWrapper/constants.js +2 -0
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +627 -79
- package/v2Containers/EmailWrapper/index.js +103 -23
- package/v2Containers/EmailWrapper/messages.js +65 -1
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +955 -0
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +596 -82
- package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +376 -0
- package/v2Containers/InApp/__tests__/sagas.test.js +363 -0
- package/v2Containers/InApp/actions.js +7 -0
- package/v2Containers/InApp/constants.js +20 -4
- package/v2Containers/InApp/index.js +802 -360
- package/v2Containers/InApp/index.scss +4 -3
- package/v2Containers/InApp/messages.js +7 -3
- package/v2Containers/InApp/reducer.js +21 -3
- package/v2Containers/InApp/sagas.js +29 -9
- package/v2Containers/InApp/selectors.js +25 -5
- package/v2Containers/InApp/tests/index.test.js +154 -50
- package/v2Containers/InApp/tests/reducer.test.js +34 -0
- package/v2Containers/InApp/tests/sagas.test.js +61 -9
- package/v2Containers/InApp/tests/selectors.test.js +612 -0
- package/v2Containers/InAppWrapper/components/InAppWrapperView.js +151 -0
- package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +267 -0
- package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +23 -0
- package/v2Containers/InAppWrapper/constants.js +16 -0
- package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +473 -0
- package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +198 -0
- package/v2Containers/InAppWrapper/index.js +148 -0
- package/v2Containers/InAppWrapper/messages.js +49 -0
- package/v2Containers/InappAdvance/index.js +1099 -0
- package/v2Containers/InappAdvance/index.scss +10 -0
- package/v2Containers/InappAdvance/tests/index.test.js +448 -0
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
- package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +2 -0
- package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +9 -0
- package/v2Containers/MobilePush/Create/index.js +1 -1
- package/v2Containers/MobilePush/Edit/index.js +10 -6
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +12 -0
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4 -0
- package/v2Containers/TagList/index.js +62 -19
- package/v2Containers/Templates/_templates.scss +60 -1
- package/v2Containers/Templates/index.js +89 -4
- package/v2Containers/Templates/messages.js +4 -0
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +4 -2
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +0 -152
- package/v2Containers/EmailWrapper/tests/EmailWrapperView.test.js +0 -214
|
@@ -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
|
+
});
|
|
@@ -101,7 +101,6 @@ export async function transformTemplateConfigWithMediaDetails(templateConfig, ch
|
|
|
101
101
|
const contentType = detectMediaContentType(templateConfig);
|
|
102
102
|
|
|
103
103
|
if (contentType === MEDIA_CONTENT_TYPE.NONE) {
|
|
104
|
-
console.log('No media content detected, returning original config');
|
|
105
104
|
return templateConfig;
|
|
106
105
|
}
|
|
107
106
|
|
|
@@ -109,24 +108,19 @@ export async function transformTemplateConfigWithMediaDetails(templateConfig, ch
|
|
|
109
108
|
let transformDirection;
|
|
110
109
|
if (forceDirection) {
|
|
111
110
|
transformDirection = forceDirection;
|
|
112
|
-
console.log(`Forced transformation direction: ${transformDirection}`);
|
|
113
111
|
} else {
|
|
114
112
|
// Auto-detect based on content
|
|
115
113
|
if (contentType === MEDIA_CONTENT_TYPE.URLS) {
|
|
116
114
|
transformDirection = TRANSFORM_DIRECTION.FORWARD; // URLs → tags
|
|
117
|
-
console.log('Detected URLs, applying forward transformation (URLs → tags)');
|
|
118
115
|
} else if (contentType === MEDIA_CONTENT_TYPE.TAGS) {
|
|
119
116
|
transformDirection = TRANSFORM_DIRECTION.REVERSE; // tags → URLs
|
|
120
|
-
console.log('Detected media_content tags, applying reverse transformation (tags → URLs)');
|
|
121
117
|
} else if (contentType === MEDIA_CONTENT_TYPE.MIXED) {
|
|
122
118
|
transformDirection = TRANSFORM_DIRECTION.SMART; // Handle mixed content intelligently
|
|
123
|
-
console.log('Detected mixed content, applying smart transformation');
|
|
124
119
|
}
|
|
125
120
|
}
|
|
126
121
|
|
|
127
122
|
const newConfig = cloneDeep(templateConfig);
|
|
128
123
|
try {
|
|
129
|
-
console.log(`Fetching media details for ${transformDirection} transformation, ID: ${mediaDetailsId}`);
|
|
130
124
|
|
|
131
125
|
// Fetch media details mapping from API
|
|
132
126
|
const response = await getMediaDetails({ id: mediaDetailsId })
|
|
@@ -145,7 +139,6 @@ export async function transformTemplateConfigWithMediaDetails(templateConfig, ch
|
|
|
145
139
|
idToUrlMapping[mediaId] = url;
|
|
146
140
|
});
|
|
147
141
|
|
|
148
|
-
console.log(`Processing ${Object.keys(urlToIdMapping).length} media mappings`);
|
|
149
142
|
|
|
150
143
|
// Apply transformations based on direction
|
|
151
144
|
newConfig?.cards?.forEach(card => {
|
|
@@ -157,7 +150,6 @@ export async function transformTemplateConfigWithMediaDetails(templateConfig, ch
|
|
|
157
150
|
// Forward transformation: URL → tag
|
|
158
151
|
const mediaDetailId = urlToIdMapping[currentUrl];
|
|
159
152
|
if (mediaDetailId) {
|
|
160
|
-
console.log(`Forward: ${currentUrl} → {{media_content(${mediaDetailId})}}`);
|
|
161
153
|
card.media.url = `{{media_content(${mediaDetailId})}}`;
|
|
162
154
|
} else {
|
|
163
155
|
console.warn(`No media detail ID found for URL: ${currentUrl}`);
|
|
@@ -169,7 +161,6 @@ export async function transformTemplateConfigWithMediaDetails(templateConfig, ch
|
|
|
169
161
|
if (mediaId) {
|
|
170
162
|
const actualUrl = idToUrlMapping[mediaId];
|
|
171
163
|
if (actualUrl) {
|
|
172
|
-
console.log(`Reverse: {{media_content(${mediaId})}} → ${actualUrl}`);
|
|
173
164
|
card.media.url = actualUrl;
|
|
174
165
|
} else {
|
|
175
166
|
console.warn(`No URL found for media ID: ${mediaId}`);
|
|
@@ -185,7 +176,6 @@ export async function transformTemplateConfigWithMediaDetails(templateConfig, ch
|
|
|
185
176
|
console.error('Failed to fetch media details for transformation:', error);
|
|
186
177
|
|
|
187
178
|
// Fallback: try to extract/convert what we can without API
|
|
188
|
-
console.log('Falling back to URL extraction method');
|
|
189
179
|
newConfig.cards.forEach(card => {
|
|
190
180
|
if (card.media && card.media.url) {
|
|
191
181
|
if (transformDirection === TRANSFORM_DIRECTION.FORWARD) {
|
|
@@ -69,7 +69,7 @@ const CapDeviceContent = (props) => {
|
|
|
69
69
|
templateDescErrorHandler,
|
|
70
70
|
templateTitleError,
|
|
71
71
|
setTemplateTitleError,
|
|
72
|
-
isAiContentBotDisabled
|
|
72
|
+
isAiContentBotDisabled,
|
|
73
73
|
} = props || {};
|
|
74
74
|
const { TextArea } = CapInput;
|
|
75
75
|
const { formatMessage } = intl;
|
|
@@ -167,21 +167,22 @@ const CapDeviceContent = (props) => {
|
|
|
167
167
|
return (
|
|
168
168
|
<>
|
|
169
169
|
<CapRow className="creatives-device-content">
|
|
170
|
-
|
|
170
|
+
<CapLink
|
|
171
171
|
title={isAndroid
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
? formatMessage(messages.copyContentFromIOS)
|
|
173
|
+
: formatMessage(messages.copyCotentFromAndroid)}
|
|
174
174
|
className="inapp-copy-content"
|
|
175
175
|
onClick={onCopyTitleAndContent}
|
|
176
|
-
|
|
176
|
+
/>
|
|
177
177
|
<CapRow className="creatives-inapp-title">
|
|
178
178
|
<CapColumn
|
|
179
|
-
|
|
179
|
+
className="inapp-content-main"
|
|
180
180
|
>
|
|
181
181
|
<CapHeading type="h5" className="inapp-title">
|
|
182
182
|
{formatMessage(messages.title)}
|
|
183
183
|
</CapHeading>
|
|
184
|
-
{getTagList(0)}
|
|
184
|
+
{getTagList(0)}
|
|
185
|
+
{/* here 0 signifies the tags for template title */}
|
|
185
186
|
</CapColumn>
|
|
186
187
|
<CapInput
|
|
187
188
|
id="inapp-title-name-input"
|
|
@@ -213,7 +214,7 @@ const CapDeviceContent = (props) => {
|
|
|
213
214
|
</CapRow>
|
|
214
215
|
<CapRow className={`creatives-inapp-message ${!isMediaTypeImage && "message-bottom-margin"}`}>
|
|
215
216
|
<CapColumn
|
|
216
|
-
|
|
217
|
+
className="inapp-message-header"
|
|
217
218
|
>
|
|
218
219
|
<CapHeading type="h5" className="inapp-message-header-style">
|
|
219
220
|
{formatMessage(messages.message)}
|
|
@@ -222,37 +223,37 @@ const CapDeviceContent = (props) => {
|
|
|
222
223
|
{/* here 1 signifies the tags for template message */}
|
|
223
224
|
</CapColumn>
|
|
224
225
|
<div className="inapp-create-template-message-input-wrapper">
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
/>
|
|
240
|
-
{!isAiContentBotDisabled && (
|
|
241
|
-
<CapAskAira.ContentGenerationBot
|
|
242
|
-
text={templateMessage || ""}
|
|
243
|
-
setText={(text) => {
|
|
244
|
-
onTemplateMessageChange({ target: { value: text } });
|
|
245
|
-
}}
|
|
246
|
-
iconPlacement="float-br"
|
|
247
|
-
iconSize="1.6rem"
|
|
248
|
-
rootStyle={{
|
|
249
|
-
bottom: "0.2rem",
|
|
250
|
-
right: "0.2rem",
|
|
251
|
-
left: "auto",
|
|
252
|
-
position: "absolute",
|
|
253
|
-
}}
|
|
226
|
+
<TextArea
|
|
227
|
+
id="inapp-create-template-message-input"
|
|
228
|
+
className="inapp-create-template-message-input"
|
|
229
|
+
placeholder={formatMessage(messages.textAreaInputPlaceholder)}
|
|
230
|
+
onChange={onTemplateMessageChange}
|
|
231
|
+
value={templateMessage || ""}
|
|
232
|
+
autosize={{ minRows: 5, maxRows: 5 }}
|
|
233
|
+
errorMessage={
|
|
234
|
+
templateMessageError && (
|
|
235
|
+
<CapError className="inapp-template-message-error">
|
|
236
|
+
{templateMessageError}
|
|
237
|
+
</CapError>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
254
240
|
/>
|
|
255
|
-
|
|
241
|
+
{!isAiContentBotDisabled && (
|
|
242
|
+
<CapAskAira.ContentGenerationBot
|
|
243
|
+
text={templateMessage || ""}
|
|
244
|
+
setText={(text) => {
|
|
245
|
+
onTemplateMessageChange({ target: { value: text } });
|
|
246
|
+
}}
|
|
247
|
+
iconPlacement="float-br"
|
|
248
|
+
iconSize="1.6rem"
|
|
249
|
+
rootStyle={{
|
|
250
|
+
bottom: "0.2rem",
|
|
251
|
+
right: "0.2rem",
|
|
252
|
+
left: "auto",
|
|
253
|
+
position: "absolute",
|
|
254
|
+
}}
|
|
255
|
+
/>
|
|
256
|
+
)}
|
|
256
257
|
</div>
|
|
257
258
|
{isMediaTypeImage && (
|
|
258
259
|
<>
|
|
@@ -279,14 +280,16 @@ const CapDeviceContent = (props) => {
|
|
|
279
280
|
</CapRow>
|
|
280
281
|
</CapRow>
|
|
281
282
|
<CapRow className="inapp-action-link">
|
|
282
|
-
<CapCheckbox onChange={onActionLinkCheckBoxChange} checked={addActionLink}/>
|
|
283
|
+
<CapCheckbox onChange={onActionLinkCheckBoxChange} checked={addActionLink} />
|
|
283
284
|
<CapRow className="inapp-render-heading">
|
|
284
285
|
<CapHeader
|
|
285
|
-
title={
|
|
286
|
-
<
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
286
|
+
title={(
|
|
287
|
+
<CapRow type="flex">
|
|
288
|
+
<CapHeading type="h4">
|
|
289
|
+
{formatMessage(messages.addActionLink)}
|
|
290
|
+
</CapHeading>
|
|
291
|
+
</CapRow>
|
|
292
|
+
)}
|
|
290
293
|
description={<CapLabel type="label3">{formatMessage(messages.addActionLinkDesc)}</CapLabel>}
|
|
291
294
|
/>
|
|
292
295
|
{addActionLink && (
|
|
@@ -310,19 +313,21 @@ const CapDeviceContent = (props) => {
|
|
|
310
313
|
<CapRow className="inapp-cta-button">
|
|
311
314
|
<CapHeader
|
|
312
315
|
className="inapp-render-heading-cta-button"
|
|
313
|
-
title={
|
|
314
|
-
<
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
316
|
+
title={(
|
|
317
|
+
<CapRow type="flex">
|
|
318
|
+
<CapHeading type="h4">
|
|
319
|
+
{formatMessage(messages.btnLabel)}
|
|
320
|
+
</CapHeading>
|
|
321
|
+
<CapHeading
|
|
322
|
+
type="h6"
|
|
323
|
+
className="inapp-optional-label"
|
|
324
|
+
>
|
|
325
|
+
{formatMessage(messages.optional)}
|
|
326
|
+
</CapHeading>
|
|
327
|
+
</CapRow>
|
|
328
|
+
)}
|
|
324
329
|
description={<CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>}
|
|
325
|
-
|
|
330
|
+
/>
|
|
326
331
|
<CapRadioGroup
|
|
327
332
|
options={BUTTON_RADIO_OPTIONS}
|
|
328
333
|
value={buttonType}
|
|
@@ -227,6 +227,10 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
|
|
|
227
227
|
|
|
228
228
|
togglePopoverVisibility = (visible) => {
|
|
229
229
|
this.setState({visible});
|
|
230
|
+
// Call onVisibleChange callback if provided (for triggering API calls when popover opens)
|
|
231
|
+
if (this.props.onVisibleChange) {
|
|
232
|
+
this.props.onVisibleChange(visible);
|
|
233
|
+
}
|
|
230
234
|
};
|
|
231
235
|
|
|
232
236
|
renderDynamicTagFlow = () => {
|
|
@@ -468,7 +472,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
|
|
|
468
472
|
onVisibleChange={this.togglePopoverVisibility}
|
|
469
473
|
content={contentSection}
|
|
470
474
|
trigger="click"
|
|
471
|
-
placement={channel === EMAIL.toUpperCase() ? "leftTop" : "rightTop"}
|
|
475
|
+
placement={this.props.popoverPlacement || (channel === EMAIL.toUpperCase() ? "leftTop" : "rightTop")}
|
|
472
476
|
>
|
|
473
477
|
<CapTooltip
|
|
474
478
|
title={
|
|
@@ -535,6 +539,7 @@ CapTagList.propTypes = {
|
|
|
535
539
|
channel: PropTypes.string,
|
|
536
540
|
disabled: PropTypes.bool,
|
|
537
541
|
fetchingSchemaError: PropTypes.bool,
|
|
542
|
+
popoverPlacement: PropTypes.string,
|
|
538
543
|
};
|
|
539
544
|
|
|
540
545
|
CapTagList.defaultValue = {
|
|
@@ -48,13 +48,14 @@ export const CapTagListWithInput = (props) => {
|
|
|
48
48
|
showTagList = true,
|
|
49
49
|
showInput = true,
|
|
50
50
|
inputProps = {},
|
|
51
|
+
popoverPlacement,
|
|
51
52
|
} = props;
|
|
52
53
|
|
|
53
54
|
const { formatMessage } = intl;
|
|
54
55
|
|
|
55
56
|
return (
|
|
56
57
|
<CapColumn style={containerStyle}>
|
|
57
|
-
<CapRow
|
|
58
|
+
<CapRow align="middle" type="flex">
|
|
58
59
|
{showHeading && headingText && (
|
|
59
60
|
<CapHeading type={headingType} style={headingStyle}>
|
|
60
61
|
{headingText}
|
|
@@ -76,6 +77,7 @@ export const CapTagListWithInput = (props) => {
|
|
|
76
77
|
selectedOfferDetails={selectedOfferDetails}
|
|
77
78
|
eventContextTags={eventContextTags}
|
|
78
79
|
style={tagListStyle}
|
|
80
|
+
popoverPlacement={popoverPlacement}
|
|
79
81
|
/>
|
|
80
82
|
)}
|
|
81
83
|
</CapRow>
|
|
@@ -136,6 +138,7 @@ CapTagListWithInput.propTypes = {
|
|
|
136
138
|
showHeading: PropTypes.bool,
|
|
137
139
|
showTagList: PropTypes.bool,
|
|
138
140
|
showInput: PropTypes.bool,
|
|
141
|
+
popoverPlacement: PropTypes.string,
|
|
139
142
|
};
|
|
140
143
|
|
|
141
144
|
CapTagListWithInput.defaultProps = {
|
|
@@ -164,6 +167,7 @@ CapTagListWithInput.defaultProps = {
|
|
|
164
167
|
showTagList: true,
|
|
165
168
|
showInput: true,
|
|
166
169
|
inputProps: {},
|
|
170
|
+
popoverPlacement: undefined,
|
|
167
171
|
};
|
|
168
172
|
|
|
169
173
|
export default injectIntl(CapTagListWithInput);
|
|
@@ -4,6 +4,11 @@ import '@testing-library/jest-dom';
|
|
|
4
4
|
import { render, screen, fireEvent } from '../../../utils/test-utils';
|
|
5
5
|
import { CapWhatsappCTA } from '../index';
|
|
6
6
|
|
|
7
|
+
// Mock the missing reducer
|
|
8
|
+
jest.mock('@capillarytech/cap-ui-library/CapCollapsibleLeftNavigation/reducer', () => {
|
|
9
|
+
return (state = {}) => state;
|
|
10
|
+
}, { virtual: true });
|
|
11
|
+
|
|
7
12
|
const updateHandler = jest.fn();
|
|
8
13
|
const deleteHandler = jest.fn();
|
|
9
14
|
const initializeComponent = (
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const LIQUID_DOC_URL = 'https://docs.capillarytech.com/docs/add-dynamic-content-using-liquid';
|