@capillarytech/creatives-library 8.0.284 → 8.0.285-alpha.0

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 (29) hide show
  1. package/constants/unified.js +0 -1
  2. package/package.json +1 -1
  3. package/utils/common.js +0 -7
  4. package/utils/commonUtils.js +2 -79
  5. package/utils/tagValidations.js +92 -71
  6. package/utils/tests/commonUtil.test.js +79 -32
  7. package/utils/tests/tagValidations.test.js +18 -37
  8. package/v2Components/FormBuilder/index.js +47 -126
  9. package/v2Containers/CreativesContainer/index.js +0 -1
  10. package/v2Containers/Email/index.js +1 -5
  11. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +6 -18
  12. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +8 -106
  13. package/v2Containers/FTP/index.js +2 -51
  14. package/v2Containers/InApp/index.js +2 -23
  15. package/v2Containers/InApp/tests/index.test.js +17 -6
  16. package/v2Containers/InappAdvance/index.js +3 -24
  17. package/v2Containers/Line/Container/Text/index.js +0 -1
  18. package/v2Containers/MobilePushNew/index.js +4 -24
  19. package/v2Containers/Rcs/index.js +12 -37
  20. package/v2Containers/SmsTrai/Edit/index.js +4 -17
  21. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +9 -0
  22. package/v2Containers/Viber/index.js +0 -1
  23. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +1 -3
  24. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +0 -7
  25. package/v2Containers/WebPush/Create/index.js +2 -2
  26. package/v2Containers/WebPush/Create/utils/validation.js +18 -9
  27. package/v2Containers/WebPush/Create/utils/validation.test.js +0 -24
  28. package/v2Containers/Whatsapp/index.js +6 -17
  29. package/v2Containers/Zalo/index.js +3 -11
@@ -43,7 +43,6 @@ export const HOSPITALITY_BASED_SCOPE = 'HOSPITALITY_BASED_SCOPE';
43
43
  export const REGISTRATION_CUSTOM_FIELD = 'Registration custom fields';
44
44
  export const GIFT_CARDS = 'GIFT_CARDS';
45
45
  export const PROMO_ENGINE = 'PROMO_ENGINE';
46
- export const LIQUID_SUPPORT = 'ENABLE_LIQUID_SUPPORT';
47
46
  export const ENABLE_NEW_MPUSH = 'ENABLE_NEW_MPUSH';
48
47
  export const SUPPORT_CK_EDITOR = 'SUPPORT_CK_EDITOR';
49
48
  export const CUSTOM_TAG = 'CustomTagMessage';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.284",
4
+ "version": "8.0.285-alpha.0",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/utils/common.js CHANGED
@@ -22,7 +22,6 @@ import {
22
22
  BADGES_ISSUE,
23
23
  ENABLE_WECHAT,
24
24
  ENABLE_WEBPUSH,
25
- LIQUID_SUPPORT,
26
25
  SUPPORT_CK_EDITOR,
27
26
  ENABLE_NEW_MPUSH
28
27
  } from '../constants/unified';
@@ -91,12 +90,6 @@ export const hasPromoFeature = Auth.hasFeatureAccess.bind(
91
90
  null,
92
91
  PROMO_ENGINE,
93
92
  );
94
-
95
- export const hasLiquidSupportFeature = Auth.hasFeatureAccess.bind(
96
- null,
97
- LIQUID_SUPPORT,
98
- );
99
-
100
93
  export const hasSupportCKEditor = Auth.hasFeatureAccess.bind(
101
94
  null,
102
95
  SUPPORT_CK_EDITOR,
@@ -16,7 +16,6 @@ import {
16
16
  IOS,
17
17
  } from "../v2Containers/CreativesContainer/constants";
18
18
  import { GLOBAL_CONVERT_OPTIONS } from "../v2Components/FormBuilder/constants";
19
- import { checkSupport, extractNames, skipTags as defaultSkipTags, isInsideLiquidBlock } from "./tagValidations";
20
19
  import { SMS_TRAI_VAR } from '../v2Containers/SmsTrai/Edit/constants';
21
20
  export const apiMessageFormatHandler = (id, fallback) => (
22
21
  <FormattedMessage id={id} defaultMessage={fallback} />
@@ -146,7 +145,6 @@ export const validateLiquidTemplateContent = async (
146
145
  isLiquidFlow,
147
146
  forwardedTags = {},
148
147
  tabType,
149
- skipTags = defaultSkipTags,
150
148
  } = options;
151
149
  const emptyBodyError = formatMessage(messages?.emailBodyEmptyError);
152
150
  const somethingWrongMsg = formatMessage(messages?.somethingWentWrong);
@@ -204,81 +202,6 @@ export const validateLiquidTemplateContent = async (
204
202
  });
205
203
  return false;
206
204
  }
207
- // Extract and validate tags
208
- const extractedLiquidTags = extractNames(result?.data || []);
209
- // Get supported tags
210
- const supportedLiquidTags = checkSupport(
211
- result,
212
- tagLookupMap,
213
- eventContextTags,
214
- isLiquidFlow,
215
- forwardedTags
216
- );
217
- // Helper function to check if a tag appears only inside {% %} blocks
218
- const isTagOnlyInsideLiquidBlocks = (tagName) => {
219
- // Escape special regex characters in tag name, including dots
220
- // Dots need to be escaped to match literally (item.name should match item.name, not item or name)
221
- const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
222
-
223
- // First, check if tag appears in {% %} syntax itself (like "order.items" in "{% for item in order.items %}")
224
- // This is a tag used in Liquid logic, not output, so it should always be skipped
225
- // Match the tag name as a whole (with dots escaped), optionally surrounded by word boundaries or non-word chars
226
- // For tags with dots, we match them directly; for simple tags, we use word boundaries
227
- const hasDot = tagName.includes('.');
228
- const liquidSyntaxPattern = hasDot
229
- ? `{%[^%]*${escapedTagName}[^%]*%}`
230
- : `{%[^%]*\\b${escapedTagName}\\b[^%]*%}`;
231
- const liquidSyntaxRegex = new RegExp(liquidSyntaxPattern, 'g');
232
- const liquidSyntaxMatches = Array.from(content.matchAll(liquidSyntaxRegex));
233
-
234
- // Find all occurrences of {{tagName}} in the content (output tags)
235
- // Match patterns like: {{tagName}}, {{ tagName }}, {{tagName }}, {{ tagName}}
236
- // Use non-word-boundary approach for tags with dots (item.name should match item.name, not item or name separately)
237
- const outputTagRegex = new RegExp(`\\{\\{\\s*${escapedTagName}\\s*\\}\\}`, 'g');
238
- const outputTagMatches = Array.from(content.matchAll(outputTagRegex));
239
- const outputTagPositions = outputTagMatches.map((match) => match.index);
240
-
241
- // If tag appears in {% %} syntax, skip validation
242
- if (liquidSyntaxMatches.length > 0) {
243
- return true;
244
- }
245
-
246
- // If no output tag matches found, don't skip validation
247
- // The tag was extracted by the API, so it exists somewhere and should be validated
248
- if (outputTagPositions.length === 0) {
249
- return false;
250
- }
251
-
252
- // Check if all output tag occurrences are inside {% %} blocks
253
- // We check the position of {{ to see if it's inside a block
254
- // Only skip validation if ALL occurrences are inside blocks
255
- return outputTagPositions.every((tagIndex) => {
256
- if (tagIndex === undefined || tagIndex === null) {
257
- return false;
258
- }
259
- return isInsideLiquidBlock(content, tagIndex);
260
- });
261
- };
262
-
263
- // Find unsupported tags, excluding those that are only inside {% %} blocks
264
- const unsupportedLiquidTags = extractedLiquidTags?.filter(
265
- (tag) => !supportedLiquidTags?.includes(tag)
266
- && !skipTags(tag)
267
- && !isTagOnlyInsideLiquidBlocks(tag)
268
- );
269
- // Handle unsupported tags
270
- if (unsupportedLiquidTags?.length > 0) {
271
- const errorMsg = formatMessage(messages.unsupportedTagsValidationError, {
272
- unsupportedTags: unsupportedLiquidTags.join(", "),
273
- });
274
- onError({
275
- standardErrors: [],
276
- liquidErrors: [errorMsg],
277
- tabType,
278
- });
279
- return false;
280
- }
281
- // All validations passed
282
205
  onSuccess(content, tabType);
283
206
  return true;
284
207
  };
@@ -365,8 +288,8 @@ export const _validatePlatformSpecificContent = async (
365
288
  /**
366
289
  * Validate Mobile Push content for both Android and iOS tabs
367
290
  * @param {object} formData - Form data containing Android and iOS content
368
- * @param {object} options - Options for validation
369
- * @returns {Promise} - Promise that resolves when validation completes
291
+ * @param {object} options - Options for validation (currentTab, onError, onSuccess, getLiquidTags, formatMessage, messages, tagLookupMap, eventContextTags, isLiquidFlow, forwardedTags, singleTab). skipTags and extractNames are no longer accepted.
292
+ * @returns {Promise<boolean>} - Promise that resolves to true when validation succeeds, false otherwise
370
293
  */
371
294
  export const validateMobilePushContent = async (formData, options) => {
372
295
  const {
@@ -177,97 +177,118 @@ export const isInsideLiquidBlock = (content, tagIndex) => {
177
177
  };
178
178
 
179
179
  /**
180
- * Validates the tags based on the provided parameters.
181
- * @param {Object} params - The parameters for tag validation.
180
+ * Shared core for tag validation. Used by validateTags (tagValidations) and FormBuilder.validateTags.
181
+ * @param {Object} params
182
+ * @param {string} params.contentForBraceCheck - String to run validateIfTagClosed on.
183
+ * @param {string} params.contentForUnsubscribeScan - String to scan for {{...}} unsubscribe variants.
184
+ * @param {Array} [params.tags] - Tag definitions (for definition-based missing tags).
185
+ * @param {string} [params.currentModule] - Module context (e.g. 'default', 'outbound').
186
+ * @param {boolean} [params.isFullMode] - If true, skip unsubscribe checks.
187
+ * @param {Array} [params.initialMissingTags=null] - If set, use instead of computing from definitions.
188
+ * @param {function} [params.skipTagsFn=skipTags] - skipTags implementation.
189
+ * @param {boolean} [params.includeIsContentEmpty=false] - If true, response includes isContentEmpty: false.
190
+ * @returns {{ valid: boolean, missingTags: string[], unsupportedTags: string[], isBraceError: boolean }}
182
191
  */
183
- export const validateTags = ({
184
- content,
185
- tagsParam,
186
- injectedTagsParams,
187
- location,
188
- tagModule,
189
- eventContextTags,
192
+ export const validateTagsCore = ({
193
+ contentForBraceCheck,
194
+ contentForUnsubscribeScan,
195
+ tags,
196
+ currentModule,
190
197
  isFullMode,
198
+ initialMissingTags = null,
199
+ skipTagsFn = skipTags,
200
+ includeIsContentEmpty = false,
191
201
  }) => {
192
- const tags = tagsParam;
193
- const injectedTags = transformInjectedTags(injectedTagsParams);
194
- let currentModule = location?.query?.module ? location?.query?.module : DEFAULT;
195
- if (tagModule) {
196
- currentModule = tagModule;
197
- }
198
202
  const response = {
199
203
  valid: true,
200
204
  missingTags: [],
201
205
  unsupportedTags: [],
202
206
  isBraceError: false,
207
+ ...(includeIsContentEmpty && { isContentEmpty: false }),
203
208
  };
204
- // Mandatory-tags check: only when we have a tags list and are in library mode
209
+
205
210
  if (tags && tags.length && !isFullMode) {
206
- lodashForEach(tags, ({
207
- definition: {
208
- supportedModules,
209
- value,
210
- },
211
- }) => {
212
- lodashForEach(supportedModules, (module) => {
213
- if (module.mandatory && (currentModule === module.context)) {
214
- if (content.indexOf(`{{${value}}}`) === -1) {
215
- response.valid = false;
216
- response.missingTags.push(value);
217
- }
211
+ if (initialMissingTags == null) {
212
+ // Definition-based: same as original tagValidations (when caller does not pass initialMissingTags)
213
+ lodashForEach(tags, ({
214
+ definition: {
215
+ supportedModules,
216
+ value,
217
+ },
218
+ }) => {
219
+ if (value === 'unsubscribe') {
220
+ lodashForEach(supportedModules, (module) => {
221
+ if (module.mandatory && (currentModule === module.context)) {
222
+ if (contentForUnsubscribeScan.indexOf(`{{${value}}}`) === -1) {
223
+ response.missingTags.push(value);
224
+ }
225
+ }
226
+ });
218
227
  }
219
228
  });
220
- });
221
- }
222
- // In library mode, always scan content for {{...}} and flag unsupported tags (even when tags list is empty)
223
- if (!isFullMode && content) {
224
- const regex = /{{[(A-Z\w+(\s\w+)*$\(\)@!#$%^&*~.,/\\]+}}/g;
225
- let match = regex.exec(content);
229
+ } else {
230
+ response.missingTags = [...initialMissingTags];
231
+ }
232
+
233
+ const regex = /{{([^}]+)}}/g;
234
+ let match = regex.exec(contentForUnsubscribeScan);
226
235
  while (match !== null) {
227
- const tagValue = match[0].substring(indexOfEnd(match[0], '{{'), match[0].indexOf('}}'));
228
- const tagIndex = match?.index;
229
- match = regex.exec(content);
230
- let ifSupported = false;
231
- lodashForEach(tags || [], (tag) => {
232
- if (tag?.definition?.value === tagValue) {
233
- ifSupported = true;
236
+ const tagValue = match[1].trim();
237
+ const ifSkipped = skipTagsFn(tagValue);
238
+ if (ifSkipped && tagValue.indexOf('unsubscribe') !== -1) {
239
+ const missingTagIndex = response.missingTags.indexOf('unsubscribe');
240
+ if (missingTagIndex !== -1) {
241
+ response.missingTags.splice(missingTagIndex, 1);
234
242
  }
235
- });
236
- const ifSkipped = skipTags(tagValue);
237
- if (ifSkipped) {
238
- ifSupported = true;
239
- const isUnsubscribeSkipped = tagValue.indexOf("unsubscribe") !== -1;
240
- if (isUnsubscribeSkipped) {
241
- const missingTagIndex = response.missingTags.indexOf("unsubscribe");
242
- if (missingTagIndex !== -1) {
243
- response.missingTags.splice(missingTagIndex, 1);
244
- }
245
- }
246
- }
247
- // Journey Event Context Tags support
248
- eventContextTags?.forEach((tag) => {
249
- if (tagValue === tag?.tagName) {
250
- ifSupported = true;
251
- }
252
- });
253
- ifSupported = ifSupported || checkIfSupportedTag(tagValue, injectedTags);
254
- // Only add to unsupportedTags if not inside a {% ... %} block and does not contain a dot
255
- if (!ifSupported && !isInsideLiquidBlock(content, tagIndex) && tagValue?.indexOf('.') === -1) {
256
- response.unsupportedTags.push(tagValue);
257
- response.valid = false;
258
- }
259
- if (response.unsupportedTags.length === 0 && response.missingTags.length === 0) {
260
- response.valid = true;
261
243
  }
244
+ match = regex.exec(contentForUnsubscribeScan);
262
245
  }
246
+
247
+ if (response.missingTags.length > 0) {
248
+ response.valid = false;
249
+ }
250
+ }
251
+
252
+ response.isBraceError = !validateIfTagClosed(contentForBraceCheck);
253
+ if (response.isBraceError) {
254
+ response.valid = false;
255
+ } else if (response.missingTags.length > 0) {
256
+ response.valid = false;
263
257
  }
264
- response.isBraceError = !validateIfTagClosed(content);
265
- // response should not be valid if there is unbalanced bracket error, as
266
- // validations (eg button) are handled on valid property coming from the response.
267
- response.isBraceError ? response.valid = false : response.valid = true;
268
258
  return response;
269
259
  };
270
260
 
261
+ /**
262
+ * Validates the tags based on the provided parameters.
263
+ * @param {Object} params - The parameters for tag validation.
264
+ * @param {string} params.content - Content to validate.
265
+ * @param {Array} [params.tagsParam] - Tag definitions.
266
+ * @param {Object} [params.location] - Location with query.module.
267
+ * @param {string} [params.tagModule] - Override for current module context.
268
+ * @param {boolean} [params.isFullMode] - If true, skip unsubscribe checks.
269
+ * @returns {{ valid: boolean, missingTags: string[], unsupportedTags: string[], isBraceError: boolean }}
270
+ */
271
+ export const validateTags = ({
272
+ content,
273
+ tagsParam,
274
+ location,
275
+ tagModule,
276
+ isFullMode,
277
+ }) => {
278
+ const tags = tagsParam;
279
+ let currentModule = location?.query?.module ? location?.query?.module : DEFAULT;
280
+ if (tagModule) {
281
+ currentModule = tagModule;
282
+ }
283
+ return validateTagsCore({
284
+ contentForBraceCheck: content,
285
+ contentForUnsubscribeScan: content,
286
+ tags,
287
+ currentModule,
288
+ isFullMode,
289
+ });
290
+ };
291
+
271
292
  /**
272
293
  * Checks if the given tag is supported based on the injected tags.
273
294
  * @param {string} checkingTag - The tag to check.
@@ -27,6 +27,11 @@ describe("validateLiquidTemplateContent", () => {
27
27
 
28
28
  beforeEach(() => {
29
29
  jest.clearAllMocks();
30
+ formatMessage.mockImplementation((msg, vars) =>
31
+ vars && vars.unsupportedTags != null
32
+ ? `${msg?.id}:${vars.unsupportedTags}`
33
+ : (msg?.id ?? "unsupported")
34
+ );
30
35
  });
31
36
 
32
37
  it("calls onError for empty content", async () => {
@@ -43,7 +48,7 @@ describe("validateLiquidTemplateContent", () => {
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
  });
@@ -86,9 +91,9 @@ describe("validateLiquidTemplateContent", () => {
86
91
  expect(onSuccess).not.toHaveBeenCalled();
87
92
  });
88
93
 
89
- it("calls onError for unsupported tags", async () => {
94
+ it("calls onSuccess when API returns no errors and extracted tags (extracted tags are not validated)", async () => {
90
95
  const getLiquidTags = jest.fn((content, cb) =>
91
- cb({ askAiraResponse: { errors: [], data: [{ name: "baz" }] }, isError: false })
96
+ cb({ askAiraResponse: { errors: [], data: [{ name: "foo" }] }, isError: false })
92
97
  );
93
98
  await validateLiquidTemplateContent("foo", {
94
99
  getLiquidTags,
@@ -99,15 +104,11 @@ describe("validateLiquidTemplateContent", () => {
99
104
  tagLookupMap,
100
105
  eventContextTags
101
106
  });
102
- expect(onError).toHaveBeenCalledWith({
103
- standardErrors: [],
104
- liquidErrors: [undefined],
105
- tabType: undefined
106
- });
107
- expect(onSuccess).not.toHaveBeenCalled();
107
+ expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
108
+ expect(onError).not.toHaveBeenCalled();
108
109
  });
109
110
 
110
- it("calls onSuccess for valid content", async () => {
111
+ it("calls onSuccess for valid content when API returns multiple extracted tags but no errors", async () => {
111
112
  const getLiquidTags = jest.fn((content, cb) =>
112
113
  cb({ askAiraResponse: { errors: [], data: [{ name: "foo" }, { name: "bar" }] }, isError: false })
113
114
  );
@@ -297,7 +298,7 @@ describe("validateLiquidTemplateContent", () => {
297
298
  expect(onSuccess).not.toHaveBeenCalled();
298
299
  });
299
300
 
300
- it("should skip tags that appear in {% %} syntax (like order.items in for loop)", async () => {
301
+ it("calls onSuccess when API returns extracted tags but no errors (extracted tags are not validated)", async () => {
301
302
  const content = '{% for item in order.items %} Hello {% endfor %}';
302
303
  const getLiquidTags = jest.fn((content, cb) =>
303
304
  cb({ askAiraResponse: { errors: [], data: [{ name: "order.items" }] }, isError: false })
@@ -311,12 +312,11 @@ describe("validateLiquidTemplateContent", () => {
311
312
  tagLookupMap,
312
313
  eventContextTags
313
314
  });
314
- // order.items appears in {% %} syntax, so it should be skipped and validation should pass
315
315
  expect(onSuccess).toHaveBeenCalledWith(content, undefined);
316
316
  expect(onError).not.toHaveBeenCalled();
317
317
  });
318
318
 
319
- it("should skip tags that appear only inside {% %} blocks (like item.name in for loop)", async () => {
319
+ it("calls onSuccess when API returns extracted tags in {% %} blocks but no errors", async () => {
320
320
  const content = '{% for item in order.items %} {{ item.name }} - {{ item.quantity }} {% endfor %}';
321
321
  const getLiquidTags = jest.fn((content, cb) =>
322
322
  cb({ askAiraResponse: { errors: [], data: [{ name: "item.name" }, { name: "item.quantity" }] }, isError: false })
@@ -330,12 +330,11 @@ describe("validateLiquidTemplateContent", () => {
330
330
  tagLookupMap,
331
331
  eventContextTags
332
332
  });
333
- // item.name and item.quantity appear inside {% for %} block, so they should be skipped
334
333
  expect(onSuccess).toHaveBeenCalledWith(content, undefined);
335
334
  expect(onError).not.toHaveBeenCalled();
336
335
  });
337
336
 
338
- it("should validate tags that don't appear in content but are extracted by API", async () => {
337
+ it("calls onSuccess when API returns extracted tags not in content but no errors", async () => {
339
338
  const content = 'Some content without the tag';
340
339
  const getLiquidTags = jest.fn((content, cb) =>
341
340
  cb({ askAiraResponse: { errors: [], data: [{ name: "extractedTag" }] }, isError: false })
@@ -349,16 +348,11 @@ describe("validateLiquidTemplateContent", () => {
349
348
  tagLookupMap,
350
349
  eventContextTags
351
350
  });
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();
351
+ expect(onSuccess).toHaveBeenCalledWith(content, undefined);
352
+ expect(onError).not.toHaveBeenCalled();
359
353
  });
360
354
 
361
- it("should validate tags that appear outside {% %} blocks", async () => {
355
+ it("calls onSuccess when API returns tags outside {% %} but no errors", async () => {
362
356
  const content = '{{ outsideTag }} {% for item in order.items %} {{ item.name }} {% endfor %}';
363
357
  const getLiquidTags = jest.fn((content, cb) =>
364
358
  cb({ askAiraResponse: { errors: [], data: [{ name: "outsideTag" }, { name: "item.name" }] }, isError: false })
@@ -372,17 +366,71 @@ describe("validateLiquidTemplateContent", () => {
372
366
  tagLookupMap,
373
367
  eventContextTags
374
368
  });
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
369
+ expect(onSuccess).toHaveBeenCalledWith(content, undefined);
370
+ expect(onError).not.toHaveBeenCalled();
371
+ });
372
+
373
+ it("calls onSuccess when API returns tag in {{ }} and no errors", async () => {
374
+ const content = 'Hello {{ unsupportedTag }} world';
375
+ const getLiquidTags = jest.fn((content, cb) =>
376
+ cb({ askAiraResponse: { errors: [], data: [{ name: "unsupportedTag" }] }, isError: false })
377
+ );
378
+ await validateLiquidTemplateContent(content, {
379
+ getLiquidTags,
380
+ formatMessage,
381
+ messages,
382
+ onError,
383
+ onSuccess,
384
+ tagLookupMap,
385
+ eventContextTags
381
386
  });
382
- expect(onSuccess).not.toHaveBeenCalled();
387
+ expect(onSuccess).toHaveBeenCalledWith(content, undefined);
388
+ expect(onError).not.toHaveBeenCalled();
389
+ });
390
+
391
+ it("calls onSuccess when API returns tag with spacing variants and no errors", async () => {
392
+ const content = '{{ tag}} and {{tag }} and {{ tag }}';
393
+ const getLiquidTags = jest.fn((content, cb) =>
394
+ cb({
395
+ askAiraResponse: {
396
+ errors: [],
397
+ data: [{ name: "tag" }]
398
+ },
399
+ isError: false
400
+ })
401
+ );
402
+ await validateLiquidTemplateContent(content, {
403
+ getLiquidTags,
404
+ formatMessage,
405
+ messages,
406
+ onError,
407
+ onSuccess,
408
+ tagLookupMap,
409
+ eventContextTags
410
+ });
411
+ expect(onSuccess).toHaveBeenCalledWith(content, undefined);
412
+ expect(onError).not.toHaveBeenCalled();
413
+ });
414
+
415
+ it("calls onSuccess when API returns tag inside {% %} blocks but no errors", async () => {
416
+ const content = '{% for x in some.unsupported %} {{ x }} {% endfor %}';
417
+ const getLiquidTags = jest.fn((content, cb) =>
418
+ cb({ askAiraResponse: { errors: [], data: [{ name: "some.unsupported" }] }, isError: false })
419
+ );
420
+ await validateLiquidTemplateContent(content, {
421
+ getLiquidTags,
422
+ formatMessage,
423
+ messages,
424
+ onError,
425
+ onSuccess,
426
+ tagLookupMap,
427
+ eventContextTags
428
+ });
429
+ expect(onSuccess).toHaveBeenCalledWith(content, undefined);
430
+ expect(onError).not.toHaveBeenCalled();
383
431
  });
384
432
 
385
- it("should skip tags with dots that appear in {% %} syntax", async () => {
433
+ it("calls onSuccess when API returns tags with dots in {% %} syntax but no errors", async () => {
386
434
  const content = '{% assign myVar = order.items %} Some text';
387
435
  const getLiquidTags = jest.fn((content, cb) =>
388
436
  cb({ askAiraResponse: { errors: [], data: [{ name: "order.items" }] }, isError: false })
@@ -396,7 +444,6 @@ describe("validateLiquidTemplateContent", () => {
396
444
  tagLookupMap,
397
445
  eventContextTags
398
446
  });
399
- // order.items appears in {% %} syntax, so it should be skipped
400
447
  expect(onSuccess).toHaveBeenCalledWith(content, undefined);
401
448
  expect(onError).not.toHaveBeenCalled();
402
449
  });