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

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 (34) hide show
  1. package/constants/unified.js +1 -0
  2. package/package.json +1 -1
  3. package/utils/common.js +7 -0
  4. package/utils/commonUtils.js +79 -2
  5. package/utils/tagValidations.js +71 -92
  6. package/utils/tests/commonUtil.test.js +32 -79
  7. package/utils/tests/tagValidations.test.js +37 -18
  8. package/v2Components/FormBuilder/index.js +126 -47
  9. package/v2Containers/CreativesContainer/index.js +1 -0
  10. package/v2Containers/Email/index.js +5 -1
  11. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +18 -6
  12. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +106 -8
  13. package/v2Containers/FTP/index.js +51 -2
  14. package/v2Containers/InApp/index.js +23 -2
  15. package/v2Containers/InApp/tests/index.test.js +6 -17
  16. package/v2Containers/InappAdvance/index.js +24 -3
  17. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +24 -3
  18. package/v2Containers/Line/Container/Text/index.js +1 -0
  19. package/v2Containers/MobilePushNew/index.js +24 -4
  20. package/v2Containers/Rcs/index.js +37 -12
  21. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +18 -4
  22. package/v2Containers/SmsTrai/Create/index.scss +1 -1
  23. package/v2Containers/SmsTrai/Edit/index.js +17 -4
  24. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +0 -9
  25. package/v2Containers/Viber/index.js +1 -0
  26. package/v2Containers/Viber/index.scss +1 -1
  27. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
  28. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
  29. package/v2Containers/WebPush/Create/index.js +2 -2
  30. package/v2Containers/WebPush/Create/utils/validation.js +9 -18
  31. package/v2Containers/WebPush/Create/utils/validation.test.js +24 -0
  32. package/v2Containers/Whatsapp/index.js +17 -6
  33. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +624 -248
  34. package/v2Containers/Zalo/index.js +11 -3
@@ -43,6 +43,7 @@ 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';
46
47
  export const ENABLE_NEW_MPUSH = 'ENABLE_NEW_MPUSH';
47
48
  export const SUPPORT_CK_EDITOR = 'SUPPORT_CK_EDITOR';
48
49
  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.285-alpha.0",
4
+ "version": "8.0.285",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/utils/common.js CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  BADGES_ISSUE,
23
23
  ENABLE_WECHAT,
24
24
  ENABLE_WEBPUSH,
25
+ LIQUID_SUPPORT,
25
26
  SUPPORT_CK_EDITOR,
26
27
  ENABLE_NEW_MPUSH
27
28
  } from '../constants/unified';
@@ -90,6 +91,12 @@ export const hasPromoFeature = Auth.hasFeatureAccess.bind(
90
91
  null,
91
92
  PROMO_ENGINE,
92
93
  );
94
+
95
+ export const hasLiquidSupportFeature = Auth.hasFeatureAccess.bind(
96
+ null,
97
+ LIQUID_SUPPORT,
98
+ );
99
+
93
100
  export const hasSupportCKEditor = Auth.hasFeatureAccess.bind(
94
101
  null,
95
102
  SUPPORT_CK_EDITOR,
@@ -16,6 +16,7 @@ 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";
19
20
  import { SMS_TRAI_VAR } from '../v2Containers/SmsTrai/Edit/constants';
20
21
  export const apiMessageFormatHandler = (id, fallback) => (
21
22
  <FormattedMessage id={id} defaultMessage={fallback} />
@@ -145,6 +146,7 @@ export const validateLiquidTemplateContent = async (
145
146
  isLiquidFlow,
146
147
  forwardedTags = {},
147
148
  tabType,
149
+ skipTags = defaultSkipTags,
148
150
  } = options;
149
151
  const emptyBodyError = formatMessage(messages?.emailBodyEmptyError);
150
152
  const somethingWrongMsg = formatMessage(messages?.somethingWentWrong);
@@ -202,6 +204,81 @@ export const validateLiquidTemplateContent = async (
202
204
  });
203
205
  return false;
204
206
  }
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
205
282
  onSuccess(content, tabType);
206
283
  return true;
207
284
  };
@@ -288,8 +365,8 @@ export const _validatePlatformSpecificContent = async (
288
365
  /**
289
366
  * Validate Mobile Push content for both Android and iOS tabs
290
367
  * @param {object} formData - Form data containing Android and iOS content
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
368
+ * @param {object} options - Options for validation
369
+ * @returns {Promise} - Promise that resolves when validation completes
293
370
  */
294
371
  export const validateMobilePushContent = async (formData, options) => {
295
372
  const {
@@ -177,118 +177,97 @@ export const isInsideLiquidBlock = (content, tagIndex) => {
177
177
  };
178
178
 
179
179
  /**
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 }}
180
+ * Validates the tags based on the provided parameters.
181
+ * @param {Object} params - The parameters for tag validation.
191
182
  */
192
- export const validateTagsCore = ({
193
- contentForBraceCheck,
194
- contentForUnsubscribeScan,
195
- tags,
196
- currentModule,
183
+ export const validateTags = ({
184
+ content,
185
+ tagsParam,
186
+ injectedTagsParams,
187
+ location,
188
+ tagModule,
189
+ eventContextTags,
197
190
  isFullMode,
198
- initialMissingTags = null,
199
- skipTagsFn = skipTags,
200
- includeIsContentEmpty = false,
201
191
  }) => {
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
+ }
202
198
  const response = {
203
199
  valid: true,
204
200
  missingTags: [],
205
201
  unsupportedTags: [],
206
202
  isBraceError: false,
207
- ...(includeIsContentEmpty && { isContentEmpty: false }),
208
203
  };
209
-
204
+ // Mandatory-tags check: only when we have a tags list and are in library mode
210
205
  if (tags && tags.length && !isFullMode) {
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
- });
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
+ }
227
218
  }
228
219
  });
229
- } else {
230
- response.missingTags = [...initialMissingTags];
231
- }
232
-
233
- const regex = /{{([^}]+)}}/g;
234
- let match = regex.exec(contentForUnsubscribeScan);
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);
235
226
  while (match !== null) {
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);
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;
234
+ }
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
+ }
242
245
  }
243
246
  }
244
- match = regex.exec(contentForUnsubscribeScan);
245
- }
246
-
247
- if (response.missingTags.length > 0) {
248
- response.valid = false;
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
+ }
249
262
  }
250
263
  }
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;
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;
258
268
  return response;
259
269
  };
260
270
 
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
-
292
271
  /**
293
272
  * Checks if the given tag is supported based on the injected tags.
294
273
  * @param {string} checkingTag - The tag to check.
@@ -27,11 +27,6 @@ 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
- );
35
30
  });
36
31
 
37
32
  it("calls onError for empty content", async () => {
@@ -48,7 +43,7 @@ describe("validateLiquidTemplateContent", () => {
48
43
  eventContextTags
49
44
  });
50
45
  expect(onError).toHaveBeenCalledWith({
51
- standardErrors: ["empty"],
46
+ standardErrors: [undefined],
52
47
  liquidErrors: [],
53
48
  tabType: undefined
54
49
  });
@@ -91,9 +86,9 @@ describe("validateLiquidTemplateContent", () => {
91
86
  expect(onSuccess).not.toHaveBeenCalled();
92
87
  });
93
88
 
94
- it("calls onSuccess when API returns no errors and extracted tags (extracted tags are not validated)", async () => {
89
+ it("calls onError for unsupported tags", async () => {
95
90
  const getLiquidTags = jest.fn((content, cb) =>
96
- cb({ askAiraResponse: { errors: [], data: [{ name: "foo" }] }, isError: false })
91
+ cb({ askAiraResponse: { errors: [], data: [{ name: "baz" }] }, isError: false })
97
92
  );
98
93
  await validateLiquidTemplateContent("foo", {
99
94
  getLiquidTags,
@@ -104,11 +99,15 @@ describe("validateLiquidTemplateContent", () => {
104
99
  tagLookupMap,
105
100
  eventContextTags
106
101
  });
107
- expect(onSuccess).toHaveBeenCalledWith("foo", undefined);
108
- expect(onError).not.toHaveBeenCalled();
102
+ expect(onError).toHaveBeenCalledWith({
103
+ standardErrors: [],
104
+ liquidErrors: [undefined],
105
+ tabType: undefined
106
+ });
107
+ expect(onSuccess).not.toHaveBeenCalled();
109
108
  });
110
109
 
111
- it("calls onSuccess for valid content when API returns multiple extracted tags but no errors", async () => {
110
+ it("calls onSuccess for valid content", async () => {
112
111
  const getLiquidTags = jest.fn((content, cb) =>
113
112
  cb({ askAiraResponse: { errors: [], data: [{ name: "foo" }, { name: "bar" }] }, isError: false })
114
113
  );
@@ -298,7 +297,7 @@ describe("validateLiquidTemplateContent", () => {
298
297
  expect(onSuccess).not.toHaveBeenCalled();
299
298
  });
300
299
 
301
- it("calls onSuccess when API returns extracted tags but no errors (extracted tags are not validated)", async () => {
300
+ it("should skip tags that appear in {% %} syntax (like order.items in for loop)", async () => {
302
301
  const content = '{% for item in order.items %} Hello {% endfor %}';
303
302
  const getLiquidTags = jest.fn((content, cb) =>
304
303
  cb({ askAiraResponse: { errors: [], data: [{ name: "order.items" }] }, isError: false })
@@ -312,11 +311,12 @@ describe("validateLiquidTemplateContent", () => {
312
311
  tagLookupMap,
313
312
  eventContextTags
314
313
  });
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("calls onSuccess when API returns extracted tags in {% %} blocks but no errors", async () => {
319
+ it("should skip tags that appear only inside {% %} blocks (like item.name in for loop)", 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,11 +330,12 @@ 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
333
334
  expect(onSuccess).toHaveBeenCalledWith(content, undefined);
334
335
  expect(onError).not.toHaveBeenCalled();
335
336
  });
336
337
 
337
- it("calls onSuccess when API returns extracted tags not in content but no errors", async () => {
338
+ it("should validate tags that don't appear in content but are extracted by API", async () => {
338
339
  const content = 'Some content without the tag';
339
340
  const getLiquidTags = jest.fn((content, cb) =>
340
341
  cb({ askAiraResponse: { errors: [], data: [{ name: "extractedTag" }] }, isError: false })
@@ -348,11 +349,16 @@ describe("validateLiquidTemplateContent", () => {
348
349
  tagLookupMap,
349
350
  eventContextTags
350
351
  });
351
- expect(onSuccess).toHaveBeenCalledWith(content, undefined);
352
- expect(onError).not.toHaveBeenCalled();
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();
353
359
  });
354
360
 
355
- it("calls onSuccess when API returns tags outside {% %} but no errors", async () => {
361
+ it("should validate tags that appear outside {% %} blocks", async () => {
356
362
  const content = '{{ outsideTag }} {% for item in order.items %} {{ item.name }} {% endfor %}';
357
363
  const getLiquidTags = jest.fn((content, cb) =>
358
364
  cb({ askAiraResponse: { errors: [], data: [{ name: "outsideTag" }, { name: "item.name" }] }, isError: false })
@@ -366,71 +372,17 @@ describe("validateLiquidTemplateContent", () => {
366
372
  tagLookupMap,
367
373
  eventContextTags
368
374
  });
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
386
- });
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
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
428
381
  });
429
- expect(onSuccess).toHaveBeenCalledWith(content, undefined);
430
- expect(onError).not.toHaveBeenCalled();
382
+ expect(onSuccess).not.toHaveBeenCalled();
431
383
  });
432
384
 
433
- it("calls onSuccess when API returns tags with dots in {% %} syntax but no errors", async () => {
385
+ it("should skip tags with dots that appear in {% %} syntax", async () => {
434
386
  const content = '{% assign myVar = order.items %} Some text';
435
387
  const getLiquidTags = jest.fn((content, cb) =>
436
388
  cb({ askAiraResponse: { errors: [], data: [{ name: "order.items" }] }, isError: false })
@@ -444,6 +396,7 @@ describe("validateLiquidTemplateContent", () => {
444
396
  tagLookupMap,
445
397
  eventContextTags
446
398
  });
399
+ // order.items appears in {% %} syntax, so it should be skipped
447
400
  expect(onSuccess).toHaveBeenCalledWith(content, undefined);
448
401
  expect(onError).not.toHaveBeenCalled();
449
402
  });