@capillarytech/creatives-library 8.0.289 → 8.0.290-alpha.1

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 (52) hide show
  1. package/constants/unified.js +0 -1
  2. package/initialState.js +0 -2
  3. package/package.json +1 -1
  4. package/utils/common.js +5 -8
  5. package/utils/commonUtils.js +4 -85
  6. package/utils/tagValidations.js +84 -222
  7. package/utils/tests/commonUtil.test.js +461 -118
  8. package/utils/tests/tagValidations.test.js +280 -358
  9. package/v2Components/ErrorInfoNote/index.js +2 -5
  10. package/v2Components/FormBuilder/index.js +78 -161
  11. package/v2Components/FormBuilder/messages.js +0 -8
  12. package/v2Components/HtmlEditor/HTMLEditor.js +0 -5
  13. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -1
  14. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +0 -15
  15. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +1 -2
  16. package/v2Containers/Cap/mockData.js +0 -14
  17. package/v2Containers/Cap/reducer.js +3 -55
  18. package/v2Containers/Cap/tests/reducer.test.js +0 -102
  19. package/v2Containers/CreativesContainer/SlideBoxFooter.js +3 -1
  20. package/v2Containers/CreativesContainer/index.js +19 -6
  21. package/v2Containers/Email/index.js +1 -5
  22. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +10 -62
  23. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +12 -115
  24. package/v2Containers/FTP/index.js +2 -51
  25. package/v2Containers/FTP/messages.js +0 -4
  26. package/v2Containers/InApp/index.js +1 -96
  27. package/v2Containers/InApp/tests/index.test.js +17 -6
  28. package/v2Containers/InappAdvance/index.js +2 -103
  29. package/v2Containers/Line/Container/Text/index.js +0 -1
  30. package/v2Containers/MobilePush/Create/index.js +6 -16
  31. package/v2Containers/MobilePush/Edit/index.js +6 -16
  32. package/v2Containers/MobilePushNew/index.js +2 -33
  33. package/v2Containers/Rcs/index.js +12 -37
  34. package/v2Containers/Sms/Create/index.js +35 -3
  35. package/v2Containers/Sms/Create/messages.js +4 -0
  36. package/v2Containers/Sms/Edit/index.js +33 -3
  37. package/v2Containers/Sms/commonMethods.js +6 -6
  38. package/v2Containers/SmsTrai/Edit/index.js +6 -47
  39. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  40. package/v2Containers/Templates/reducer.js +3 -1
  41. package/v2Containers/Templates/tests/reducer.test.js +12 -0
  42. package/v2Containers/Viber/index.js +0 -1
  43. package/v2Containers/WebPush/Create/components/BrandIconSection.test.js +264 -0
  44. package/v2Containers/WebPush/Create/components/__snapshots__/BrandIconSection.test.js.snap +187 -0
  45. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +1 -3
  46. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +0 -7
  47. package/v2Containers/WebPush/Create/index.js +2 -2
  48. package/v2Containers/WebPush/Create/preview/tests/NotificationContainer.test.js +269 -0
  49. package/v2Containers/WebPush/Create/utils/validation.js +17 -2
  50. package/v2Containers/WebPush/Create/utils/validation.test.js +0 -24
  51. package/v2Containers/Whatsapp/index.js +9 -17
  52. package/v2Containers/Zalo/index.js +3 -11
@@ -6,6 +6,8 @@ import {
6
6
  extractContent,
7
7
  addBaseToTemplate,
8
8
  validateCarouselCards,
9
+ hasPersonalizationTags,
10
+ checkForPersonalizationTokens,
9
11
  } from "../commonUtils";
10
12
  import { skipTags } from "../tagValidations";
11
13
  import { SMS_TRAI_VAR } from '../../v2Containers/SmsTrai/Edit/constants';
@@ -20,13 +22,17 @@ describe("validateLiquidTemplateContent", () => {
20
22
  somethingWentWrong: { id: "wrong" },
21
23
  unsupportedTagsValidationError: { id: "unsupported" }
22
24
  };
23
- const tagLookupMap = { foo: true };
24
25
  const eventContextTags = [{ tagName: "bar" }];
25
26
  const onError = jest.fn();
26
27
  const onSuccess = jest.fn();
27
28
 
28
29
  beforeEach(() => {
29
30
  jest.clearAllMocks();
31
+ formatMessage.mockImplementation((msg, vars) =>
32
+ vars && vars.unsupportedTags != null
33
+ ? `${msg?.id}:${vars.unsupportedTags}`
34
+ : (msg?.id ?? "unsupported")
35
+ );
30
36
  });
31
37
 
32
38
  it("calls onError for empty content", async () => {
@@ -39,11 +45,10 @@ describe("validateLiquidTemplateContent", () => {
39
45
  messages,
40
46
  onError,
41
47
  onSuccess,
42
- tagLookupMap,
43
48
  eventContextTags
44
49
  });
45
50
  expect(onError).toHaveBeenCalledWith({
46
- standardErrors: [undefined],
51
+ standardErrors: ["empty"],
47
52
  liquidErrors: [],
48
53
  tabType: undefined
49
54
  });
@@ -58,7 +63,6 @@ describe("validateLiquidTemplateContent", () => {
58
63
  getLiquidTags,
59
64
  formatMessage,
60
65
  messages,
61
- tagLookupMap,
62
66
  eventContextTags
63
67
  });
64
68
  expect(onError).not.toHaveBeenCalled();
@@ -75,7 +79,6 @@ describe("validateLiquidTemplateContent", () => {
75
79
  messages,
76
80
  onError,
77
81
  onSuccess,
78
- tagLookupMap,
79
82
  eventContextTags
80
83
  });
81
84
  expect(onError).toHaveBeenCalledWith({
@@ -86,9 +89,149 @@ describe("validateLiquidTemplateContent", () => {
86
89
  expect(onSuccess).not.toHaveBeenCalled();
87
90
  });
88
91
 
89
- it("calls onError for unsupported tags", async () => {
92
+ it("categorizes errors as liquidErrors when isError=true with no result.errors (line 194-197)", async () => {
93
+ // isError=true + empty errors array → else-if (isError) branch → liquidErrors = [somethingWrongMsg]
94
+ // Use a plain function (not jest.fn) to avoid resetMocks:true wiping the implementation
95
+ const localFormatMessage = (msg) => msg?.id;
90
96
  const getLiquidTags = jest.fn((content, cb) =>
91
- cb({ askAiraResponse: { errors: [], data: [{ name: "baz" }] }, isError: false })
97
+ cb({ askAiraResponse: { errors: [], data: [] }, isError: true })
98
+ );
99
+ await validateLiquidTemplateContent("foo", {
100
+ getLiquidTags,
101
+ formatMessage: localFormatMessage,
102
+ messages,
103
+ onError,
104
+ onSuccess,
105
+ eventContextTags
106
+ });
107
+ expect(onError).toHaveBeenCalledWith({
108
+ standardErrors: [],
109
+ liquidErrors: ["wrong"],
110
+ tabType: undefined
111
+ });
112
+ expect(onSuccess).not.toHaveBeenCalled();
113
+ });
114
+
115
+ it("falls back to somethingWentWrong when error.message is not a string (line 189-191)", async () => {
116
+ // error.message is an object, not a string → typeof check fails → returns somethingWrongMsg
117
+ const localFormatMessage = (msg) => msg?.id;
118
+ const getLiquidTags = jest.fn((content, cb) =>
119
+ cb({ askAiraResponse: { errors: [{ message: { code: 123 } }], data: [] }, isError: false })
120
+ );
121
+ await validateLiquidTemplateContent("foo", {
122
+ getLiquidTags,
123
+ formatMessage: localFormatMessage,
124
+ messages,
125
+ onError,
126
+ onSuccess,
127
+ eventContextTags
128
+ });
129
+ expect(onError).toHaveBeenCalledWith({
130
+ standardErrors: [],
131
+ liquidErrors: ["wrong"],
132
+ tabType: undefined
133
+ });
134
+ });
135
+
136
+ it("falls back to somethingWentWrong when error.message is null (line 189-191)", async () => {
137
+ const localFormatMessage = (msg) => msg?.id;
138
+ const getLiquidTags = jest.fn((content, cb) =>
139
+ cb({ askAiraResponse: { errors: [{ message: null }], data: [] }, isError: false })
140
+ );
141
+ await validateLiquidTemplateContent("foo", {
142
+ getLiquidTags,
143
+ formatMessage: localFormatMessage,
144
+ messages,
145
+ onError,
146
+ onSuccess,
147
+ eventContextTags
148
+ });
149
+ expect(onError).toHaveBeenCalledWith({
150
+ standardErrors: [],
151
+ liquidErrors: ["wrong"],
152
+ tabType: undefined
153
+ });
154
+ });
155
+
156
+ it("falls back to somethingWentWrong when error.message is undefined (line 189-191)", async () => {
157
+ const localFormatMessage = (msg) => msg?.id;
158
+ const getLiquidTags = jest.fn((content, cb) =>
159
+ cb({ askAiraResponse: { errors: [{}], data: [] }, isError: false })
160
+ );
161
+ await validateLiquidTemplateContent("foo", {
162
+ getLiquidTags,
163
+ formatMessage: localFormatMessage,
164
+ messages,
165
+ onError,
166
+ onSuccess,
167
+ eventContextTags
168
+ });
169
+ expect(onError).toHaveBeenCalledWith({
170
+ standardErrors: [],
171
+ liquidErrors: ["wrong"],
172
+ tabType: undefined
173
+ });
174
+ });
175
+
176
+ it("maps multiple errors with mixed string/non-string messages (line 188-193)", async () => {
177
+ const localFormatMessage = (msg) => msg?.id;
178
+ const getLiquidTags = jest.fn((content, cb) =>
179
+ cb({
180
+ askAiraResponse: {
181
+ errors: [
182
+ { message: "First error" },
183
+ { message: null },
184
+ { message: "Third error" },
185
+ ],
186
+ data: [],
187
+ },
188
+ isError: false,
189
+ })
190
+ );
191
+ await validateLiquidTemplateContent("foo", {
192
+ getLiquidTags,
193
+ formatMessage: localFormatMessage,
194
+ messages,
195
+ onError,
196
+ onSuccess,
197
+ eventContextTags
198
+ });
199
+ expect(onError).toHaveBeenCalledWith({
200
+ standardErrors: [],
201
+ liquidErrors: ["First error", "wrong", "Third error"],
202
+ tabType: undefined
203
+ });
204
+ });
205
+
206
+ it("prefers result.errors over isError when both are present (result.errors takes the if-branch)", async () => {
207
+ // Both errors array AND isError=true → the if-branch (line 186) wins over else-if (line 194)
208
+ const getLiquidTags = jest.fn((content, cb) =>
209
+ cb({ askAiraResponse: { errors: [{ message: "from errors array" }], data: [] }, isError: true })
210
+ );
211
+ await validateLiquidTemplateContent("foo", {
212
+ getLiquidTags,
213
+ formatMessage,
214
+ messages,
215
+ onError,
216
+ onSuccess,
217
+ eventContextTags
218
+ });
219
+ expect(onError).toHaveBeenCalledWith({
220
+ standardErrors: [],
221
+ liquidErrors: ["from errors array"],
222
+ tabType: undefined
223
+ });
224
+ });
225
+
226
+ it("calls onError when API returns success but response.errors has validation errors (e.g. unsupported tag)", async () => {
227
+ const getLiquidTags = jest.fn((content, cb) =>
228
+ cb({
229
+ askAiraResponse: {
230
+ errors: [{ message: "Unsupported tag: custom_tag" }],
231
+ data: []
232
+ },
233
+ isError: false
234
+ })
92
235
  );
93
236
  await validateLiquidTemplateContent("foo", {
94
237
  getLiquidTags,
@@ -96,18 +239,33 @@ describe("validateLiquidTemplateContent", () => {
96
239
  messages,
97
240
  onError,
98
241
  onSuccess,
99
- tagLookupMap,
100
242
  eventContextTags
101
243
  });
102
244
  expect(onError).toHaveBeenCalledWith({
103
245
  standardErrors: [],
104
- liquidErrors: [undefined],
246
+ liquidErrors: ["Unsupported tag: custom_tag"],
105
247
  tabType: undefined
106
248
  });
107
249
  expect(onSuccess).not.toHaveBeenCalled();
108
250
  });
109
251
 
110
- it("calls onSuccess for valid content", async () => {
252
+ it("calls onSuccess when API returns no errors and a single extracted tag (extracted tags are not validated)", async () => {
253
+ const getLiquidTags = jest.fn((content, cb) =>
254
+ cb({ askAiraResponse: { errors: [], data: [{ name: "foo" }] }, isError: false })
255
+ );
256
+ await validateLiquidTemplateContent("foo", {
257
+ getLiquidTags,
258
+ formatMessage,
259
+ messages,
260
+ onError,
261
+ onSuccess,
262
+ eventContextTags
263
+ });
264
+ expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
265
+ expect(onError).not.toHaveBeenCalled();
266
+ });
267
+
268
+ it("calls onSuccess for valid content when API returns multiple extracted tags but no errors", async () => {
111
269
  const getLiquidTags = jest.fn((content, cb) =>
112
270
  cb({ askAiraResponse: { errors: [], data: [{ name: "foo" }, { name: "bar" }] }, isError: false })
113
271
  );
@@ -117,7 +275,6 @@ describe("validateLiquidTemplateContent", () => {
117
275
  messages,
118
276
  onError,
119
277
  onSuccess,
120
- tagLookupMap,
121
278
  eventContextTags
122
279
  });
123
280
  expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
@@ -133,7 +290,6 @@ describe("validateLiquidTemplateContent", () => {
133
290
  messages,
134
291
  onError,
135
292
  onSuccess,
136
- tagLookupMap,
137
293
  eventContextTags,
138
294
  skipTags
139
295
  });
@@ -151,7 +307,6 @@ describe("validateLiquidTemplateContent", () => {
151
307
  messages,
152
308
  onError,
153
309
  onSuccess,
154
- tagLookupMap,
155
310
  eventContextTags,
156
311
  skipTags
157
312
  });
@@ -169,7 +324,6 @@ describe("validateLiquidTemplateContent", () => {
169
324
  messages,
170
325
  onError,
171
326
  onSuccess,
172
- tagLookupMap,
173
327
  eventContextTags,
174
328
  skipTags
175
329
  });
@@ -187,7 +341,6 @@ describe("validateLiquidTemplateContent", () => {
187
341
  messages,
188
342
  onError,
189
343
  onSuccess,
190
- tagLookupMap,
191
344
  eventContextTags,
192
345
  skipTags
193
346
  });
@@ -205,7 +358,6 @@ describe("validateLiquidTemplateContent", () => {
205
358
  messages,
206
359
  onError,
207
360
  onSuccess,
208
- tagLookupMap,
209
361
  eventContextTags,
210
362
  skipTags
211
363
  });
@@ -223,7 +375,6 @@ describe("validateLiquidTemplateContent", () => {
223
375
  messages,
224
376
  onError,
225
377
  onSuccess,
226
- tagLookupMap,
227
378
  eventContextTags,
228
379
  skipTags
229
380
  });
@@ -241,7 +392,6 @@ describe("validateLiquidTemplateContent", () => {
241
392
  messages,
242
393
  onError,
243
394
  onSuccess,
244
- tagLookupMap,
245
395
  eventContextTags,
246
396
  skipTags
247
397
  });
@@ -259,7 +409,6 @@ describe("validateLiquidTemplateContent", () => {
259
409
  messages,
260
410
  onError,
261
411
  onSuccess,
262
- tagLookupMap,
263
412
  eventContextTags,
264
413
  skipTags
265
414
  });
@@ -277,7 +426,6 @@ describe("validateLiquidTemplateContent", () => {
277
426
  };
278
427
  const onError = jest.fn();
279
428
  const onSuccess = jest.fn();
280
- const tagLookupMap = {};
281
429
  const eventContextTags = [];
282
430
  await validateLiquidTemplateContent('', {
283
431
  getLiquidTags,
@@ -285,7 +433,6 @@ describe("validateLiquidTemplateContent", () => {
285
433
  messages,
286
434
  onError,
287
435
  onSuccess,
288
- tagLookupMap,
289
436
  eventContextTags,
290
437
  });
291
438
  expect(formatMessage).toHaveBeenCalledWith(messages.emailBodyEmptyError);
@@ -297,7 +444,7 @@ describe("validateLiquidTemplateContent", () => {
297
444
  expect(onSuccess).not.toHaveBeenCalled();
298
445
  });
299
446
 
300
- it("should skip tags that appear in {% %} syntax (like order.items in for loop)", async () => {
447
+ it("calls onSuccess when API returns extracted tags from {% for %} template but no errors (extracted tags are not validated)", async () => {
301
448
  const content = '{% for item in order.items %} Hello {% endfor %}';
302
449
  const getLiquidTags = jest.fn((content, cb) =>
303
450
  cb({ askAiraResponse: { errors: [], data: [{ name: "order.items" }] }, isError: false })
@@ -308,15 +455,13 @@ describe("validateLiquidTemplateContent", () => {
308
455
  messages,
309
456
  onError,
310
457
  onSuccess,
311
- tagLookupMap,
312
458
  eventContextTags
313
459
  });
314
- // order.items appears in {% %} syntax, so it should be skipped and validation should pass
315
460
  expect(onSuccess).toHaveBeenCalledWith(content, undefined);
316
461
  expect(onError).not.toHaveBeenCalled();
317
462
  });
318
463
 
319
- it("should skip tags that appear only inside {% %} blocks (like item.name in for loop)", async () => {
464
+ it("calls onSuccess when API returns extracted tags in {% %} blocks but no errors", async () => {
320
465
  const content = '{% for item in order.items %} {{ item.name }} - {{ item.quantity }} {% endfor %}';
321
466
  const getLiquidTags = jest.fn((content, cb) =>
322
467
  cb({ askAiraResponse: { errors: [], data: [{ name: "item.name" }, { name: "item.quantity" }] }, isError: false })
@@ -327,15 +472,13 @@ describe("validateLiquidTemplateContent", () => {
327
472
  messages,
328
473
  onError,
329
474
  onSuccess,
330
- tagLookupMap,
331
475
  eventContextTags
332
476
  });
333
- // item.name and item.quantity appear inside {% for %} block, so they should be skipped
334
477
  expect(onSuccess).toHaveBeenCalledWith(content, undefined);
335
478
  expect(onError).not.toHaveBeenCalled();
336
479
  });
337
480
 
338
- it("should validate tags that don't appear in content but are extracted by API", async () => {
481
+ it("calls onSuccess when API returns extracted tags not in content but no errors", async () => {
339
482
  const content = 'Some content without the tag';
340
483
  const getLiquidTags = jest.fn((content, cb) =>
341
484
  cb({ askAiraResponse: { errors: [], data: [{ name: "extractedTag" }] }, isError: false })
@@ -346,19 +489,13 @@ describe("validateLiquidTemplateContent", () => {
346
489
  messages,
347
490
  onError,
348
491
  onSuccess,
349
- tagLookupMap,
350
492
  eventContextTags
351
493
  });
352
- // extractedTag doesn't appear in content but was extracted by API, should be validated
353
- expect(onError).toHaveBeenCalledWith({
354
- standardErrors: [],
355
- liquidErrors: [undefined],
356
- tabType: undefined
357
- });
358
- expect(onSuccess).not.toHaveBeenCalled();
494
+ expect(onSuccess).toHaveBeenCalledWith(content, undefined);
495
+ expect(onError).not.toHaveBeenCalled();
359
496
  });
360
497
 
361
- it("should validate tags that appear outside {% %} blocks", async () => {
498
+ it("calls onSuccess when API returns tags outside {% %} but no errors", async () => {
362
499
  const content = '{{ outsideTag }} {% for item in order.items %} {{ item.name }} {% endfor %}';
363
500
  const getLiquidTags = jest.fn((content, cb) =>
364
501
  cb({ askAiraResponse: { errors: [], data: [{ name: "outsideTag" }, { name: "item.name" }] }, isError: false })
@@ -369,20 +506,70 @@ describe("validateLiquidTemplateContent", () => {
369
506
  messages,
370
507
  onError,
371
508
  onSuccess,
372
- tagLookupMap,
373
509
  eventContextTags
374
510
  });
375
- // outsideTag appears outside {% %} block, so it should be validated
376
- // item.name appears inside block, so it should be skipped
377
- expect(onError).toHaveBeenCalledWith({
378
- standardErrors: [],
379
- liquidErrors: [undefined],
380
- tabType: undefined
511
+ expect(onSuccess).toHaveBeenCalledWith(content, undefined);
512
+ expect(onError).not.toHaveBeenCalled();
513
+ });
514
+
515
+ it("calls onSuccess when API returns tag in {{ }} and no errors", async () => {
516
+ const content = 'Hello {{ unsupportedTag }} world';
517
+ const getLiquidTags = jest.fn((content, cb) =>
518
+ cb({ askAiraResponse: { errors: [], data: [{ name: "unsupportedTag" }] }, isError: false })
519
+ );
520
+ await validateLiquidTemplateContent(content, {
521
+ getLiquidTags,
522
+ formatMessage,
523
+ messages,
524
+ onError,
525
+ onSuccess,
526
+ eventContextTags
381
527
  });
382
- expect(onSuccess).not.toHaveBeenCalled();
528
+ expect(onSuccess).toHaveBeenCalledWith(content, undefined);
529
+ expect(onError).not.toHaveBeenCalled();
530
+ });
531
+
532
+ it("calls onSuccess when API returns tag with spacing variants and no errors", async () => {
533
+ const content = '{{ tag}} and {{tag }} and {{ tag }}';
534
+ const getLiquidTags = jest.fn((content, cb) =>
535
+ cb({
536
+ askAiraResponse: {
537
+ errors: [],
538
+ data: [{ name: "tag" }]
539
+ },
540
+ isError: false
541
+ })
542
+ );
543
+ await validateLiquidTemplateContent(content, {
544
+ getLiquidTags,
545
+ formatMessage,
546
+ messages,
547
+ onError,
548
+ onSuccess,
549
+ eventContextTags
550
+ });
551
+ expect(onSuccess).toHaveBeenCalledWith(content, undefined);
552
+ expect(onError).not.toHaveBeenCalled();
553
+ });
554
+
555
+ it("calls onSuccess when API returns tag inside {% %} blocks but no errors", async () => {
556
+ const content = '{% for x in some.unsupported %} {{ x }} {% endfor %}';
557
+ const getLiquidTags = jest.fn((content, cb) =>
558
+ cb({ askAiraResponse: { errors: [], data: [{ name: "some.unsupported" }] }, isError: false })
559
+ );
560
+ await validateLiquidTemplateContent(content, {
561
+ getLiquidTags,
562
+ formatMessage,
563
+ messages,
564
+ onError,
565
+ onSuccess,
566
+ eventContextTags
567
+ });
568
+ expect(onSuccess).toHaveBeenCalledWith(content, undefined);
569
+ expect(onError).not.toHaveBeenCalled();
383
570
  });
384
571
 
385
- it("should skip tags with dots that appear in {% %} syntax", async () => {
572
+ it("calls onSuccess when API returns tags with dots in {% %} syntax but no errors", async () => {
386
573
  const content = '{% assign myVar = order.items %} Some text';
387
574
  const getLiquidTags = jest.fn((content, cb) =>
388
575
  cb({ askAiraResponse: { errors: [], data: [{ name: "order.items" }] }, isError: false })
@@ -393,10 +580,8 @@ describe("validateLiquidTemplateContent", () => {
393
580
  messages,
394
581
  onError,
395
582
  onSuccess,
396
- tagLookupMap,
397
583
  eventContextTags
398
584
  });
399
- // order.items appears in {% %} syntax, so it should be skipped
400
585
  expect(onSuccess).toHaveBeenCalledWith(content, undefined);
401
586
  expect(onError).not.toHaveBeenCalled();
402
587
  });
@@ -451,7 +636,6 @@ describe("validateMobilePushContent", () => {
451
636
  somethingWentWrong: { id: "wrong" },
452
637
  unsupportedTagsValidationError: { id: "unsupported" }
453
638
  };
454
- const tagLookupMap = { foo: true };
455
639
  const eventContextTags = [{ tagName: "foo" }];
456
640
  const onError = jest.fn();
457
641
  const onSuccess = jest.fn();
@@ -460,7 +644,7 @@ describe("validateMobilePushContent", () => {
460
644
  jest.clearAllMocks();
461
645
  });
462
646
 
463
- it("calls onError for empty formData", async () => {
647
+ it("calls onError for empty formData (validateMobilePushContent)", async () => {
464
648
  const getLiquidTags = jest.fn((content, cb) =>
465
649
  cb({ askAiraResponse: { errors: [], data: [] }, isError: false })
466
650
  );
@@ -469,18 +653,17 @@ describe("validateMobilePushContent", () => {
469
653
  {
470
654
  getLiquidTags,
471
655
  formatMessage,
472
- messages,
473
- onError,
474
- onSuccess,
475
- tagLookupMap,
476
- eventContextTags,
656
+ messages,
657
+ onError,
658
+ onSuccess,
659
+ eventContextTags,
477
660
  currentTab: 1
478
661
  }
479
662
  );
480
663
  expect(onError).toHaveBeenCalled();
481
664
  });
482
665
 
483
- it("calls onSuccess for valid android and ios content", async () => {
666
+ it("calls onSuccess for valid android and ios content (validateMobilePushContent)", async () => {
484
667
  const getLiquidTags = jest.fn((content, cb) =>
485
668
  cb({ askAiraResponse: { errors: [], data: [] }, isError: false })
486
669
  );
@@ -491,7 +674,6 @@ describe("validateMobilePushContent", () => {
491
674
  messages,
492
675
  onError,
493
676
  onSuccess,
494
- tagLookupMap,
495
677
  eventContextTags,
496
678
  currentTab: 1
497
679
  });
@@ -509,7 +691,6 @@ describe("validateMobilePushContent", () => {
509
691
  messages,
510
692
  onError,
511
693
  onSuccess,
512
- tagLookupMap,
513
694
  eventContextTags,
514
695
  currentTab: 1
515
696
  });
@@ -527,7 +708,6 @@ describe("validateMobilePushContent", () => {
527
708
  messages,
528
709
  onError,
529
710
  onSuccess,
530
- tagLookupMap,
531
711
  eventContextTags,
532
712
  currentTab: 2
533
713
  });
@@ -545,7 +725,6 @@ describe("validateMobilePushContent", () => {
545
725
  messages,
546
726
  onError,
547
727
  onSuccess,
548
- tagLookupMap,
549
728
  eventContextTags
550
729
  });
551
730
  expect(onSuccess).toHaveBeenCalledWith(JSON.stringify(formData[0]), "android");
@@ -562,7 +741,6 @@ describe("validateMobilePushContent", () => {
562
741
  messages,
563
742
  onError,
564
743
  onSuccess,
565
- tagLookupMap,
566
744
  eventContextTags
567
745
  });
568
746
  expect(onSuccess).toHaveBeenCalledWith("null", "android");
@@ -575,11 +753,10 @@ describe("validateMobilePushContent", () => {
575
753
  {
576
754
  getLiquidTags,
577
755
  formatMessage,
578
- messages,
579
- onError,
580
- onSuccess,
581
- tagLookupMap,
582
- eventContextTags,
756
+ messages,
757
+ onError,
758
+ onSuccess,
759
+ eventContextTags,
583
760
  currentTab: 1,
584
761
  },
585
762
  );
@@ -593,11 +770,10 @@ describe("validateMobilePushContent", () => {
593
770
  {
594
771
  getLiquidTags,
595
772
  formatMessage,
596
- messages,
597
- onError,
598
- onSuccess,
599
- tagLookupMap,
600
- eventContextTags,
773
+ messages,
774
+ onError,
775
+ onSuccess,
776
+ eventContextTags,
601
777
  currentTab: 1,
602
778
  },
603
779
  );
@@ -611,11 +787,10 @@ describe("validateMobilePushContent", () => {
611
787
  {
612
788
  getLiquidTags,
613
789
  formatMessage,
614
- messages,
615
- onError,
616
- onSuccess,
617
- tagLookupMap,
618
- eventContextTags,
790
+ messages,
791
+ onError,
792
+ onSuccess,
793
+ eventContextTags,
619
794
  currentTab: 1,
620
795
  },
621
796
  );
@@ -629,11 +804,10 @@ describe("validateMobilePushContent", () => {
629
804
  {
630
805
  getLiquidTags,
631
806
  formatMessage,
632
- messages,
633
- onError,
634
- onSuccess,
635
- tagLookupMap,
636
- eventContextTags,
807
+ messages,
808
+ onError,
809
+ onSuccess,
810
+ eventContextTags,
637
811
  currentTab: 1,
638
812
  },
639
813
  );
@@ -647,11 +821,10 @@ describe("validateMobilePushContent", () => {
647
821
  {
648
822
  getLiquidTags,
649
823
  formatMessage,
650
- messages,
651
- onError,
652
- onSuccess,
653
- tagLookupMap,
654
- eventContextTags,
824
+ messages,
825
+ onError,
826
+ onSuccess,
827
+ eventContextTags,
655
828
  },
656
829
  );
657
830
  expect(onError).toHaveBeenCalled();
@@ -664,11 +837,10 @@ describe("validateMobilePushContent", () => {
664
837
  {
665
838
  getLiquidTags,
666
839
  formatMessage,
667
- messages,
668
- onError,
669
- onSuccess,
670
- tagLookupMap,
671
- eventContextTags,
840
+ messages,
841
+ onError,
842
+ onSuccess,
843
+ eventContextTags,
672
844
  },
673
845
  );
674
846
  expect(onError).toHaveBeenCalled();
@@ -681,11 +853,10 @@ describe("validateMobilePushContent", () => {
681
853
  {
682
854
  getLiquidTags,
683
855
  formatMessage,
684
- messages,
685
- onError,
686
- onSuccess,
687
- tagLookupMap,
688
- eventContextTags,
856
+ messages,
857
+ onError,
858
+ onSuccess,
859
+ eventContextTags,
689
860
  },
690
861
  );
691
862
  expect(onError).toHaveBeenCalled();
@@ -698,11 +869,10 @@ describe("validateMobilePushContent", () => {
698
869
  {
699
870
  getLiquidTags,
700
871
  formatMessage,
701
- messages,
702
- onError,
703
- onSuccess,
704
- tagLookupMap,
705
- eventContextTags,
872
+ messages,
873
+ onError,
874
+ onSuccess,
875
+ eventContextTags,
706
876
  },
707
877
  );
708
878
  expect(onError).toHaveBeenCalled();
@@ -715,11 +885,10 @@ describe("validateMobilePushContent", () => {
715
885
  {
716
886
  getLiquidTags,
717
887
  formatMessage,
718
- messages,
719
- onError,
720
- onSuccess,
721
- tagLookupMap,
722
- eventContextTags,
888
+ messages,
889
+ onError,
890
+ onSuccess,
891
+ eventContextTags,
723
892
  },
724
893
  );
725
894
  expect(onError).toHaveBeenCalled();
@@ -733,7 +902,6 @@ describe("validateInAppContent", () => {
733
902
  somethingWentWrong: { id: "wrong" },
734
903
  unsupportedTagsValidationError: { id: "unsupported" }
735
904
  };
736
- const tagLookupMap = { foo: true };
737
905
  const eventContextTags = [{ tagName: "foo" }];
738
906
  const onError = jest.fn();
739
907
  const onSuccess = jest.fn();
@@ -742,7 +910,7 @@ describe("validateInAppContent", () => {
742
910
  jest.clearAllMocks();
743
911
  });
744
912
 
745
- it("calls onError for empty formData", async () => {
913
+ it("calls onError for empty formData (validateInAppContent)", async () => {
746
914
  const getLiquidTags = jest.fn((content, cb) =>
747
915
  cb({ askAiraResponse: { errors: [], data: [] }, isError: false })
748
916
  );
@@ -751,17 +919,16 @@ describe("validateInAppContent", () => {
751
919
  {
752
920
  getLiquidTags,
753
921
  formatMessage,
754
- messages,
755
- onError,
756
- onSuccess,
757
- tagLookupMap,
758
- eventContextTags,
922
+ messages,
923
+ onError,
924
+ onSuccess,
925
+ eventContextTags,
759
926
  }
760
927
  );
761
928
  expect(onError).toHaveBeenCalled();
762
929
  });
763
930
 
764
- it("calls onSuccess for valid android and ios content", async () => {
931
+ it("calls onSuccess for valid android and ios content (validateInAppContent)", async () => {
765
932
  const getLiquidTags = jest.fn((content, cb) =>
766
933
  cb({ askAiraResponse: { errors: [], data: [] }, isError: false })
767
934
  );
@@ -781,7 +948,6 @@ describe("validateInAppContent", () => {
781
948
  messages,
782
949
  onError,
783
950
  onSuccess,
784
- tagLookupMap,
785
951
  eventContextTags,
786
952
  singleTab: ANDROID,
787
953
  });
@@ -808,7 +974,6 @@ describe("validateInAppContent", () => {
808
974
  messages,
809
975
  onError,
810
976
  onSuccess,
811
- tagLookupMap,
812
977
  eventContextTags,
813
978
  singleTab: IOS,
814
979
  });
@@ -835,11 +1000,11 @@ describe("getChannelData", () => {
835
1000
  expect(getChannelData("SMS", formData)).toBe("Hi Test");
836
1001
  });
837
1002
 
838
- it("returns string with undefineds for SMS channel with missing fields", () => {
1003
+ it("returns string with undefineds for SMS when base and template-name are empty or undefined", () => {
839
1004
  const formData = { base: {}, "template-name": undefined };
840
1005
  expect(getChannelData("SMS", formData)).toBe("undefined undefined");
841
1006
  });
842
- it("returns string with undefineds for SMS channel with missing fields", () => {
1007
+ it("returns string with undefineds for SMS when formData is empty string", () => {
843
1008
  expect(getChannelData("SMS", "")).toBe("undefined undefined");
844
1009
  });
845
1010
 
@@ -1601,3 +1766,181 @@ describe("validateCarouselCards", () => {
1601
1766
  });
1602
1767
  });
1603
1768
 
1769
+ describe('hasPersonalizationTags', () => {
1770
+ describe('liquid tags {{ }}', () => {
1771
+ it('returns true when text contains a liquid tag', () => {
1772
+ expect(hasPersonalizationTags('Hello {{user.name}}')).toBe(true);
1773
+ });
1774
+
1775
+ it('returns true for text with only opening and closing braces', () => {
1776
+ expect(hasPersonalizationTags('{{}}')).toBe(true);
1777
+ });
1778
+
1779
+ it('returns false when only {{ is present without }}', () => {
1780
+ expect(hasPersonalizationTags('Hello {{user.name')).toBeFalsy();
1781
+ });
1782
+
1783
+ it('returns false when only }} is present without {{', () => {
1784
+ expect(hasPersonalizationTags('Hello user.name}}')).toBeFalsy();
1785
+ });
1786
+ });
1787
+
1788
+ describe('event context tags [ ]', () => {
1789
+ it('returns true when text contains square bracket tags', () => {
1790
+ expect(hasPersonalizationTags('Hello [first_name]')).toBe(true);
1791
+ });
1792
+
1793
+ it('returns true for text with only [ ]', () => {
1794
+ expect(hasPersonalizationTags('[]')).toBe(true);
1795
+ });
1796
+
1797
+ it('returns false when only [ is present without ]', () => {
1798
+ expect(hasPersonalizationTags('Hello [first_name')).toBeFalsy();
1799
+ });
1800
+
1801
+ it('returns false when only ] is present without [', () => {
1802
+ expect(hasPersonalizationTags('Hello first_name]')).toBeFalsy();
1803
+ });
1804
+ });
1805
+
1806
+ describe('both tag types', () => {
1807
+ it('returns true when text contains both liquid and square bracket tags', () => {
1808
+ expect(hasPersonalizationTags('{{title}} and [body]')).toBe(true);
1809
+ });
1810
+ });
1811
+
1812
+ describe('edge cases', () => {
1813
+ it('returns falsy for empty string (default parameter)', () => {
1814
+ expect(hasPersonalizationTags()).toBeFalsy();
1815
+ });
1816
+
1817
+ it('returns falsy for empty string argument', () => {
1818
+ expect(hasPersonalizationTags('')).toBeFalsy();
1819
+ });
1820
+
1821
+ it('returns falsy for plain text with no tokens', () => {
1822
+ expect(hasPersonalizationTags('Hello World')).toBeFalsy();
1823
+ });
1824
+
1825
+ it('returns falsy for null', () => {
1826
+ expect(hasPersonalizationTags(null)).toBeFalsy();
1827
+ });
1828
+
1829
+ it('returns falsy for undefined', () => {
1830
+ expect(hasPersonalizationTags(undefined)).toBeFalsy();
1831
+ });
1832
+ });
1833
+ });
1834
+
1835
+ describe('checkForPersonalizationTokens', () => {
1836
+ describe('liquid tags {{ }}', () => {
1837
+ it('returns true when a field contains a liquid tag', () => {
1838
+ const formData = {
1839
+ tab1: { title: 'Hello {{user.name}}' },
1840
+ };
1841
+ expect(checkForPersonalizationTokens(formData)).toBe(true);
1842
+ });
1843
+
1844
+ it('returns true for multiline liquid tag spanning field value', () => {
1845
+ const formData = {
1846
+ tab1: { body: '{{user\n.name}}' },
1847
+ };
1848
+ expect(checkForPersonalizationTokens(formData)).toBe(true);
1849
+ });
1850
+ });
1851
+
1852
+ describe('event context tags [ ]', () => {
1853
+ it('returns true when a field contains a square bracket token', () => {
1854
+ const formData = {
1855
+ tab1: { message: 'Hi [first_name]' },
1856
+ };
1857
+ expect(checkForPersonalizationTokens(formData)).toBe(true);
1858
+ });
1859
+
1860
+ it('returns true for multiline square bracket token', () => {
1861
+ const formData = {
1862
+ tab1: { body: '[first\nname]' },
1863
+ };
1864
+ expect(checkForPersonalizationTokens(formData)).toBe(true);
1865
+ });
1866
+ });
1867
+
1868
+ describe('multiple tabs / fields', () => {
1869
+ it('returns true when token is in a nested tab other than the first', () => {
1870
+ const formData = {
1871
+ tab1: { title: 'plain text' },
1872
+ tab2: { body: 'Hello {{name}}' },
1873
+ };
1874
+ expect(checkForPersonalizationTokens(formData)).toBe(true);
1875
+ });
1876
+
1877
+ it('returns true when token is in a field other than the first in a tab', () => {
1878
+ const formData = {
1879
+ tab1: { title: 'plain text', body: '[event_tag]' },
1880
+ };
1881
+ expect(checkForPersonalizationTokens(formData)).toBe(true);
1882
+ });
1883
+
1884
+ it('returns false when no field in any tab has a token', () => {
1885
+ const formData = {
1886
+ tab1: { title: 'Hello World', body: 'No tokens here' },
1887
+ tab2: { title: 'Also plain', body: 'Still plain' },
1888
+ };
1889
+ expect(checkForPersonalizationTokens(formData)).toBe(false);
1890
+ });
1891
+ });
1892
+
1893
+ describe('non-string field values', () => {
1894
+ it('ignores numeric field values', () => {
1895
+ const formData = {
1896
+ tab1: { count: 42 },
1897
+ };
1898
+ expect(checkForPersonalizationTokens(formData)).toBe(false);
1899
+ });
1900
+
1901
+ it('ignores boolean field values', () => {
1902
+ const formData = {
1903
+ tab1: { enabled: true },
1904
+ };
1905
+ expect(checkForPersonalizationTokens(formData)).toBe(false);
1906
+ });
1907
+
1908
+ it('ignores null field values', () => {
1909
+ const formData = {
1910
+ tab1: { title: null },
1911
+ };
1912
+ expect(checkForPersonalizationTokens(formData)).toBe(false);
1913
+ });
1914
+
1915
+ it('ignores nested object field values (only one level deep)', () => {
1916
+ const formData = {
1917
+ tab1: { nested: { title: '{{token}}' } },
1918
+ };
1919
+ expect(checkForPersonalizationTokens(formData)).toBe(false);
1920
+ });
1921
+ });
1922
+
1923
+ describe('edge cases', () => {
1924
+ it('returns false for null input', () => {
1925
+ expect(checkForPersonalizationTokens(null)).toBe(false);
1926
+ });
1927
+
1928
+ it('returns false for undefined input', () => {
1929
+ expect(checkForPersonalizationTokens(undefined)).toBe(false);
1930
+ });
1931
+
1932
+ it('returns false for empty object', () => {
1933
+ expect(checkForPersonalizationTokens({})).toBe(false);
1934
+ });
1935
+
1936
+ it('returns false for non-object input (string)', () => {
1937
+ expect(checkForPersonalizationTokens('{{token}}')).toBe(false);
1938
+ });
1939
+
1940
+ it('returns false when tab value is not an object', () => {
1941
+ const formData = { tab1: '{{token}}' };
1942
+ expect(checkForPersonalizationTokens(formData)).toBe(false);
1943
+ });
1944
+ });
1945
+ });
1946
+