@capillarytech/creatives-library 8.0.114 → 8.0.116

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 (42) hide show
  1. package/package.json +1 -1
  2. package/utils/commonUtils.js +354 -4
  3. package/utils/tagValidations.js +22 -5
  4. package/utils/tests/commonUtil.test.js +605 -169
  5. package/utils/tests/tagValidations.test.js +129 -3
  6. package/v2Components/ErrorInfoNote/ErrorTypeRenderer.js +125 -0
  7. package/v2Components/ErrorInfoNote/ErrorTypeRenderer.test.js +147 -0
  8. package/v2Components/ErrorInfoNote/index.js +114 -47
  9. package/v2Components/ErrorInfoNote/messages.js +25 -0
  10. package/v2Components/ErrorInfoNote/style.scss +14 -1
  11. package/v2Components/ErrorInfoNote/utils.js +50 -0
  12. package/v2Components/ErrorInfoNote/utils.test.js +189 -0
  13. package/v2Components/FormBuilder/index.js +203 -127
  14. package/v2Components/FormBuilder/messages.js +1 -1
  15. package/v2Containers/Cap/reducer.js +4 -4
  16. package/v2Containers/Cap/sagas.js +9 -3
  17. package/v2Containers/Cap/tests/saga.test.js +12 -0
  18. package/v2Containers/CreativesContainer/SlideBoxContent.js +26 -3
  19. package/v2Containers/CreativesContainer/SlideBoxFooter.js +3 -1
  20. package/v2Containers/CreativesContainer/constants.js +4 -1
  21. package/v2Containers/CreativesContainer/index.js +46 -19
  22. package/v2Containers/CreativesContainer/messages.js +4 -0
  23. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +21 -3
  24. package/v2Containers/CreativesContainer/tests/index.test.js +1 -0
  25. package/v2Containers/Ebill/index.js +3 -3
  26. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +1 -1
  27. package/v2Containers/InApp/index.js +126 -50
  28. package/v2Containers/InApp/tests/index.test.js +1 -1
  29. package/v2Containers/InApp/tests/sagas.test.js +1 -1
  30. package/v2Containers/InApp/tests/utils.test.js +85 -0
  31. package/v2Containers/InApp/utils.js +57 -0
  32. package/v2Containers/InApp/utils.test.js +70 -0
  33. package/v2Containers/MobilePush/Create/index.js +24 -20
  34. package/v2Containers/MobilePush/Edit/index.js +6 -2
  35. package/v2Containers/MobilepushWrapper/index.js +2 -0
  36. package/v2Containers/Sms/Create/index.js +1 -0
  37. package/v2Containers/Sms/Edit/index.js +2 -0
  38. package/v2Containers/SmsTrai/Edit/index.js +49 -10
  39. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +112 -36
  40. package/v2Containers/SmsTrai/Edit/tests/index.test.js +1 -3
  41. package/v2Containers/SmsWrapper/index.js +5 -1
  42. package/v2Containers/Templates/sagas.js +1 -1
@@ -1,222 +1,658 @@
1
- import "@testing-library/jest-dom";
2
- import get from 'lodash/get';
3
- import { getTreeStructuredTags } from "../common";
4
- import * as mockdata from "./common.mockdata";
5
- import { addBaseToTemplate, isEmbeddedEditOrPreview, transformCustomFieldsData } from "../commonUtils";
6
- import { EMBEDDED, FULL } from "../../v2Containers/Whatsapp/constants";
7
- import { CREATE, EDIT, PREVIEW } from "../../v2Containers/App/constants";
8
-
9
- jest.mock('@capillarytech/cap-ui-utils', () => ({
10
- Auth: {
11
- hasAccess: () => jest.fn(() => true),
12
- hasFeatureAccess: () => jest.fn(() => true),
13
- authHoc: jest.fn(),
14
- initialize: jest.fn(),
15
- },
16
- }));
17
-
18
- jest.mock('lodash/get');
19
-
20
- describe("common utils test", () => {
21
- it("test for getTreeStructuredTags when tagsList is not empty", () => {
22
- expect(getTreeStructuredTags({tagsList: mockdata.tagsList})).toEqual(mockdata.output2);
23
- });
24
- it("test for getTreeStructuredTags when tagsList is empty", () => {
25
- expect(getTreeStructuredTags({tagsList: []})).toEqual([]);
26
- });
27
- });
1
+ import {
2
+ validateLiquidTemplateContent,
3
+ validateMobilePushContent,
4
+ validateInAppContent,
5
+ getChannelData,
6
+ extractContent,addBaseToTemplate
7
+ } from "../commonUtils";
8
+ import { SMS_TRAI_VAR } from '../../v2Containers/SmsTrai/Edit/constants';
9
+ import { ANDROID, IOS } from '../../v2Containers/CreativesContainer/constants';
28
10
 
29
- describe('addBaseToTemplate', () => {
30
- it('should add the first item in history as base if history exists', () => {
31
- const template = {
32
- versions: {
33
- history: ['v1', 'v2', 'v3'],
34
- },
35
- };
36
- const expected = {
37
- versions: {
38
- history: ['v1', 'v2', 'v3'],
39
- base: {
40
- 0: "v", 1: "1", subject: undefined,
41
- },
42
- },
43
- };
44
- expect(addBaseToTemplate(template)).toEqual(expected);
11
+ describe("validateLiquidTemplateContent", () => {
12
+ const formatMessage = jest.fn((msg, vars) =>
13
+ vars ? `${msg.id}:${vars.unsupportedTags}` : msg.id
14
+ );
15
+ const messages = {
16
+ emailBodyEmptyError: { id: "empty" },
17
+ somethingWentWrong: { id: "wrong" },
18
+ unsupportedTagsValidationError: { id: "unsupported" }
19
+ };
20
+ const tagLookupMap = { foo: true };
21
+ const eventContextTags = [{ tagName: "bar" }];
22
+ const onError = jest.fn();
23
+ const onSuccess = jest.fn();
24
+
25
+ beforeEach(() => {
26
+ jest.clearAllMocks();
45
27
  });
46
28
 
47
- it('should return the original template if history is empty', () => {
48
- const template = {
49
- versions: {
50
- history: [],
51
- },
52
- };
53
- expect(addBaseToTemplate(template)).toEqual(template);
29
+ it("calls onError for empty content", async () => {
30
+ const getLiquidTags = jest.fn((content, cb) =>
31
+ cb({ errors: [], data: [] })
32
+ );
33
+ await validateLiquidTemplateContent("", {
34
+ getLiquidTags,
35
+ formatMessage,
36
+ messages,
37
+ onError,
38
+ onSuccess,
39
+ tagLookupMap,
40
+ eventContextTags
41
+ });
42
+ expect(onError).toHaveBeenCalledWith({
43
+ standardErrors: [undefined],
44
+ liquidErrors: [],
45
+ tabType: undefined
46
+ });
47
+ expect(onSuccess).not.toHaveBeenCalled();
54
48
  });
55
49
 
56
- it('should return the original template if history is undefined', () => {
57
- const template = {
58
- versions: {},
59
- };
60
- expect(addBaseToTemplate(template)).toEqual(template);
50
+ it("when onError is not provided, it should not throw an error", async () => {
51
+ const getLiquidTags = jest.fn((content, cb) =>
52
+ cb({ errors: [], data: [] })
53
+ );
54
+ await validateLiquidTemplateContent("", {
55
+ getLiquidTags,
56
+ formatMessage,
57
+ messages,
58
+ tagLookupMap,
59
+ eventContextTags
60
+ });
61
+ expect(onError).not.toHaveBeenCalled();
62
+ expect(onSuccess).not.toHaveBeenCalled();
61
63
  });
62
64
 
63
- it('should return the original template if versions is undefined', () => {
64
- const template = {};
65
- expect(addBaseToTemplate(template)).toEqual(template);
65
+ it("calls onError for API errors", async () => {
66
+ const getLiquidTags = jest.fn((content, cb) =>
67
+ cb({ askAiraResponse: { errors: [{ message: "API error" }], data: [] }, isError: true })
68
+ );
69
+ await validateLiquidTemplateContent("foo", {
70
+ getLiquidTags,
71
+ formatMessage,
72
+ messages,
73
+ onError,
74
+ onSuccess,
75
+ tagLookupMap,
76
+ eventContextTags
77
+ });
78
+ expect(onError).toHaveBeenCalledWith({
79
+ standardErrors: [],
80
+ liquidErrors: ["API error"],
81
+ tabType: undefined
82
+ });
83
+ expect(onSuccess).not.toHaveBeenCalled();
66
84
  });
67
85
 
68
- it('should handle null input gracefully', () => {
69
- expect(addBaseToTemplate(null)).toBeNull();
86
+ it("calls onError for unsupported tags", async () => {
87
+ const getLiquidTags = jest.fn((content, cb) =>
88
+ cb({ askAiraResponse: { errors: [], data: [{ name: "baz" }] }, isError: false })
89
+ );
90
+ await validateLiquidTemplateContent("foo", {
91
+ getLiquidTags,
92
+ formatMessage,
93
+ messages,
94
+ onError,
95
+ onSuccess,
96
+ tagLookupMap,
97
+ eventContextTags
98
+ });
99
+ expect(onError).toHaveBeenCalledWith({
100
+ standardErrors: [],
101
+ liquidErrors: [undefined],
102
+ tabType: undefined
103
+ });
104
+ expect(onSuccess).not.toHaveBeenCalled();
70
105
  });
71
106
 
72
- it('should handle undefined input gracefully', () => {
73
- expect(addBaseToTemplate(undefined)).toBeUndefined();
107
+ it("calls onSuccess for valid content", async () => {
108
+ const getLiquidTags = jest.fn((content, cb) =>
109
+ cb({ askAiraResponse: { errors: [], data: [{ name: "foo" }, { name: "bar" }] }, isError: false })
110
+ );
111
+ await validateLiquidTemplateContent("foo", {
112
+ getLiquidTags,
113
+ formatMessage,
114
+ messages,
115
+ onError,
116
+ onSuccess,
117
+ tagLookupMap,
118
+ eventContextTags
119
+ });
120
+ expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
74
121
  });
75
122
 
76
- it('should not modify the original template object', () => {
77
- const template = {
78
- versions: {
79
- history: ['v1', 'v2', 'v3'],
80
- },
123
+ it("calls onError with emailBodyEmptyError when validString is falsy", async () => {
124
+ const getLiquidTags = jest.fn((content, cb) => cb({ askAiraResponse: { errors: [], data: [] }, isError: false }));
125
+ const formatMessage = jest.fn((msg) => msg.id);
126
+ const messages = {
127
+ emailBodyEmptyError: { id: 'empty' },
128
+ somethingWentWrong: { id: 'wrong' },
129
+ unsupportedTagsValidationError: { id: 'unsupported' },
81
130
  };
82
- const originalTemplateCopy = JSON.parse(JSON.stringify(template));
83
- addBaseToTemplate(template);
84
- expect(template).toEqual(originalTemplateCopy);
131
+ const onError = jest.fn();
132
+ const onSuccess = jest.fn();
133
+ const tagLookupMap = {};
134
+ const eventContextTags = [];
135
+ await validateLiquidTemplateContent('', {
136
+ getLiquidTags,
137
+ formatMessage,
138
+ messages,
139
+ onError,
140
+ onSuccess,
141
+ tagLookupMap,
142
+ eventContextTags,
143
+ });
144
+ expect(formatMessage).toHaveBeenCalledWith(messages.emailBodyEmptyError);
145
+ expect(onError).toHaveBeenCalledWith({
146
+ standardErrors: ['empty'],
147
+ liquidErrors: [],
148
+ tabType: undefined,
149
+ });
150
+ expect(onSuccess).not.toHaveBeenCalled();
151
+ });
152
+
153
+ describe("addBaseToTemplate - subject property logic", () => {
154
+ it("should use template.versions.base.subject if it exists", () => {
155
+ const template = {
156
+ versions: {
157
+ base: { subject: "base-subject" },
158
+ history: [{ subject: "history-subject" }],
159
+ },
160
+ };
161
+ const result = addBaseToTemplate(template);
162
+ expect(result.versions.base.subject).toBe("base-subject");
163
+ });
164
+
165
+ it("should use history[0].subject if template.versions.base.subject does not exist", () => {
166
+ const template = {
167
+ versions: {
168
+ history: [{ subject: "history-subject" }],
169
+ },
170
+ };
171
+ const result = addBaseToTemplate(template);
172
+ expect(result.versions.base.subject).toBe("history-subject");
173
+ });
174
+
175
+ it("should set subject as undefined if neither exists", () => {
176
+ const template = {
177
+ versions: {
178
+ history: [{}],
179
+ },
180
+ };
181
+ const result = addBaseToTemplate(template);
182
+ expect(result.versions.base.subject).toBeUndefined();
183
+ });
184
+
185
+ it("should return the original template if history is empty", () => {
186
+ const template = {
187
+ versions: {
188
+ history: [],
189
+ },
190
+ };
191
+ expect(addBaseToTemplate(template)).toEqual(template);
192
+ });
85
193
  });
86
194
  });
87
195
 
88
- describe('isEmbeddedEditOrPreview', () => {
196
+ describe("validateMobilePushContent", () => {
197
+ const formatMessage = jest.fn(msg => msg.id);
198
+ const messages = {
199
+ emailBodyEmptyError: { id: "empty" },
200
+ somethingWentWrong: { id: "wrong" },
201
+ unsupportedTagsValidationError: { id: "unsupported" }
202
+ };
203
+ const tagLookupMap = { foo: true };
204
+ const eventContextTags = [{ tagName: "foo" }];
205
+ const onError = jest.fn();
206
+ const onSuccess = jest.fn();
207
+
89
208
  beforeEach(() => {
90
- get.mockClear();
209
+ jest.clearAllMocks();
91
210
  });
92
211
 
93
- it('should return true when query type is embedded and creatives mode is edit', () => {
94
- const queryType = EMBEDDED;
95
- const creativesMode = EDIT;
96
- expect(isEmbeddedEditOrPreview(queryType, creativesMode)).toBe(true);
212
+ it("calls onError for empty formData", async () => {
213
+ const getLiquidTags = jest.fn((content, cb) =>
214
+ cb({ askAiraResponse: { errors: [], data: [] }, isError: false })
215
+ );
216
+ await validateMobilePushContent(
217
+ {},
218
+ {
219
+ getLiquidTags,
220
+ formatMessage,
221
+ messages,
222
+ onError,
223
+ onSuccess,
224
+ tagLookupMap,
225
+ eventContextTags,
226
+ currentTab: 1
227
+ }
228
+ );
229
+ expect(onError).toHaveBeenCalled();
97
230
  });
98
231
 
99
- it('should return true when query type is embedded and creatives mode is preview', () => {
100
- const queryType = EMBEDDED;
101
- const creativesMode = PREVIEW;
102
- expect(isEmbeddedEditOrPreview(queryType, creativesMode)).toBe(true);
232
+ it("calls onSuccess for valid android and ios content", async () => {
233
+ const getLiquidTags = jest.fn((content, cb) =>
234
+ cb({ askAiraResponse: { errors: [], data: [] }, isError: false })
235
+ );
236
+ const formData = [{ foo: "bar" }, { baz: "qux" }];
237
+ await validateMobilePushContent(formData, {
238
+ getLiquidTags,
239
+ formatMessage,
240
+ messages,
241
+ onError,
242
+ onSuccess,
243
+ tagLookupMap,
244
+ eventContextTags,
245
+ currentTab: 1
246
+ });
247
+ expect(onSuccess).toHaveBeenCalled();
103
248
  });
104
249
 
105
- it('should return false when query type is not embedded', () => {
106
- const queryType = FULL;
107
- const creativesMode = PREVIEW;
108
- expect(isEmbeddedEditOrPreview(queryType, creativesMode)).toBe(false);
250
+ it("calls onSuccess with android content when tab is 1", async () => {
251
+ const getLiquidTags = jest.fn((content, cb) =>
252
+ cb({ askAiraResponse: { errors: [], data: [] }, isError: false })
253
+ );
254
+ const formData = [{ android: "content" }, { ios: "content" }];
255
+ await validateMobilePushContent(formData, {
256
+ getLiquidTags,
257
+ formatMessage,
258
+ messages,
259
+ onError,
260
+ onSuccess,
261
+ tagLookupMap,
262
+ eventContextTags,
263
+ currentTab: 1
264
+ });
265
+ expect(onSuccess).toHaveBeenCalledWith(JSON.stringify(formData[0]), "android");
109
266
  });
110
267
 
111
- it('should return false when creatives mode is not edit or preview', () => {
112
- const queryType = EMBEDDED;
113
- const creativesMode = CREATE;
114
- expect(isEmbeddedEditOrPreview(queryType, creativesMode)).toBe(false);
268
+ it("calls onSuccess with ios content when tab is 2", async () => {
269
+ const getLiquidTags = jest.fn((content, cb) =>
270
+ cb({ askAiraResponse: { errors: [], data: [] }, isError: false })
271
+ );
272
+ const formData = [{ android: "content" }, { ios: "content" }];
273
+ await validateMobilePushContent(formData, {
274
+ getLiquidTags,
275
+ formatMessage,
276
+ messages,
277
+ onError,
278
+ onSuccess,
279
+ tagLookupMap,
280
+ eventContextTags,
281
+ currentTab: 2
282
+ });
283
+ expect(onSuccess).toHaveBeenCalledWith(JSON.stringify(formData[1]), "ios");
115
284
  });
116
- });
117
285
 
118
- describe('transformCustomFieldsData', () => {
119
- it('should transform registration custom fields correctly', () => {
120
- const input = {
121
- 1: {
122
- name: 'age_group',
123
- label: 'Age Group',
124
- scope: 'loyalty_registration',
125
- },
126
- 2: {
127
- name: 'gender',
128
- label: 'Gender',
129
- scope: 'loyalty_registration',
286
+ it("falls back to android content when no tab selected and android exists", async () => {
287
+ const getLiquidTags = jest.fn((content, cb) =>
288
+ cb({ askAiraResponse: { errors: [], data: [] }, isError: false })
289
+ );
290
+ const formData = [{ android: "content" }, null];
291
+ await validateMobilePushContent(formData, {
292
+ getLiquidTags,
293
+ formatMessage,
294
+ messages,
295
+ onError,
296
+ onSuccess,
297
+ tagLookupMap,
298
+ eventContextTags
299
+ });
300
+ expect(onSuccess).toHaveBeenCalledWith(JSON.stringify(formData[0]), "android");
301
+ });
302
+
303
+ it("falls back to ios content when no tab selected and only ios exists", async () => {
304
+ const getLiquidTags = jest.fn((content, cb) =>
305
+ cb({ errors: [], data: [] })
306
+ );
307
+ const formData = [null, { ios: "content" }];
308
+ await validateMobilePushContent(formData, {
309
+ getLiquidTags,
310
+ formatMessage,
311
+ messages,
312
+ onError,
313
+ onSuccess,
314
+ tagLookupMap,
315
+ eventContextTags
316
+ });
317
+ expect(onSuccess).toHaveBeenCalledWith("null", "android");
318
+ });
319
+
320
+ it("calls onError for null formData", async () => {
321
+ const getLiquidTags = jest.fn((content, cb) => cb({ askAiraResponse: { errors: [], data: [] }, isError: false }));
322
+ await validateMobilePushContent(
323
+ null,
324
+ {
325
+ getLiquidTags,
326
+ formatMessage,
327
+ messages,
328
+ onError,
329
+ onSuccess,
330
+ tagLookupMap,
331
+ eventContextTags,
332
+ currentTab: 1,
130
333
  },
131
- };
334
+ );
335
+ expect(onError).toHaveBeenCalled();
336
+ });
132
337
 
133
- const expected = {
134
- 'Registration custom fields': {
135
- name: 'Registration custom fields',
136
- subtags: {
137
- 'custom_field.age_group': {
138
- name: 'age_group',
139
- desc: 'age_group',
140
- },
141
- 'custom_field.gender': {
142
- name: 'gender',
143
- desc: 'gender',
144
- },
145
- },
338
+ it("calls onError for undefined formData", async () => {
339
+ const getLiquidTags = jest.fn((content, cb) => cb({ askAiraResponse: { errors: [], data: [] }, isError: false }));
340
+ await validateMobilePushContent(
341
+ undefined,
342
+ {
343
+ getLiquidTags,
344
+ formatMessage,
345
+ messages,
346
+ onError,
347
+ onSuccess,
348
+ tagLookupMap,
349
+ eventContextTags,
350
+ currentTab: 1,
146
351
  },
147
- };
352
+ );
353
+ expect(onError).toHaveBeenCalled();
354
+ });
148
355
 
149
- expect(transformCustomFieldsData(input)).toEqual(expected);
356
+ it("calls onError for empty string formData", async () => {
357
+ const getLiquidTags = jest.fn((content, cb) => cb({ askAiraResponse: { errors: [], data: [] }, isError: false }));
358
+ await validateMobilePushContent(
359
+ '',
360
+ {
361
+ getLiquidTags,
362
+ formatMessage,
363
+ messages,
364
+ onError,
365
+ onSuccess,
366
+ tagLookupMap,
367
+ eventContextTags,
368
+ currentTab: 1,
369
+ },
370
+ );
371
+ expect(onError).toHaveBeenCalled();
150
372
  });
151
373
 
152
- it('should transform organization custom fields correctly', () => {
153
- const input = {
154
- 1: {
155
- name: 'org_phone',
156
- label: 'Phone',
157
- scope: 'org_custom_field',
374
+ it("calls onError for empty array formData", async () => {
375
+ const getLiquidTags = jest.fn((content, cb) => cb({ askAiraResponse: { errors: [], data: [] }, isError: false }));
376
+ await validateMobilePushContent(
377
+ [],
378
+ {
379
+ getLiquidTags,
380
+ formatMessage,
381
+ messages,
382
+ onError,
383
+ onSuccess,
384
+ tagLookupMap,
385
+ eventContextTags,
386
+ currentTab: 1,
158
387
  },
159
- 2: {
160
- name: 'store_location',
161
- label: 'Location',
162
- scope: 'store_custom_fields',
388
+ );
389
+ expect(onError).toHaveBeenCalled();
390
+ });
391
+
392
+ it("calls onError for both android and ios missing", async () => {
393
+ const getLiquidTags = jest.fn((content, cb) => cb({ askAiraResponse: { errors: [], data: [] }, isError: false }));
394
+ await validateMobilePushContent(
395
+ [{}, {}],
396
+ {
397
+ getLiquidTags,
398
+ formatMessage,
399
+ messages,
400
+ onError,
401
+ onSuccess,
402
+ tagLookupMap,
403
+ eventContextTags,
163
404
  },
164
- };
405
+ );
406
+ expect(onError).toHaveBeenCalled();
407
+ });
165
408
 
166
- const expected = {
167
- 'Organization custom fields': {
168
- name: 'Organization custom fields',
169
- subtags: {
170
- 'org_custom_field.org_phone': {
171
- name: 'org_phone',
172
- desc: 'org_phone',
173
- },
174
- 'store_custom_field.store_location': {
175
- name: 'store_location',
176
- desc: 'store_location',
177
- },
178
- },
409
+ it("calls onError for both android and ios null", async () => {
410
+ const getLiquidTags = jest.fn((content, cb) => cb({ askAiraResponse: { errors: [], data: [] }, isError: false }));
411
+ await validateMobilePushContent(
412
+ [null, null],
413
+ {
414
+ getLiquidTags,
415
+ formatMessage,
416
+ messages,
417
+ onError,
418
+ onSuccess,
419
+ tagLookupMap,
420
+ eventContextTags,
179
421
  },
180
- };
422
+ );
423
+ expect(onError).toHaveBeenCalled();
424
+ });
181
425
 
182
- expect(transformCustomFieldsData(input)).toEqual(expected);
426
+ it("calls onError for android and ios as empty strings", async () => {
427
+ const getLiquidTags = jest.fn((content, cb) => cb({ askAiraResponse: { errors: [], data: [] }, isError: false }));
428
+ await validateMobilePushContent(
429
+ [{ android: '' }, { ios: '' }],
430
+ {
431
+ getLiquidTags,
432
+ formatMessage,
433
+ messages,
434
+ onError,
435
+ onSuccess,
436
+ tagLookupMap,
437
+ eventContextTags,
438
+ },
439
+ );
440
+ expect(onError).toHaveBeenCalled();
183
441
  });
184
442
 
185
- it('should handle empty input correctly', () => {
186
- expect(transformCustomFieldsData({})).toEqual({});
443
+ it("calls onError for non-object types in formData", async () => {
444
+ const getLiquidTags = jest.fn((content, cb) => cb({ askAiraResponse: { errors: [], data: [] }, isError: false }));
445
+ await validateMobilePushContent(
446
+ [123, 456],
447
+ {
448
+ getLiquidTags,
449
+ formatMessage,
450
+ messages,
451
+ onError,
452
+ onSuccess,
453
+ tagLookupMap,
454
+ eventContextTags,
455
+ },
456
+ );
457
+ expect(onError).toHaveBeenCalled();
187
458
  });
188
459
 
189
- it('should handle fields with missing properties', () => {
190
- const input = {
191
- 1: {
192
- scope: 'loyalty_registration',
460
+ it("calls onError for string types in formData", async () => {
461
+ const getLiquidTags = jest.fn((content, cb) => cb({ askAiraResponse: { errors: [], data: [] }, isError: false }));
462
+ await validateMobilePushContent(
463
+ ['', ''],
464
+ {
465
+ getLiquidTags,
466
+ formatMessage,
467
+ messages,
468
+ onError,
469
+ onSuccess,
470
+ tagLookupMap,
471
+ eventContextTags,
193
472
  },
473
+ );
474
+ expect(onError).toHaveBeenCalled();
475
+ });
476
+ });
477
+
478
+ describe("validateInAppContent", () => {
479
+ const formatMessage = jest.fn(msg => msg.id);
480
+ const messages = {
481
+ emailBodyEmptyError: { id: "empty" },
482
+ somethingWentWrong: { id: "wrong" },
483
+ unsupportedTagsValidationError: { id: "unsupported" }
484
+ };
485
+ const tagLookupMap = { foo: true };
486
+ const eventContextTags = [{ tagName: "foo" }];
487
+ const onError = jest.fn();
488
+ const onSuccess = jest.fn();
489
+
490
+ beforeEach(() => {
491
+ jest.clearAllMocks();
492
+ });
493
+
494
+ it("calls onError for empty formData", async () => {
495
+ const getLiquidTags = jest.fn((content, cb) =>
496
+ cb({ askAiraResponse: { errors: [], data: [] }, isError: false })
497
+ );
498
+ await validateInAppContent(
499
+ {},
500
+ {
501
+ getLiquidTags,
502
+ formatMessage,
503
+ messages,
504
+ onError,
505
+ onSuccess,
506
+ tagLookupMap,
507
+ eventContextTags,
508
+ }
509
+ );
510
+ expect(onError).toHaveBeenCalled();
511
+ });
512
+
513
+ it("calls onSuccess for valid android and ios content", async () => {
514
+ const getLiquidTags = jest.fn((content, cb) =>
515
+ cb({ askAiraResponse: { errors: [], data: [] }, isError: false })
516
+ );
517
+ const formData = {
518
+ versions: {
519
+ base: {
520
+ content: {
521
+ ANDROID: { title: "t", message: "m", ctas: [{ text: "c" }] },
522
+ IOS: { title: "t2", message: "m2", ctas: [{ text: "c2" }] }
523
+ }
524
+ }
525
+ }
194
526
  };
527
+ await validateInAppContent(formData, {
528
+ getLiquidTags,
529
+ formatMessage,
530
+ messages,
531
+ onError,
532
+ onSuccess,
533
+ tagLookupMap,
534
+ eventContextTags,
535
+ singleTab: ANDROID,
536
+ });
537
+ expect(onSuccess).toHaveBeenCalled();
538
+ });
539
+ it("calls onSuccess for valid ios content", async () => {
195
540
 
196
- const expected = {
197
- 'Registration custom fields': {
198
- name: 'Registration custom fields',
199
- subtags: {
200
- 'custom_field.undefined': {
201
- name: undefined,
202
- desc: undefined,
203
- },
204
- },
205
- },
541
+ const getLiquidTags = jest.fn((content, cb) =>
542
+ cb({ askAiraResponse: { errors: [], data: [] }, isError: false })
543
+ );
544
+ const formData = {
545
+ versions: {
546
+ base: {
547
+ content: {
548
+ ANDROID: { title: "t", message: "m", ctas: [{ text: "c" }] },
549
+ IOS: { title: "t2", message: "m2", ctas: [{ text: "c2" }] }
550
+ }
551
+ }
552
+ }
206
553
  };
554
+ await validateInAppContent(formData, {
555
+ getLiquidTags,
556
+ formatMessage,
557
+ messages,
558
+ onError,
559
+ onSuccess,
560
+ tagLookupMap,
561
+ eventContextTags,
562
+ singleTab: IOS,
563
+ });
564
+ expect(onSuccess).toHaveBeenCalled();
565
+ });
566
+ });
207
567
 
208
- expect(transformCustomFieldsData(input)).toEqual(expected);
568
+ describe("getChannelData", () => {
569
+ it("returns converted email content for EMAIL channel with template-content", () => {
570
+ const formData = { base: { en: { "template-content": "<h1>Hello</h1>" } } };
571
+ // Mock convert to just return the input string for test
572
+ jest.mock("html-to-text", () => ({ convert: (str) => str }));
573
+ expect(getChannelData("email", formData)).toBe("");
209
574
  });
210
575
 
211
- it('should ignore fields with invalid scope', () => {
212
- const input = {
213
- 1: {
214
- name: 'test',
215
- label: 'Test',
216
- scope: 'invalid_scope',
217
- },
218
- };
576
+ it("returns empty string for EMAIL channel with missing template-content", () => {
577
+ const formData = { base: { en: {} } };
578
+ jest.mock("html-to-text", () => ({ convert: (str) => str }));
579
+ expect(getChannelData("email", formData)).toBe("");
580
+ });
581
+
582
+ it("returns SMS editor and template name for SMS channel", () => {
583
+ const formData = { base: { "sms-editor": "Hi" }, "template-name": "Test" };
584
+ expect(getChannelData("SMS", formData)).toBe("Hi Test");
585
+ });
219
586
 
220
- expect(transformCustomFieldsData(input)).toEqual({});
587
+ it("returns string with undefineds for SMS channel with missing fields", () => {
588
+ const formData = { base: {}, "template-name": undefined };
589
+ expect(getChannelData("SMS", formData)).toBe("undefined undefined");
590
+ });
591
+ it("returns string with undefineds for SMS channel with missing fields", () => {
592
+ expect(getChannelData("SMS", "")).toBe("undefined undefined");
593
+ });
594
+
595
+ it("returns JSON string for unknown channel", () => {
596
+ const formData = { foo: "bar" };
597
+ expect(getChannelData("unknown", formData)).toBe(JSON.stringify(formData));
598
+ });
599
+
600
+ it("returns JSON string for null formData", () => {
601
+ expect(getChannelData("SMS", null)).toBe("undefined undefined");
602
+ });
603
+
604
+ it("returns JSON string for undefined formData", () => {
605
+ expect(getChannelData("SMS", undefined)).toBe("undefined undefined");
606
+ });
607
+
608
+ it("handles missing base/en gracefully for EMAIL", () => {
609
+ expect(getChannelData("email", {})).toBe("");
610
+ expect(getChannelData("email", { base: {} })).toBe("");
611
+ expect(getChannelData("email", { base: { en: null } })).toBe("");
612
+ });
613
+ it("returns empty string for SMS_TRAI_VAR channel with missing fields", () => {
614
+ expect(getChannelData(SMS_TRAI_VAR, {})).toBe("");
615
+ expect(getChannelData(SMS_TRAI_VAR, { versions: {} })).toBe("");
616
+ expect(getChannelData(SMS_TRAI_VAR, { versions: { base: { "updated-sms-editor": [] } } })).toBe("");
617
+ });
618
+ });
619
+
620
+ describe("extractContent", () => {
621
+ it("returns empty string for null or undefined", () => {
622
+ expect(extractContent(null)).toBe("");
623
+ expect(extractContent(undefined)).toBe("");
624
+ });
625
+ it("returns empty string for empty object", () => {
626
+ expect(extractContent({})).toBe("");
627
+ });
628
+ it("returns only title if only title is present", () => {
629
+ expect(extractContent({ title: "Title" })).toBe("Title");
630
+ });
631
+ it("returns only message if only message is present", () => {
632
+ expect(extractContent({ message: "Message" })).toBe("Message");
633
+ });
634
+ it("returns only ctas text if only ctas with text is present", () => {
635
+ expect(extractContent({ ctas: [{ text: "CTA1" }, { text: "CTA2" }] })).toBe("CTA1 CTA2");
636
+ });
637
+ it("returns only ctas actionLink if only ctas with actionLink is present", () => {
638
+ expect(extractContent({ ctas: [{ actionLink: "/link1" }, { actionLink: "/link2" }] })).toBe("/link1 /link2");
639
+ });
640
+ it("returns ctas text if both text and actionLink are present (text preferred)", () => {
641
+ expect(extractContent({ ctas: [{ text: "CTA1", actionLink: "/link1" }, { text: "CTA2", actionLink: "/link2" }] })).toBe("CTA1 CTA2");
642
+ });
643
+ it("returns actionLink if text is missing", () => {
644
+ expect(extractContent({ ctas: [{ actionLink: "/link1" }, { text: null, actionLink: "/link2" }] })).toBe("/link1 /link2");
645
+ });
646
+ it("returns empty string for ctas with neither text nor actionLink", () => {
647
+ expect(extractContent({ ctas: [{}, { text: null }] })).toBe("");
648
+ });
649
+ it("returns all fields joined if all present", () => {
650
+ expect(extractContent({ title: "T", message: "M", ctas: [{ text: "C1" }, { actionLink: "C2" }] })).toBe("T M C1 C2");
651
+ });
652
+ it("handles ctas as empty array", () => {
653
+ expect(extractContent({ title: "T", message: "M", ctas: [] })).toBe("T M");
654
+ });
655
+ it("handles ctas as undefined", () => {
656
+ expect(extractContent({ title: "T", message: "M" })).toBe("T M");
221
657
  });
222
658
  });