@capillarytech/creatives-library 8.0.302 → 8.0.303

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.302",
4
+ "version": "8.0.303",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -1331,7 +1331,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1331
1331
  }
1332
1332
  onSubmitWrapper = (args) => {
1333
1333
  const {singleTab = null} = args || {};
1334
- if (this.liquidFlow()) {
1334
+ if (this.liquidFlow() && !this.props?.isFullMode) {
1335
1335
  // For MPUSH, we need to validate both Android and iOS content separately
1336
1336
  if (this.props.channel === MOBILE_PUSH || this.props?.schema?.channel?.toUpperCase() === MOBILE_PUSH) {
1337
1337
  this.validateFormBuilderMPush(this.state.formData, singleTab);
@@ -294,6 +294,7 @@ const HTMLEditor = forwardRef(({
294
294
  enableSanitization: true,
295
295
  securityLevel: 'standard',
296
296
  apiValidationErrors, // Pass API validation errors to merge with client-side validation
297
+ isFullMode, // Skip liquid validation in standalone/full mode
297
298
  }, formatSanitizerMessage, formatValidatorMessage);
298
299
 
299
300
  // Expose validation and content state via ref
@@ -1375,6 +1375,7 @@ describe('HTMLEditor', () => {
1375
1375
  debounceMs: 500,
1376
1376
  enableSanitization: true,
1377
1377
  securityLevel: 'standard',
1378
+ isFullMode: true,
1378
1379
  },
1379
1380
  expect.any(Function),
1380
1381
  expect.any(Function)
@@ -155,7 +155,7 @@ describe('useValidation', () => {
155
155
  await Promise.resolve();
156
156
  });
157
157
 
158
- expect(validateHTML).toHaveBeenCalledWith('<p>Test</p>', 'email', null);
158
+ expect(validateHTML).toHaveBeenCalledWith('<p>Test</p>', 'email', null, { skipLiquidValidation: false });
159
159
  });
160
160
 
161
161
  it('updates validation when content changes', async () => {
@@ -472,7 +472,7 @@ describe('useValidation', () => {
472
472
  await Promise.resolve();
473
473
  });
474
474
 
475
- expect(validateHTML).toHaveBeenCalledWith('<p>Test</p>', 'inapp', null);
475
+ expect(validateHTML).toHaveBeenCalledWith('<p>Test</p>', 'inapp', null, { skipLiquidValidation: false });
476
476
  });
477
477
 
478
478
  it('defaults to email variant', async () => {
@@ -487,7 +487,7 @@ describe('useValidation', () => {
487
487
  await Promise.resolve();
488
488
  });
489
489
 
490
- expect(validateHTML).toHaveBeenCalledWith('<p>Test</p>', 'email', null);
490
+ expect(validateHTML).toHaveBeenCalledWith('<p>Test</p>', 'email', null, { skipLiquidValidation: false });
491
491
  });
492
492
  });
493
493
 
@@ -1242,4 +1242,133 @@ describe('useValidation', () => {
1242
1242
  });
1243
1243
  });
1244
1244
  });
1245
+
1246
+ describe('isFullMode - skip liquid validation', () => {
1247
+ it('passes skipLiquidValidation: true to validateHTML when isFullMode is true', async () => {
1248
+ const { validateHTML } = require('../../utils/htmlValidator');
1249
+
1250
+ render(<TestComponent content="<p>Test</p>" options={{ isFullMode: true }} />);
1251
+
1252
+ await act(async () => {
1253
+ jest.advanceTimersByTime(500);
1254
+ await Promise.resolve();
1255
+ await Promise.resolve();
1256
+ await Promise.resolve();
1257
+ });
1258
+
1259
+ expect(validateHTML).toHaveBeenCalledWith('<p>Test</p>', 'email', null, { skipLiquidValidation: true });
1260
+ });
1261
+
1262
+ it('excludes API liquid errors from getAllIssues when isFullMode is true', async () => {
1263
+ let validationState;
1264
+ const onStateChange = (state) => { validationState = state; };
1265
+
1266
+ const apiValidationErrors = {
1267
+ liquidErrors: ['Unsupported tag: points_balance'],
1268
+ standardErrors: [],
1269
+ };
1270
+
1271
+ render(
1272
+ <TestComponent
1273
+ content="<p>Test</p>"
1274
+ options={{ apiValidationErrors, isFullMode: true }}
1275
+ onStateChange={onStateChange}
1276
+ />
1277
+ );
1278
+
1279
+ await act(async () => {
1280
+ jest.advanceTimersByTime(500);
1281
+ await Promise.resolve();
1282
+ await Promise.resolve();
1283
+ await Promise.resolve();
1284
+ });
1285
+
1286
+ await waitFor(() => {
1287
+ expect(validationState).toBeDefined();
1288
+ });
1289
+
1290
+ const issues = validationState.getAllIssues();
1291
+ const liquidIssues = issues.filter((i) => i.source === 'liquid-validator');
1292
+ expect(liquidIssues).toHaveLength(0);
1293
+ });
1294
+
1295
+ it('returns isClean true when only liquid errors exist and isFullMode is true', () => {
1296
+ const apiValidationErrors = {
1297
+ liquidErrors: ['Unsupported tag: points_balance'],
1298
+ standardErrors: [],
1299
+ };
1300
+
1301
+ render(
1302
+ <TestComponent
1303
+ content=""
1304
+ options={{ apiValidationErrors, isFullMode: true }}
1305
+ />
1306
+ );
1307
+
1308
+ // Before validation runs, isClean should be true because liquid errors are ignored in full mode
1309
+ expect(screen.getByTestId('is-clean')).toHaveTextContent('true');
1310
+ });
1311
+
1312
+ it('does not treat liquid errors as blocking when isFullMode is true', async () => {
1313
+ let validationState;
1314
+ const onStateChange = (state) => { validationState = state; };
1315
+
1316
+ const apiValidationErrors = {
1317
+ liquidErrors: ['Unsupported tag: points_balance'],
1318
+ standardErrors: [],
1319
+ };
1320
+
1321
+ render(
1322
+ <TestComponent
1323
+ content="<p>Valid</p>"
1324
+ options={{ apiValidationErrors, isFullMode: true }}
1325
+ onStateChange={onStateChange}
1326
+ />
1327
+ );
1328
+
1329
+ await act(async () => {
1330
+ jest.advanceTimersByTime(500);
1331
+ await Promise.resolve();
1332
+ await Promise.resolve();
1333
+ await Promise.resolve();
1334
+ });
1335
+
1336
+ await waitFor(() => {
1337
+ expect(validationState).toBeDefined();
1338
+ });
1339
+
1340
+ expect(validationState.hasBlockingErrors).toBe(false);
1341
+ });
1342
+
1343
+ it('still treats standard API errors as blocking when isFullMode is true', async () => {
1344
+ let validationState;
1345
+ const onStateChange = (state) => { validationState = state; };
1346
+
1347
+ const apiValidationErrors = {
1348
+ liquidErrors: ['Liquid error'],
1349
+ standardErrors: ['Standard error'],
1350
+ };
1351
+
1352
+ render(
1353
+ <TestComponent
1354
+ content="<p>Valid</p>"
1355
+ options={{ apiValidationErrors, isFullMode: true }}
1356
+ onStateChange={onStateChange}
1357
+ />
1358
+ );
1359
+
1360
+ await act(async () => {
1361
+ jest.advanceTimersByTime(500);
1362
+ await Promise.resolve();
1363
+ await Promise.resolve();
1364
+ await Promise.resolve();
1365
+ });
1366
+
1367
+ await waitFor(() => {
1368
+ expect(validationState).toBeDefined();
1369
+ });
1370
+
1371
+ expect(validationState.hasBlockingErrors).toBe(true);
1372
+ });
1373
+ });
1245
1374
  });
@@ -77,6 +77,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
77
77
  enableSanitization = true,
78
78
  securityLevel = 'standard',
79
79
  apiValidationErrors = null, // API validation errors from validateLiquidTemplateContent
80
+ isFullMode = false, // When true, skip liquid validation (standalone/full mode)
80
81
  } = options;
81
82
 
82
83
  // Validation state
@@ -137,7 +138,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
137
138
 
138
139
  try {
139
140
  // 1. HTML Validation
140
- const htmlValidation = validateHTML(htmlContent, variant, formatValidatorMessage);
141
+ const htmlValidation = validateHTML(htmlContent, variant, formatValidatorMessage, { skipLiquidValidation: isFullMode });
141
142
 
142
143
  // 2. CSS Validation (extract from HTML)
143
144
  const cssValidation = extractAndValidateCSS(htmlContent, formatValidatorMessage);
@@ -206,7 +207,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
206
207
  },
207
208
  }));
208
209
  }
209
- }, [variant, enableSanitization, securityLevel, formatSanitizerMessage, formatValidatorMessage]);
210
+ }, [variant, enableSanitization, securityLevel, formatSanitizerMessage, formatValidatorMessage, isFullMode]);
210
211
 
211
212
  /**
212
213
  * Validates content with debouncing
@@ -339,7 +340,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
339
340
  */
340
341
  const getAllIssues = useCallback(() => {
341
342
  // API errors (liquid + standard) are blocking – they block Save/Update/Preview/Test
342
- const apiLiquidErrors = (apiValidationErrors?.liquidErrors || []).map((errorMessage) => {
343
+ const apiLiquidErrors = (isFullMode ? [] : (apiValidationErrors?.liquidErrors || [])).map((errorMessage) => {
343
344
  const extractedLine = extractLineNumberFromMessage(errorMessage);
344
345
  return {
345
346
  type: VALIDATION_SEVERITY.ERROR,
@@ -420,19 +421,20 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
420
421
 
421
422
  // Ensure we always return an array
422
423
  return Array.isArray(allIssues) ? allIssues : [];
423
- }, [validationState, apiValidationErrors, extractLineNumberFromMessage, content]);
424
+ }, [validationState, apiValidationErrors, extractLineNumberFromMessage, content, isFullMode]);
424
425
 
425
426
  /**
426
427
  * Check if validation is clean (no errors or warnings)
427
428
  * Includes API validation errors in the check
428
429
  */
429
430
  const isClean = useCallback(() => {
430
- const hasApiErrors = (apiValidationErrors?.liquidErrors?.length || 0) + (apiValidationErrors?.standardErrors?.length || 0) > 0;
431
+ const liquidErrorCount = isFullMode ? 0 : (apiValidationErrors?.liquidErrors?.length || 0);
432
+ const hasApiErrors = liquidErrorCount + (apiValidationErrors?.standardErrors?.length || 0) > 0;
431
433
  return validationState.summary.totalErrors === 0
432
434
  && validationState.summary.totalWarnings === 0
433
435
  && !validationState.summary.hasSecurityIssues
434
436
  && !hasApiErrors;
435
- }, [validationState.summary, apiValidationErrors]);
437
+ }, [validationState.summary, apiValidationErrors, isFullMode]);
436
438
 
437
439
  // Effect to validate content when it changes
438
440
  useEffect(() => {
@@ -448,11 +450,12 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
448
450
  }
449
451
  }, []);
450
452
 
451
- const hasApiErrors = (apiValidationErrors?.liquidErrors?.length || 0) + (apiValidationErrors?.standardErrors?.length || 0) > 0;
453
+ const hasApiLiquidErrors = isFullMode ? false : (apiValidationErrors?.liquidErrors?.length || 0) > 0;
454
+ const hasApiErrors = hasApiLiquidErrors || (apiValidationErrors?.standardErrors?.length || 0) > 0;
452
455
 
453
456
  const protocolTypes = ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'];
454
- // Client-side Liquid validation errors are blocking (genuine syntax errors)
455
- const hasClientSideLiquidErrors = (validationState.htmlErrors || []).some((e) => e.source === ISSUE_SOURCES.LIQUID && e.severity === VALIDATION_SEVERITY.ERROR);
457
+ // Client-side Liquid validation errors are blocking (genuine syntax errors) - skip in full mode
458
+ const hasClientSideLiquidErrors = isFullMode ? false : (validationState.htmlErrors || []).some((e) => e.source === ISSUE_SOURCES.LIQUID && e.severity === VALIDATION_SEVERITY.ERROR);
456
459
  const hasBlockingErrors = (validationState.sanitizationWarnings || []).some((w) => BLOCKING_ERROR_RULE_IDS.includes(w.rule)) || (validationState.securityIssues || []).some((s) => protocolTypes.includes(s?.type)) || hasApiErrors || hasClientSideLiquidErrors;
457
460
 
458
461
  return {
@@ -76,7 +76,7 @@ const CUSTOM_VALIDATIONS = {
76
76
  * @param {Function} formatMessage - Message formatter function for internationalization
77
77
  * @returns {Object} Validation result with errors and warnings
78
78
  */
79
- export const validateHTML = (html, variant = 'email', formatMessage = defaultMessageFormatter) => {
79
+ export const validateHTML = (html, variant = 'email', formatMessage = defaultMessageFormatter, options = {}) => {
80
80
  if (!html || typeof html !== 'string') {
81
81
  return {
82
82
  isValid: true,
@@ -133,7 +133,9 @@ export const validateHTML = (html, variant = 'email', formatMessage = defaultMes
133
133
  // Always run custom validations and Liquid validation, even if HTMLHint failed
134
134
  // This ensures unsafe protocol detection and other critical validations still run
135
135
  runCustomValidations(html, variant, results, formatMessage);
136
- runLiquidValidation(html, variant, results, formatMessage);
136
+ if (!options.skipLiquidValidation) {
137
+ runLiquidValidation(html, variant, results, formatMessage);
138
+ }
137
139
 
138
140
  return results;
139
141
  };
@@ -956,8 +956,8 @@ const EmailHTMLEditor = (props) => {
956
956
  }
957
957
  };
958
958
 
959
- // If liquid enabled, validate first using extractTags API
960
- if (isLiquidEnabled && getLiquidTags) {
959
+ // If liquid enabled, validate first using extractTags API (skip in full/standalone mode)
960
+ if (isLiquidEnabled && getLiquidTags && !isFullMode) {
961
961
  // Note: API validation errors are already cleared at the start of handleSave
962
962
  // This ensures fresh validation on every save attempt
963
963
 
@@ -1176,9 +1176,10 @@ describe('EmailHTMLEditor', () => {
1176
1176
  mockGetAllIssues.mockReturnValue([]);
1177
1177
 
1178
1178
  // Set subject and content via component interactions
1179
+ // Use isFullMode: false (library mode) to test liquid validation path
1179
1180
  const { rerender } = renderWithIntl({
1180
1181
  isGetFormData: false,
1181
- isFullMode: true,
1182
+ isFullMode: false,
1182
1183
  metaEntities: {
1183
1184
  tags: {
1184
1185
  standard: [{ name: 'customer.name' }],
@@ -1206,7 +1207,7 @@ describe('EmailHTMLEditor', () => {
1206
1207
  <EmailHTMLEditor
1207
1208
  {...defaultProps}
1208
1209
  isGetFormData
1209
- isFullMode
1210
+ isFullMode={false}
1210
1211
  metaEntities={{
1211
1212
  tags: {
1212
1213
  standard: [{ name: 'customer.name' }],
@@ -1231,9 +1232,10 @@ describe('EmailHTMLEditor', () => {
1231
1232
  mockGetAllIssues.mockReturnValue([]);
1232
1233
 
1233
1234
  // Set subject and content via component interactions
1235
+ // Use isFullMode: false (library mode) to test liquid validation path
1234
1236
  const { rerender } = renderWithIntl({
1235
1237
  isGetFormData: false,
1236
- isFullMode: true,
1238
+ isFullMode: false,
1237
1239
  isLiquidEnabled: true,
1238
1240
  getLiquidTags,
1239
1241
  });
@@ -1253,7 +1255,7 @@ describe('EmailHTMLEditor', () => {
1253
1255
  await act(async () => {
1254
1256
  rerender(
1255
1257
  <IntlProvider locale="en" messages={{}}>
1256
- <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode getLiquidTags={getLiquidTags} />
1258
+ <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode={false} getLiquidTags={getLiquidTags} />
1257
1259
  </IntlProvider>
1258
1260
  );
1259
1261
  });
@@ -1281,9 +1283,10 @@ describe('EmailHTMLEditor', () => {
1281
1283
  mockGetAllIssues.mockReturnValue([]);
1282
1284
 
1283
1285
  // Set subject and content via component interactions
1286
+ // Use isFullMode: false (library mode) to test liquid validation path
1284
1287
  const { rerender } = renderWithIntl({
1285
1288
  isGetFormData: false,
1286
- isFullMode: true,
1289
+ isFullMode: false,
1287
1290
  isLiquidEnabled: true,
1288
1291
  getLiquidTags,
1289
1292
  showLiquidErrorInFooter,
@@ -1305,7 +1308,7 @@ describe('EmailHTMLEditor', () => {
1305
1308
  await act(async () => {
1306
1309
  rerender(
1307
1310
  <IntlProvider locale="en" messages={{}}>
1308
- <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode getLiquidTags={getLiquidTags} showLiquidErrorInFooter={showLiquidErrorInFooter} onValidationFail={onValidationFail} />
1311
+ <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode={false} getLiquidTags={getLiquidTags} showLiquidErrorInFooter={showLiquidErrorInFooter} onValidationFail={onValidationFail} />
1309
1312
  </IntlProvider>
1310
1313
  );
1311
1314
  });
@@ -1325,13 +1328,7 @@ describe('EmailHTMLEditor', () => {
1325
1328
  return Promise.resolve(true);
1326
1329
  });
1327
1330
 
1328
- const emailActions = {
1329
- ...defaultProps.emailActions,
1330
- transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
1331
- createTemplate: jest.fn((obj, callback) => {
1332
- callback({ templateId: { _id: '123', versions: {} } });
1333
- }),
1334
- };
1331
+ const getFormdata = jest.fn();
1335
1332
  const getLiquidTags = jest.fn((content, callback) => {
1336
1333
  callback({ askAiraResponse: { data: [] }, isError: false });
1337
1334
  });
@@ -1339,12 +1336,12 @@ describe('EmailHTMLEditor', () => {
1339
1336
  mockGetAllIssues.mockReturnValue([]);
1340
1337
 
1341
1338
  // Set subject and content via component interactions
1339
+ // Use isFullMode: false (library mode) to test liquid validation path
1342
1340
  const { rerender } = renderWithIntl({
1343
1341
  isGetFormData: false,
1344
- isFullMode: true,
1345
- isLiquidEnabled: true,
1342
+ isFullMode: false,
1346
1343
  getLiquidTags,
1347
- emailActions,
1344
+ getFormdata,
1348
1345
  templateName: 'New Template',
1349
1346
  });
1350
1347
  const input = screen.getByTestId('subject-input');
@@ -1363,7 +1360,7 @@ describe('EmailHTMLEditor', () => {
1363
1360
  await act(async () => {
1364
1361
  rerender(
1365
1362
  <IntlProvider locale="en" messages={{}}>
1366
- <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode getLiquidTags={getLiquidTags} emailActions={emailActions} templateName="New Template" />
1363
+ <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode={false} getLiquidTags={getLiquidTags} getFormdata={getFormdata} templateName="New Template" />
1367
1364
  </IntlProvider>
1368
1365
  );
1369
1366
  });
@@ -1372,8 +1369,9 @@ describe('EmailHTMLEditor', () => {
1372
1369
  expect(validateLiquidTemplateContent).toHaveBeenCalled();
1373
1370
  }, { timeout: 5000 });
1374
1371
 
1372
+ // In library mode (isFullMode=false), save proceeds via getFormdata
1375
1373
  await waitFor(() => {
1376
- expect(emailActions.createTemplate).toHaveBeenCalled();
1374
+ expect(getFormdata).toHaveBeenCalled();
1377
1375
  }, { timeout: 5000 });
1378
1376
  });
1379
1377
 
@@ -2711,4 +2709,98 @@ describe('EmailHTMLEditor', () => {
2711
2709
  expect(screen.getByTestId('html-editor')).toBeInTheDocument();
2712
2710
  });
2713
2711
  });
2712
+
2713
+ describe('isFullMode - skip liquid validation on save', () => {
2714
+ it('skips liquid validation when isFullMode is true', async () => {
2715
+ validateLiquidTemplateContent.mockClear();
2716
+ const getLiquidTags = jest.fn((content, callback) => {
2717
+ callback({ askAiraResponse: { data: [] }, isError: false });
2718
+ });
2719
+ const getFormdata = jest.fn();
2720
+ // Ensure no HTML/Label/Liquid errors from HtmlEditor
2721
+ mockGetAllIssues.mockReturnValue([]);
2722
+
2723
+ const { rerender } = renderWithIntl({
2724
+ isGetFormData: false,
2725
+ isFullMode: true,
2726
+ isLiquidEnabled: true,
2727
+ getLiquidTags,
2728
+ getFormdata,
2729
+ });
2730
+ const input = screen.getByTestId('subject-input');
2731
+ await act(async () => {
2732
+ fireEvent.change(input, { target: { value: 'Valid Subject' } });
2733
+ });
2734
+ const changeButton = screen.getByTestId('trigger-content-change');
2735
+ await act(async () => {
2736
+ fireEvent.click(changeButton);
2737
+ });
2738
+ await act(async () => {
2739
+ await new Promise((resolve) => setTimeout(resolve, 100));
2740
+ });
2741
+ // Trigger save
2742
+ await act(async () => {
2743
+ rerender(
2744
+ <IntlProvider locale="en" messages={{}}>
2745
+ <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode getLiquidTags={getLiquidTags} getFormdata={getFormdata} />
2746
+ </IntlProvider>
2747
+ );
2748
+ });
2749
+
2750
+ // In full mode, liquid validation should be skipped entirely
2751
+ await act(async () => {
2752
+ await new Promise((resolve) => setTimeout(resolve, 200));
2753
+ });
2754
+ expect(validateLiquidTemplateContent).not.toHaveBeenCalled();
2755
+ });
2756
+
2757
+ it('proceeds directly to save without liquid validation in full mode', async () => {
2758
+ validateLiquidTemplateContent.mockClear();
2759
+ const getLiquidTags = jest.fn((content, callback) => {
2760
+ callback({ askAiraResponse: { data: [] }, isError: false });
2761
+ });
2762
+ const emailActions = {
2763
+ ...defaultProps.emailActions,
2764
+ transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
2765
+ createTemplate: jest.fn((obj, callback) => {
2766
+ callback({ templateId: { _id: '123', versions: {} } });
2767
+ }),
2768
+ };
2769
+ // Ensure no HTML/Label/Liquid errors from HtmlEditor
2770
+ mockGetAllIssues.mockReturnValue([]);
2771
+
2772
+ const { rerender } = renderWithIntl({
2773
+ isGetFormData: false,
2774
+ isFullMode: true,
2775
+ getLiquidTags,
2776
+ emailActions,
2777
+ templateName: 'New Template',
2778
+ });
2779
+ const input = screen.getByTestId('subject-input');
2780
+ await act(async () => {
2781
+ fireEvent.change(input, { target: { value: 'Valid Subject' } });
2782
+ });
2783
+ const changeButton = screen.getByTestId('trigger-content-change');
2784
+ await act(async () => {
2785
+ fireEvent.click(changeButton);
2786
+ });
2787
+ await act(async () => {
2788
+ await new Promise((resolve) => setTimeout(resolve, 100));
2789
+ });
2790
+ // Trigger save
2791
+ await act(async () => {
2792
+ rerender(
2793
+ <IntlProvider locale="en" messages={{}}>
2794
+ <EmailHTMLEditor {...defaultProps} isGetFormData isFullMode getLiquidTags={getLiquidTags} emailActions={emailActions} templateName="New Template" />
2795
+ </IntlProvider>
2796
+ );
2797
+ });
2798
+
2799
+ // Should skip liquid validation and proceed to save directly
2800
+ await waitFor(() => {
2801
+ expect(emailActions.createTemplate).toHaveBeenCalled();
2802
+ }, { timeout: 5000 });
2803
+ expect(validateLiquidTemplateContent).not.toHaveBeenCalled();
2804
+ });
2805
+ });
2714
2806
  });