@capillarytech/creatives-library 8.0.307 → 8.0.308

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 (54) hide show
  1. package/constants/unified.js +1 -5
  2. package/initialState.js +2 -0
  3. package/package.json +1 -1
  4. package/utils/common.js +8 -5
  5. package/utils/commonUtils.js +93 -36
  6. package/utils/tagValidations.js +223 -83
  7. package/utils/tests/commonUtil.test.js +124 -147
  8. package/utils/tests/tagValidations.test.js +358 -441
  9. package/v2Components/ErrorInfoNote/index.js +5 -2
  10. package/v2Components/FormBuilder/index.js +203 -137
  11. package/v2Components/FormBuilder/messages.js +8 -0
  12. package/v2Components/HtmlEditor/HTMLEditor.js +5 -0
  13. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  14. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +15 -0
  15. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +2 -1
  16. package/v2Containers/Cap/mockData.js +14 -0
  17. package/v2Containers/Cap/reducer.js +55 -3
  18. package/v2Containers/Cap/tests/reducer.test.js +102 -0
  19. package/v2Containers/CreativesContainer/SlideBoxContent.js +1 -5
  20. package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -13
  21. package/v2Containers/CreativesContainer/constants.js +0 -6
  22. package/v2Containers/CreativesContainer/index.js +7 -47
  23. package/v2Containers/Email/index.js +5 -1
  24. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +70 -23
  25. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +120 -20
  26. package/v2Containers/FTP/index.js +51 -2
  27. package/v2Containers/FTP/messages.js +4 -0
  28. package/v2Containers/InApp/index.js +122 -35
  29. package/v2Containers/InApp/tests/index.test.js +6 -17
  30. package/v2Containers/InappAdvance/index.js +112 -4
  31. package/v2Containers/InappAdvance/tests/index.test.js +0 -2
  32. package/v2Containers/Line/Container/Text/index.js +1 -0
  33. package/v2Containers/MobilePush/Create/index.js +19 -59
  34. package/v2Containers/MobilePush/Edit/index.js +20 -48
  35. package/v2Containers/MobilePushNew/index.js +32 -12
  36. package/v2Containers/MobilepushWrapper/index.js +1 -3
  37. package/v2Containers/Rcs/index.js +37 -12
  38. package/v2Containers/Sms/Create/index.js +3 -39
  39. package/v2Containers/Sms/Create/messages.js +0 -4
  40. package/v2Containers/Sms/Edit/index.js +3 -35
  41. package/v2Containers/Sms/commonMethods.js +6 -3
  42. package/v2Containers/SmsTrai/Edit/index.js +47 -11
  43. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  44. package/v2Containers/SmsWrapper/index.js +0 -2
  45. package/v2Containers/TemplatesV2/index.js +13 -28
  46. package/v2Containers/Viber/index.js +1 -0
  47. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
  48. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
  49. package/v2Containers/WebPush/Create/index.js +2 -2
  50. package/v2Containers/WebPush/Create/utils/validation.js +8 -17
  51. package/v2Containers/WebPush/Create/utils/validation.test.js +24 -44
  52. package/v2Containers/Whatsapp/index.js +17 -9
  53. package/v2Containers/Zalo/index.js +11 -3
  54. package/v2Containers/Sms/tests/commonMethods.test.js +0 -122
@@ -1,7 +1,7 @@
1
1
  import '@testing-library/jest-dom';
2
2
  import {
3
- hasUnsubscribeTag,
4
- validateTagsCore,
3
+ checkSupport,
4
+ extractNames,
5
5
  getTagMapValue,
6
6
  getLoyaltyTagsMapValue,
7
7
  getForwardedMapValues,
@@ -9,160 +9,10 @@ import {
9
9
  validateIfTagClosed,
10
10
  validateTags,
11
11
  skipTags,
12
- checkIfSupportedTag,
13
- transformInjectedTags,
12
+ isInsideLiquidBlock,
14
13
  } from '../tagValidations';
15
14
  import { containsBase64Images } from '../content';
16
-
17
- describe('hasUnsubscribeTag', () => {
18
- it('should return false when content is not a string', () => {
19
- expect(hasUnsubscribeTag(null)).toBe(false);
20
- expect(hasUnsubscribeTag(undefined)).toBe(false);
21
- expect(hasUnsubscribeTag(123)).toBe(false);
22
- expect(hasUnsubscribeTag({})).toBe(false);
23
- });
24
-
25
- it('should return false when content has no unsubscribe tag', () => {
26
- expect(hasUnsubscribeTag('Hello world')).toBe(false);
27
- expect(hasUnsubscribeTag('{{ name }}')).toBe(false);
28
- });
29
-
30
- it('should return true when content has {{ unsubscribe }}', () => {
31
- expect(hasUnsubscribeTag('Click {{ unsubscribe }} here')).toBe(true);
32
- expect(hasUnsubscribeTag('{{ unsubscribe }}')).toBe(true);
33
- });
34
- });
35
-
36
- describe('validateTagsCore', () => {
37
- it('should include isContentEmpty: false when includeIsContentEmpty is true', () => {
38
- const result = validateTagsCore({
39
- contentForBraceCheck: '{{a}}',
40
- contentForUnsubscribeScan: '{{a}}',
41
- tags: null,
42
- currentModule: 'default',
43
- isFullMode: true,
44
- includeIsContentEmpty: true,
45
- });
46
- expect(result.isContentEmpty).toBe(false);
47
- expect(result.valid).toBe(true);
48
- });
49
-
50
- it('should use initialMissingTags when provided', () => {
51
- const result = validateTagsCore({
52
- contentForBraceCheck: '{{a}}',
53
- contentForUnsubscribeScan: '{{a}}',
54
- tags: [{ definition: { supportedModules: [], value: 'x' } }],
55
- currentModule: 'default',
56
- isFullMode: false,
57
- initialMissingTags: ['requiredTag'],
58
- });
59
- expect(result.missingTags).toEqual(['requiredTag']);
60
- expect(result.valid).toBe(false);
61
- });
62
-
63
- it('should use custom skipTagsFn when provided', () => {
64
- const customSkip = jest.fn(() => true);
65
- const result = validateTagsCore({
66
- contentForBraceCheck: '{{ unsubscribe }}',
67
- contentForUnsubscribeScan: '{{ unsubscribe }}',
68
- tags: [
69
- {
70
- definition: {
71
- supportedModules: [{ context: 'DEFAULT', mandatory: true }],
72
- value: 'unsubscribe',
73
- },
74
- },
75
- ],
76
- currentModule: 'DEFAULT',
77
- isFullMode: false,
78
- skipTagsFn: customSkip,
79
- });
80
- expect(customSkip).toHaveBeenCalled();
81
- expect(result.valid).toBe(true);
82
- });
83
- });
84
-
85
- describe('checkIfSupportedTag', () => {
86
- it('should return false for empty or no injected tags', () => {
87
- expect(checkIfSupportedTag('someTag', {})).toBe(false);
88
- expect(checkIfSupportedTag('someTag', undefined)).toBe(false);
89
- });
90
-
91
- it('should return true when tag matches a top-level key', () => {
92
- const injectedTags = { name: { name: 'Name' }, unsubscribe: { name: 'Unsubscribe' } };
93
- expect(checkIfSupportedTag('name', injectedTags)).toBe(true);
94
- expect(checkIfSupportedTag('unsubscribe', injectedTags)).toBe(true);
95
- });
96
-
97
- it('should return false when tag does not match any key', () => {
98
- const injectedTags = { name: { name: 'Name' } };
99
- expect(checkIfSupportedTag('other', injectedTags)).toBe(false);
100
- });
101
-
102
- it('should return true when tag matches a nested subtag', () => {
103
- const injectedTags = {
104
- customer: {
105
- name: 'Customer',
106
- subtags: {
107
- first_name: { name: 'First Name' },
108
- last_name: { name: 'Last Name' },
109
- },
110
- },
111
- };
112
- expect(checkIfSupportedTag('first_name', injectedTags)).toBe(true);
113
- expect(checkIfSupportedTag('last_name', injectedTags)).toBe(true);
114
- });
115
-
116
- it('should return false when tag is in neither top-level nor subtags', () => {
117
- const injectedTags = {
118
- customer: {
119
- name: 'Customer',
120
- subtags: { first_name: { name: 'First Name' } },
121
- },
122
- };
123
- expect(checkIfSupportedTag('unknown', injectedTags)).toBe(false);
124
- });
125
- });
126
-
127
- describe('transformInjectedTags', () => {
128
- it('should add tag-header and normalize subtags when key contains "subtags"', () => {
129
- const tags = [
130
- {
131
- name: 'Customer',
132
- subtags: {
133
- first_name: { name: 'First Name' },
134
- },
135
- },
136
- ];
137
- const result = transformInjectedTags(tags);
138
- expect(result).toBe(tags);
139
- expect(tags[0]['tag-header']).toBe(true);
140
- expect(tags[0].subtags).toEqual({ first_name: { name: 'First Name' } });
141
- });
142
-
143
- it('should recursively transform nested subtags', () => {
144
- const tags = [
145
- {
146
- name: 'Parent',
147
- subtags: {
148
- child: {
149
- name: 'Child',
150
- subtags: { grandchild: { name: 'Grandchild' } },
151
- },
152
- },
153
- },
154
- ];
155
- transformInjectedTags(tags);
156
- expect(tags[0].subtags.child.subtags).toEqual({ grandchild: { name: 'Grandchild' } });
157
- });
158
-
159
- it('should return tags unchanged when no subtag keys exist', () => {
160
- const tags = [{ name: 'Simple', desc: 'No subtags' }];
161
- const result = transformInjectedTags(tags);
162
- expect(result).toBe(tags);
163
- expect(tags[0]).toEqual({ name: 'Simple', desc: 'No subtags' });
164
- });
165
- });
15
+ import { eventContextTags } from '../../v2Containers/TagList/tests/mockdata';
166
16
 
167
17
  describe("check if curly brackets are balanced", () => {
168
18
  it("test for balanced curly brackets", () => {
@@ -172,9 +22,6 @@ describe("check if curly brackets are balanced", () => {
172
22
  value += "}}"
173
23
  let result = validateIfTagClosed(value);
174
24
  expect(result).toEqual(true);
175
- // no braces or empty string: match returns null, l1/l2/l3 undefined -> true
176
- expect(validateIfTagClosed("")).toEqual(true);
177
- expect(validateIfTagClosed("plain text no braces")).toEqual(true);
178
25
  //valid cases
179
26
  expect(validateIfTagClosed("{{{Hello}}}")).toEqual(true);
180
27
  expect(validateIfTagClosed("{{{Hello}}")).toEqual(true);
@@ -217,41 +64,53 @@ describe("validateTags", () => {
217
64
  it("should return valid response when all tags are present", () => {
218
65
  const content = "Hello {{tag1}}, {{tag2}}, {{tag3}} {{entryTrigger.lifetimePurchases}}";
219
66
 
67
+ const injectedTagsParams = [];
220
68
  const location = { query: { module: "DEFAULT" } };
221
69
  const tagModule = null;
222
70
 
223
71
  const result = validateTags({
224
72
  content,
225
73
  tagsParam,
74
+ injectedTagsParams,
226
75
  location,
227
76
  tagModule,
77
+ eventContextTags,
228
78
  });
229
79
 
230
80
  expect(result.valid).toEqual(true);
231
81
  expect(result.missingTags).toEqual([]);
82
+ expect(result.unsupportedTags).toEqual([]);
232
83
  expect(result.isBraceError).toEqual(false);
233
84
  });
234
85
 
235
- it("should return valid response when content has balanced braces and all tags present", () => {
86
+ it("should return invalid response when a mandatory tag is missing", () => {
236
87
  const content = "Hello {{tag1}}, {{tag2}}, {{tag3}}";
88
+ const updatedTagsParam = [...tagsParam, {
89
+ definition: {
90
+ supportedModules: [{ context: "DEFAULT", mandatory: true }],
91
+ value: "tag4",
92
+ },
93
+ },];
94
+ const injectedTagsParams = [];
237
95
  const location = { query: { module: "DEFAULT" } };
238
96
  const tagModule = null;
239
97
 
240
98
  const result = validateTags({
241
99
  content,
242
- tagsParam,
100
+ updatedTagsParam,
101
+ injectedTagsParams,
243
102
  location,
244
103
  tagModule,
245
104
  });
246
105
 
247
106
  expect(result.valid).toEqual(true);
248
- expect(result.missingTags).toEqual([]);
107
+ expect(result.unsupportedTags).toEqual(["tag1", "tag2", "tag3"]);
249
108
  expect(result.isBraceError).toEqual(false);
250
109
  });
251
110
 
252
- it("should return valid response when content has balanced braces with missingEventContextTags and partial tagsParam", () => {
111
+ it("should return invalid response when an unsupported tag is present", () => {
253
112
  const content = "Hello {{tag1}}, {{tag2}}, {{tag3}} {{missingEventContextTags}}";
254
- const tagsParamLocal = [
113
+ const tagsParam = [
255
114
  {
256
115
  definition: {
257
116
  supportedModules: [{ context: "DEFAULT", mandatory: true }],
@@ -265,24 +124,27 @@ describe("validateTags", () => {
265
124
  },
266
125
  },
267
126
  ];
127
+ const injectedTagsParams = [];
268
128
  const location = { query: { module: "DEFAULT" } };
269
129
  const tagModule = null;
270
130
 
271
131
  const result = validateTags({
272
132
  content,
273
- tagsParam: tagsParamLocal,
133
+ tagsParam,
134
+ injectedTagsParams,
274
135
  location,
275
136
  tagModule,
276
137
  });
277
138
 
278
139
  expect(result.valid).toEqual(true);
279
140
  expect(result.missingTags).toEqual([]);
141
+ expect(result.unsupportedTags).toEqual(["tag3", "missingEventContextTags"]);
280
142
  expect(result.isBraceError).toEqual(false);
281
143
  });
282
144
 
283
145
  it("should return invalid response when there is an unbalanced bracket error", () => {
284
146
  const content = "Hello {{tag1}, {{tag2}}, {{tag3}}";
285
- const tagsParamLocal = [
147
+ const tagsParam = [
286
148
  {
287
149
  definition: {
288
150
  supportedModules: [{ context: "DEFAULT", mandatory: true }],
@@ -302,23 +164,26 @@ describe("validateTags", () => {
302
164
  },
303
165
  },
304
166
  ];
167
+ const injectedTagsParams = [];
305
168
  const location = { query: { module: "DEFAULT" } };
306
169
  const tagModule = null;
307
170
 
308
171
  const result = validateTags({
309
172
  content,
310
- tagsParam: tagsParamLocal,
173
+ tagsParam,
174
+ injectedTagsParams,
311
175
  location,
312
176
  tagModule,
313
177
  });
314
178
 
315
179
  expect(result.valid).toEqual(false);
316
- expect(result.missingTags).toEqual([]);
180
+ expect(result.missingTags).toEqual(["tag1"]);
181
+ expect(result.unsupportedTags).toEqual([]);
317
182
  expect(result.isBraceError).toEqual(true);
318
183
  });
319
184
 
320
- it("should require unsubscribe when mandatory and missing, and accept skipped unsubscribe variant", () => {
321
- const tagsParamUnsubscribe = [
185
+ it("should remove 'unsubscribe' from missingTags if skipTags logic is triggered", () => {
186
+ const tagsParam = [
322
187
  {
323
188
  definition: {
324
189
  supportedModules: [{ context: "DEFAULT", mandatory: true }],
@@ -326,310 +191,288 @@ describe("validateTags", () => {
326
191
  },
327
192
  },
328
193
  ];
194
+ // Content does not include {{unsubscribe}}, so it would be missing
195
+ const contentMissing = "Hello {{tag1}}";
196
+ const injectedTagsParams = [];
329
197
  const location = { query: { module: "DEFAULT" } };
330
198
  const tagModule = null;
331
199
 
332
- const contentMissing = "Hello world";
200
+ // First, verify unsubscribe is missing if not present
333
201
  const resultMissing = validateTags({
334
202
  content: contentMissing,
335
- tagsParam: tagsParamUnsubscribe,
203
+ tagsParam,
204
+ injectedTagsParams,
336
205
  location,
337
206
  tagModule,
338
207
  });
339
208
  expect(resultMissing.missingTags).toContain("unsubscribe");
340
- expect(resultMissing.valid).toBe(false);
341
209
 
210
+ // Now, content includes a tag that triggers skipTags logic for unsubscribe
211
+ // e.g., {{unsubscribe(#123456)}} matches the skipTags regex
342
212
  const contentWithSkippedUnsubscribe = "Hello {{unsubscribe(#123456)}}";
343
213
  const resultSkipped = validateTags({
344
214
  content: contentWithSkippedUnsubscribe,
345
- tagsParam: tagsParamUnsubscribe,
215
+ tagsParam,
216
+ injectedTagsParams,
346
217
  location,
347
218
  tagModule,
348
219
  });
349
220
  expect(resultSkipped.missingTags).not.toContain("unsubscribe");
350
221
  expect(resultSkipped.valid).toBe(true);
351
-
352
- const contentWithWhitespaceUnsubscribe = "Hello {{ unsubscribe }}";
353
- const resultWhitespace = validateTags({
354
- content: contentWithWhitespaceUnsubscribe,
355
- tagsParam: tagsParamUnsubscribe,
356
- location,
357
- tagModule,
358
- });
359
- expect(resultWhitespace.missingTags).not.toContain("unsubscribe");
360
- expect(resultWhitespace.valid).toBe(true);
361
- expect(resultWhitespace.unsupportedTags ?? []).toEqual([]);
362
222
  });
363
223
  });
364
224
 
365
- describe('validateTags wrapper (v2 consumers)', () => {
366
- const tagsWithUnsubscribe = [
367
- {
368
- definition: {
369
- supportedModules: [{ context: 'DEFAULT', mandatory: true }],
370
- value: 'unsubscribe',
225
+ describe("extractNames", () => {
226
+ it("should return an empty array when data is empty", () => {
227
+ const data = [];
228
+ const result = extractNames(data);
229
+ expect(result).toEqual([]);
230
+ });
231
+
232
+ it("should return an array of names when data contains nodes with names", () => {
233
+ const data = [
234
+ {
235
+ name: "Node 1",
236
+ children: [
237
+ {
238
+ name: "Node 1.1",
239
+ children: [],
240
+ },
241
+ {
242
+ name: "Node 1.2",
243
+ children: [],
244
+ },
245
+ ],
371
246
  },
372
- },
373
- ];
374
- const tagsOutboundUnsubscribe = [
375
- {
376
- definition: {
377
- supportedModules: [{ context: 'outbound', mandatory: true }],
378
- value: 'unsubscribe',
247
+ {
248
+ name: "Node 2",
249
+ children: [
250
+ {
251
+ name: "Node 2.1",
252
+ children: [],
253
+ },
254
+ ],
379
255
  },
380
- },
381
- ];
382
- const tagsDefaultLowercase = [
383
- {
384
- definition: {
385
- supportedModules: [{ context: 'default', mandatory: true }],
386
- value: 'unsubscribe',
256
+ ];
257
+ const result = extractNames(data);
258
+ expect(result).toEqual([
259
+ "Node 1",
260
+ "Node 1.1",
261
+ "Node 1.2",
262
+ "Node 2",
263
+ "Node 2.1",
264
+ ]);
265
+ });
266
+
267
+ it("should ignore nodes without names", () => {
268
+ const data = [
269
+ {
270
+ name: "Node 1",
271
+ children: [
272
+ {
273
+ name: "Node 1.1",
274
+ children: [],
275
+ },
276
+ {
277
+ children: [],
278
+ },
279
+ ],
387
280
  },
388
- },
389
- ];
390
-
391
- describe('module selection (location.query.module vs tagModule vs DEFAULT)', () => {
392
- it('uses location.query.module when set and tagModule not provided', () => {
393
- const content = 'Hello world';
394
- const result = validateTags({
395
- content,
396
- tagsParam: tagsWithUnsubscribe,
397
- location: { query: { module: 'DEFAULT' } },
398
- tagModule: null,
399
- });
400
- expect(result.missingTags).toContain('unsubscribe');
401
- expect(result.valid).toBe(false);
402
- });
403
-
404
- it('uses tagModule override when provided (overrides location.query.module)', () => {
405
- const content = 'Hello world';
406
- const resultWithOutbound = validateTags({
407
- content,
408
- tagsParam: tagsOutboundUnsubscribe,
409
- location: { query: { module: 'DEFAULT' } },
410
- tagModule: 'outbound',
411
- });
412
- expect(resultWithOutbound.missingTags).toContain('unsubscribe');
413
- expect(resultWithOutbound.valid).toBe(false);
414
-
415
- const resultWithDefault = validateTags({
416
- content,
417
- tagsParam: tagsWithUnsubscribe,
418
- location: { query: { module: 'outbound' } },
419
- tagModule: 'DEFAULT',
420
- });
421
- expect(resultWithDefault.missingTags).toContain('unsubscribe');
422
- expect(resultWithDefault.valid).toBe(false);
423
- });
424
-
425
- it('uses DEFAULT (lowercase) when location is undefined', () => {
426
- const content = 'Hello world';
427
- const result = validateTags({
428
- content,
429
- tagsParam: tagsDefaultLowercase,
430
- location: undefined,
431
- tagModule: null,
432
- });
433
- expect(result.missingTags).toContain('unsubscribe');
434
- expect(result.valid).toBe(false);
435
- });
436
-
437
- it('uses DEFAULT when location.query is undefined', () => {
438
- const content = 'Hello world';
439
- const result = validateTags({
440
- content,
441
- tagsParam: tagsDefaultLowercase,
442
- location: {},
443
- tagModule: null,
444
- });
445
- expect(result.missingTags).toContain('unsubscribe');
446
- expect(result.valid).toBe(false);
447
- });
448
-
449
- it('uses DEFAULT when location.query.module is falsy', () => {
450
- const content = 'Hello world';
451
- const result = validateTags({
452
- content,
453
- tagsParam: tagsDefaultLowercase,
454
- location: { query: { module: '' } },
455
- tagModule: null,
456
- });
457
- expect(result.missingTags).toContain('unsubscribe');
458
- expect(result.valid).toBe(false);
459
- });
281
+ {
282
+ name: "Node 2",
283
+ children: [
284
+ {
285
+ name: "Node 2.1",
286
+ children: [],
287
+ },
288
+ {
289
+ name: "Node 2.2",
290
+ },
291
+ ],
292
+ },
293
+ ];
294
+ const result = extractNames(data);
295
+ expect(result).toEqual([
296
+ "Node 1",
297
+ "Node 1.1",
298
+ "Node 2",
299
+ "Node 2.1",
300
+ "Node 2.2",
301
+ ]);
460
302
  });
303
+ });
461
304
 
462
- describe('content passed to contentForBraceCheck and contentForUnsubscribeScan', () => {
463
- it('uses same content for brace check (isBraceError when unbalanced)', () => {
464
- const content = 'Hello {{tag1}, {{tag2}}';
465
- const result = validateTags({
466
- content,
467
- tagsParam: [],
468
- location: { query: { module: 'DEFAULT' } },
469
- });
470
- expect(result.isBraceError).toBe(true);
471
- expect(result.valid).toBe(false);
472
- });
473
-
474
- it('uses same content for unsubscribe scan (missing unsubscribe when required)', () => {
475
- const content = 'Hello {{other}}';
476
- const result = validateTags({
477
- content,
478
- tagsParam: tagsWithUnsubscribe,
479
- location: { query: { module: 'DEFAULT' } },
480
- });
481
- expect(result.missingTags).toContain('unsubscribe');
482
- expect(result.valid).toBe(false);
483
- });
484
305
 
485
- it('content with {{ unsubscribe }} satisfies unsubscribe requirement', () => {
486
- const content = 'Hello {{ unsubscribe }}';
487
- const result = validateTags({
488
- content,
489
- tagsParam: tagsWithUnsubscribe,
490
- location: { query: { module: 'DEFAULT' } },
491
- });
492
- expect(result.missingTags).not.toContain('unsubscribe');
493
- expect(result.valid).toBe(true);
494
- });
306
+ describe("checkSupport", () => {
307
+ it("should return an empty array when args are empty", () => {
308
+ const result = checkSupport();
309
+ expect(result).toEqual([]);
495
310
  });
496
-
497
- describe('isFullMode', () => {
498
- it('when true skips unsubscribe check (no missingTags for mandatory unsubscribe)', () => {
499
- const content = 'Hello world';
500
- const result = validateTags({
501
- content,
502
- tagsParam: tagsWithUnsubscribe,
503
- location: { query: { module: 'DEFAULT' } },
504
- isFullMode: true,
505
- });
506
- expect(result.missingTags).toEqual([]);
507
- expect(result.valid).toBe(true);
508
- });
509
-
510
- it('when true still runs brace check', () => {
511
- const content = 'Hello {{tag1}';
512
- const result = validateTags({
513
- content,
514
- tagsParam: tagsWithUnsubscribe,
515
- location: { query: { module: 'DEFAULT' } },
516
- isFullMode: true,
517
- });
518
- expect(result.isBraceError).toBe(true);
519
- expect(result.valid).toBe(false);
520
- });
311
+ it("should return an empty array when response data is empty", () => {
312
+ const response = { data: [] };
313
+ const tagObject = {};
314
+ const result = checkSupport(response, tagObject);
315
+ expect(result).toEqual([]);
521
316
  });
522
317
 
523
- describe('tagsParam null or empty', () => {
524
- it('when null: only brace check runs, no missing-tag logic', () => {
525
- const content = 'Hello {{a}}';
526
- const result = validateTags({
527
- content,
528
- tagsParam: null,
529
- location: { query: { module: 'DEFAULT' } },
530
- });
531
- expect(result.missingTags).toEqual([]);
532
- expect(result.isBraceError).toBe(false);
533
- expect(result.valid).toBe(true);
534
- });
535
-
536
- it('when empty array: only brace check runs', () => {
537
- const content = 'Hello {{a}}';
538
- const result = validateTags({
539
- content,
540
- tagsParam: [],
541
- location: { query: { module: 'DEFAULT' } },
542
- });
543
- expect(result.missingTags).toEqual([]);
544
- expect(result.valid).toBe(true);
545
- });
546
-
547
- it('when null with unbalanced braces: returns isBraceError', () => {
548
- const content = 'Hello {{a}';
549
- const result = validateTags({
550
- content,
551
- tagsParam: null,
552
- location: { query: { module: 'DEFAULT' } },
553
- });
554
- expect(result.isBraceError).toBe(true);
555
- expect(result.valid).toBe(false);
556
- expect(result.missingTags).toEqual([]);
557
- });
318
+ it("should return an empty array when tagObject is empty", () => {
319
+ const response = {
320
+ data: [{ name: "tag1" }, { name: "tag2" }, { name: "tag3" }],
321
+ };
322
+ const tagObject = {};
323
+ const result = checkSupport(response, tagObject);
324
+ expect(result).toEqual([]);
558
325
  });
559
326
 
560
- describe('v2 consumer call patterns', () => {
561
- it('Whatsapp-style: content, tagsParam, location, tagModule (getDefaultTags), isFullMode', () => {
562
- const content = 'Hello {{ unsubscribe }}';
563
- const result = validateTags({
564
- content: content,
565
- tagsParam: tagsWithUnsubscribe,
566
- location: { query: { module: 'DEFAULT' } },
567
- tagModule: 'DEFAULT',
568
- isFullMode: false,
569
- });
570
- expect(result.valid).toBe(true);
571
- expect(result.isBraceError).toBe(false);
572
- });
573
-
574
- it('Zalo / Rcs / MobilePushNew / EmailWrapper: same pattern as Whatsapp', () => {
575
- const result = validateTags({
576
- content: 'Hi {{ unsubscribe }}',
577
- tagsParam: tagsWithUnsubscribe,
578
- location: { query: { module: 'DEFAULT' } },
579
- tagModule: 'DEFAULT',
580
- isFullMode: false,
581
- });
582
- expect(result.valid).toBe(true);
583
- });
327
+ it("should return event context tags even if tagObject is empty", () => {
328
+ const response = {
329
+ data: [{ name: "tag1" }, { name: "entryTrigger.lifetimePurchases" }],
330
+ };
331
+ const tagObject = {};
332
+ const result = checkSupport(response, tagObject, eventContextTags);
333
+ expect(result).toEqual(['entryTrigger.lifetimePurchases']);
334
+ });
584
335
 
585
- it('Line / Viber: tagModule "outbound"', () => {
586
- const contentMissing = 'Hello';
587
- const resultMissing = validateTags({
588
- content: contentMissing,
589
- tagsParam: tagsOutboundUnsubscribe,
590
- location: { query: { module: 'inbound' } },
591
- tagModule: 'outbound',
592
- isFullMode: false,
593
- });
594
- expect(resultMissing.missingTags).toContain('unsubscribe');
595
- expect(resultMissing.valid).toBe(false);
596
-
597
- const resultOk = validateTags({
598
- content: 'Hello {{ unsubscribe }}',
599
- tagsParam: tagsOutboundUnsubscribe,
600
- location: {},
601
- tagModule: 'outbound',
602
- isFullMode: false,
603
- });
604
- expect(resultOk.valid).toBe(true);
605
- });
336
+ it("should return an array of supported tags", () => {
337
+ const response = {
338
+ data: [{ name: "tag1" }, { name: "tag2" }, { name: "tag3" }],
339
+ };
340
+ const tagObject = {
341
+ tag1: { definition: { subtags: ["tag1.1", "tag1.2"] } },
342
+ tag2: { definition: { subtags: ["tag2.1", "tag2.2"] } },
343
+ tag3: { definition: { subtags: ["tag3.1", "tag3.2"] } },
344
+ };
345
+ const result = checkSupport(response, tagObject);
346
+ expect(result).toEqual(["tag1", "tag2", "tag3"]);
347
+ });
606
348
 
607
- it('WebPush: validationConfig spread (content + tagsParam, location, tagModule, isFullMode)', () => {
608
- const validationConfig = {
609
- tagsParam: tagsWithUnsubscribe,
610
- location: { query: { module: 'DEFAULT' } },
611
- tagModule: 'DEFAULT',
612
- };
613
- const result = validateTags({
614
- content: 'Hello {{ unsubscribe }}',
615
- ...validationConfig,
616
- isFullMode: false,
617
- });
618
- expect(result.valid).toBe(true);
619
- expect(result.isBraceError).toBe(false);
620
- });
349
+ it("should return an array of supported tags with subtags", () => {
350
+ const response = {
351
+ data: [{ name: "tag1" }, { name: "tag2" }, { name: "tag3" }],
352
+ };
353
+ const tagObject = {
354
+ tag1: { definition: { subtags: ["tag1.1", "tag1.2"] } },
355
+ tag2: { definition: { subtags: ["tag2.1", "tag2.2"] } },
356
+ tag3: { definition: { subtags: ["tag3.1", "tag3.2"] } },
357
+ "tag1.1": {},
358
+ "tag2.1": {},
359
+ "tag3.1": {},
360
+ };
361
+ const result = checkSupport(response, tagObject);
362
+ expect(result).toEqual(["tag1", "tag2", "tag3"]);
363
+ });
621
364
 
622
- it('WebPush with isFullMode: only brace check', () => {
623
- const result = validateTags({
624
- content: 'Hello world',
625
- tagsParam: tagsWithUnsubscribe,
626
- location: { query: { module: 'DEFAULT' } },
627
- tagModule: 'DEFAULT',
628
- isFullMode: true,
629
- });
630
- expect(result.valid).toBe(true);
631
- expect(result.missingTags).toEqual([]);
632
- });
365
+ it("should return an empty array when no tags are supported", () => {
366
+ const response = {
367
+ data: [{ name: "tag1" }, { name: "tag2" }, { name: "tag3" }],
368
+ };
369
+ const tagObject = {
370
+ tag4: {},
371
+ tag5: {},
372
+ tag6: {},
373
+ };
374
+ const result = checkSupport(response, tagObject);
375
+ expect(result).toEqual([]);
376
+ });
377
+ it("should return an array of supported tags with subtags", () => {
378
+ const response = {
379
+ data: [
380
+ {
381
+ name: "tag1",
382
+ children: [{ name: "tag1.1", children: [{ name: "tag1.2" }] }],
383
+ },
384
+ { name: "tag2", children: [] },
385
+ { name: "tag3" },
386
+ ],
387
+ };
388
+ const tagObject = {
389
+ tag1: { definition: { subtags: [".1", "tag1.2"] } },
390
+ tag2: { definition: { subtags: ["tag2.1", "tag2.2"] } },
391
+ tag3: { definition: { subtags: ["tag3.1", "tag3.2"] } },
392
+ "tag1.1": {},
393
+ "tag2.1": {},
394
+ "tag3.1": {},
395
+ };
396
+ const result = checkSupport(response, tagObject);
397
+ expect(result).toEqual(["tag1", "tag1.1", "tag2", "tag3"]);
398
+ });
399
+ it("should return an array of supported tags children as an object", () => {
400
+ const response = {
401
+ data: [
402
+ { name: "tag1", children: [{ name: "tag" }] },
403
+ { name: "tag2", children: [] },
404
+ { name: "tag3" },
405
+ ],
406
+ };
407
+ const tagObject = {
408
+ tag1: { definition: { subtags: [".1", "tag1.2"] } },
409
+ tag2: { definition: { subtags: ["tag2.1", "tag2.2"] } },
410
+ tag3: { definition: { subtags: ["tag3.1", "tag3.2"] } },
411
+ "tag1.1": {},
412
+ "tag2.1": {},
413
+ "tag3.1": {},
414
+ };
415
+ const result = checkSupport(response, tagObject);
416
+ expect(result).toEqual(["tag1", "tag2", "tag3"]);
417
+ });
418
+ it("should return an empty array if tagObject is empty", () => {
419
+ const response = {
420
+ data: [
421
+ { name: "tag1", children: [{ name: "tag" }] },
422
+ { name: "tag2", children: [] },
423
+ { name: "tag3" },
424
+ ],
425
+ };
426
+ const result = checkSupport(response, {});
427
+ expect(result).toEqual([]);
428
+ });
429
+
430
+ it("should add childName to supportedList if it is a subtag of parentTag in eventContextTags", () => {
431
+ const response = { data: [{ name: "leaderboard", children: [{name: "person.userId"}]}]};
432
+ const tagObject = {};
433
+ const eventContextTags = [{ tagName: "leaderboard", subTags: ["userId"]}];
434
+ const isLiquidFlow = true;
435
+ const result = checkSupport(response, tagObject, eventContextTags, isLiquidFlow);
436
+ expect(result).toEqual( [ 'leaderboard', 'person.userId' ]);
437
+ });
438
+
439
+ it("should not add childName to supportedList which does not have dot in eventContextTags", () => {
440
+ // This case is unlikely to happen as we are not supporting tags without dot in eventContextTags
441
+ const response = { data: [{ name: "entryTrigger.lifetimePoints", children: [{name: "userId"}] }]};
442
+ const tagObject = {};
443
+ const eventContextTags = [{ tagName: "entryTrigger.lifetimePoints", children: [{name: "userId"}] }];
444
+ const isLiquidFlow = true;
445
+ const result = checkSupport(response, tagObject, eventContextTags, isLiquidFlow);
446
+ expect(result).toEqual( [ "entryTrigger.lifetimePoints" ]);
447
+ });
448
+
449
+ it("should add only parent tag to supportedList if isLiquidFlow false", () => {
450
+ const response = { data: [{ name: "leaderboard", children: [{name: "person.userId"}]}]};
451
+ const tagObject = {};
452
+ const eventContextTags = [{ tagName: "leaderboard", subTags: ["userId"]}];
453
+ const isLiquidFlow = false;
454
+ const result = checkSupport(response, tagObject, eventContextTags, isLiquidFlow);
455
+ expect(result).toEqual( [ 'leaderboard' ]);
456
+ });
457
+
458
+ it("test for checking loyalty tags in that are coming in forwardedTags", () => {
459
+ const response = { data: [{ name: "leaderboard", children: [{name: "person.userId"}]}]};
460
+ const tagObject = {};
461
+ const isLiquidFlow = true;
462
+ // forwardedTags contains tag hierarchy with parent tags and their subtags
463
+ // needed for loyalty email liquid tag resolution
464
+ const forwardedTags = {
465
+ leaderboard: {
466
+ name: "Leaderboard",
467
+ subtags: {
468
+ "person.userId": {
469
+ name: "User ID",
470
+ },
471
+ },
472
+ },
473
+ };
474
+ const result = checkSupport(response, tagObject, [], isLiquidFlow, forwardedTags);
475
+ expect(result).toEqual( [ 'leaderboard', 'person.userId' ]);
633
476
  });
634
477
  });
635
478
 
@@ -1205,10 +1048,6 @@ describe('getForwardedMapValues', () => {
1205
1048
  expect(getForwardedMapValues(input)).toEqual(expected);
1206
1049
  });
1207
1050
 
1208
- test('should return empty object when called with no argument (default param)', () => {
1209
- expect(getForwardedMapValues()).toEqual({});
1210
- });
1211
-
1212
1051
  test('should correctly process objects with subtags', () => {
1213
1052
  const input = {
1214
1053
  customer: {
@@ -1470,6 +1309,84 @@ describe('getForwardedMapValues', () => {
1470
1309
  });
1471
1310
  });
1472
1311
 
1312
+ describe('isInsideLiquidBlock', () => {
1313
+ it('returns true for index inside a single block', () => {
1314
+ const content = 'Hello {% assign foo = 1 %} World';
1315
+ // Index of 'a' in 'assign' inside the block
1316
+ const tagIndex = content.indexOf('assign');
1317
+ expect(isInsideLiquidBlock(content, tagIndex)).toBe(true);
1318
+ });
1319
+
1320
+ it('returns false for index outside any block', () => {
1321
+ const content = 'Hello {% assign foo = 1 %} World';
1322
+ // Index of 'H' in 'Hello'
1323
+ expect(isInsideLiquidBlock(content, 0)).toBe(false);
1324
+ // Index of 'W' in 'World'
1325
+ expect(isInsideLiquidBlock(content, content.indexOf('World'))).toBe(false);
1326
+ });
1327
+
1328
+ it('returns true for index at the start of a block', () => {
1329
+ const content = 'Hello {% assign foo = 1 %} World';
1330
+ // Index of '{' in '{%'
1331
+ const tagIndex = content.indexOf('{%');
1332
+ expect(isInsideLiquidBlock(content, tagIndex)).toBe(true);
1333
+ });
1334
+
1335
+ it('returns false for index at the end of a block (exclusive)', () => {
1336
+ const content = 'Hello {% assign foo = 1 %} World';
1337
+ // Index just after the closing '%}'
1338
+ const blockEnd = content.indexOf('%}') + 2;
1339
+ expect(isInsideLiquidBlock(content, blockEnd)).toBe(false);
1340
+ });
1341
+
1342
+ it('returns true for index inside the second of multiple blocks', () => {
1343
+ const content = 'A {% first %} B {% second %} C';
1344
+ const tagIndex = content.indexOf('second');
1345
+ expect(isInsideLiquidBlock(content, tagIndex)).toBe(true);
1346
+ });
1347
+
1348
+ it('returns false for index between blocks', () => {
1349
+ const content = 'A {% first %} B {% second %} C';
1350
+ // Index of 'B' (between blocks)
1351
+ const tagIndex = content.indexOf('B');
1352
+ expect(isInsideLiquidBlock(content, tagIndex)).toBe(false);
1353
+ });
1354
+
1355
+ it('returns false for empty string', () => {
1356
+ expect(isInsideLiquidBlock('', 0)).toBe(false);
1357
+ });
1358
+
1359
+ it('returns false if there are no blocks', () => {
1360
+ const content = 'Just some text with no blocks';
1361
+ expect(isInsideLiquidBlock(content, 5)).toBe(false);
1362
+ });
1363
+
1364
+ it('returns false for negative index', () => {
1365
+ const content = 'Hello {% assign foo = 1 %} World';
1366
+ expect(isInsideLiquidBlock(content, -1)).toBe(false);
1367
+ });
1368
+
1369
+ it('returns false for index beyond string length', () => {
1370
+ const content = 'Hello {% assign foo = 1 %} World';
1371
+ expect(isInsideLiquidBlock(content, 100)).toBe(false);
1372
+ });
1373
+
1374
+ it('works for nested-like blocks (not truly nested)', () => {
1375
+ const content = 'A {% outer {% inner %} outer %} B';
1376
+ // Index of 'inner' (should be inside the first block)
1377
+ const tagIndex = content.indexOf('inner');
1378
+ expect(isInsideLiquidBlock(content, tagIndex)).toBe(true);
1379
+ });
1380
+
1381
+ it('returns true for index at last char inside block', () => {
1382
+ const content = 'A {% foo %} B';
1383
+ // Index of last char inside block (just before %})
1384
+ const blockStart = content.indexOf('{%');
1385
+ const blockEnd = content.indexOf('%}');
1386
+ expect(isInsideLiquidBlock(content, blockEnd - 1)).toBe(true);
1387
+ });
1388
+ });
1389
+
1473
1390
  describe('containsBase64Images', () => {
1474
1391
  let mockCapNotification;
1475
1392
  let mockCallback;