@capillarytech/creatives-library 8.0.299-alpha.5 → 8.0.299-alpha.7

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.
@@ -171,9 +171,6 @@ const CommonTestAndPreview = (props) => {
171
171
  const [selectedTestEntities, setSelectedTestEntities] = useState([]);
172
172
  const [beeContent, setBeeContent] = useState(''); // Track BEE editor content separately (EMAIL only)
173
173
  const previousBeeContentRef = useRef(''); // Track previous BEE content (EMAIL only)
174
- // Container for notifications so they render inside the slidebox (visible in campaigns/library mode)
175
- const notificationContainerRef = useRef(null);
176
- const getNotificationContainer = () => notificationContainerRef.current || document.body;
177
174
  // Delivery settings for Test and Preview (SMS, Email, WhatsApp) — user selection only
178
175
  const [testPreviewDeliverySettings, setTestPreviewDeliverySettings] = useState({
179
176
  [CHANNELS.SMS]: {
@@ -447,7 +444,6 @@ const CommonTestAndPreview = (props) => {
447
444
  if (response && response.success) {
448
445
  CapNotification.success({
449
446
  message: formatMessage(messages.newTestCustomerAddedSuccess),
450
- getContainer: getNotificationContainer,
451
447
  });
452
448
  // API may return customerId in response.response (e.g. { response: { customerId: 438845651 } })
453
449
  const res = response?.response || response;
@@ -470,7 +466,6 @@ const CommonTestAndPreview = (props) => {
470
466
  CapNotification.error({
471
467
  message: formatMessage(messages.errorTitle),
472
468
  description: response?.message || formatMessage(messages.failedToAddTestCustomer),
473
- getContainer: getNotificationContainer,
474
469
  });
475
470
  }
476
471
  } catch (error) {
@@ -479,7 +474,6 @@ const CommonTestAndPreview = (props) => {
479
474
  CapNotification.error({
480
475
  message: formatMessage(messages.errorTitle),
481
476
  description: error?.message || formatMessage(messages.errorAddingTestCustomer),
482
- getContainer: getNotificationContainer,
483
477
  });
484
478
  }
485
479
  } finally {
@@ -2528,7 +2522,6 @@ const CommonTestAndPreview = (props) => {
2528
2522
  } catch (error) {
2529
2523
  CapNotification.error({
2530
2524
  message: formatMessage(messages.invalidJSON),
2531
- getContainer: getNotificationContainer,
2532
2525
  });
2533
2526
  }
2534
2527
  };
@@ -2575,7 +2568,6 @@ const CommonTestAndPreview = (props) => {
2575
2568
  } catch (error) {
2576
2569
  CapNotification.error({
2577
2570
  message: formatMessage(messages.previewUpdateError),
2578
- getContainer: getNotificationContainer,
2579
2571
  });
2580
2572
  }
2581
2573
  };
@@ -2669,7 +2661,6 @@ const CommonTestAndPreview = (props) => {
2669
2661
  setSearchValue('');
2670
2662
  CapNotification.success({
2671
2663
  message: formatMessage(messages.customerAlreadyInTestList),
2672
- getContainer: getNotificationContainer,
2673
2664
  });
2674
2665
  return;
2675
2666
  }
@@ -2685,7 +2676,7 @@ const CommonTestAndPreview = (props) => {
2685
2676
 
2686
2677
  if (!success) {
2687
2678
  const errorMessage = response?.message || response?.status?.message || formatMessage(messages.memberLookupError);
2688
- CapNotification.error({ title: formatMessage(messages.errorTitle), message: errorMessage, getContainer: getNotificationContainer });
2679
+ CapNotification.error({ title: formatMessage(messages.errorTitle), message: errorMessage });
2689
2680
  return;
2690
2681
  }
2691
2682
 
@@ -2702,7 +2693,6 @@ const CommonTestAndPreview = (props) => {
2702
2693
  setSearchValue('');
2703
2694
  CapNotification.success({
2704
2695
  message: formatMessage(messages.customerAlreadyInTestList),
2705
- getContainer: getNotificationContainer,
2706
2696
  });
2707
2697
  return;
2708
2698
  }
@@ -2720,7 +2710,6 @@ const CommonTestAndPreview = (props) => {
2720
2710
  } catch {
2721
2711
  CapNotification.error({
2722
2712
  message: formatMessage(messages.memberLookupError),
2723
- getContainer: getNotificationContainer,
2724
2713
  });
2725
2714
  } finally {
2726
2715
  setIsCustomerDataLoading(false);
@@ -2773,12 +2762,10 @@ const CommonTestAndPreview = (props) => {
2773
2762
  if (result) {
2774
2763
  CapNotification.success({
2775
2764
  message: formatMessage(messages.testMessageSent),
2776
- getContainer: getNotificationContainer,
2777
2765
  });
2778
2766
  } else {
2779
2767
  CapNotification.error({
2780
2768
  message: formatMessage(messages.testMessageFailed),
2781
- getContainer: getNotificationContainer,
2782
2769
  });
2783
2770
  }
2784
2771
  });
@@ -2900,7 +2887,6 @@ const CommonTestAndPreview = (props) => {
2900
2887
  show={show}
2901
2888
  size="size-xl"
2902
2889
  content={(
2903
- <div ref={notificationContainerRef} className="common-test-and-preview-notification-container" style={{ position: 'relative', height: '100%' }}>
2904
2890
  <CapSpin
2905
2891
  spinning={isCustomerDataLoading}
2906
2892
  className={`common-test-preview-lookup-spin ${isCustomerDataLoading ? 'common-test-preview-customer-loading' : ''}`}
@@ -2947,7 +2933,6 @@ const CommonTestAndPreview = (props) => {
2947
2933
  )}
2948
2934
  </CapRow>
2949
2935
  </CapSpin>
2950
- </div>
2951
2936
  )}
2952
2937
  />
2953
2938
  );
@@ -644,6 +644,326 @@ describe('useValidation', () => {
644
644
  // The functionality is verified through integration tests and the hasErrors test above
645
645
  });
646
646
 
647
+ describe('getLineAndColumnFromPosition utility (lines 36-46)', () => {
648
+ it('computes correct line and column from position in security issue', async () => {
649
+ const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
650
+ const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
651
+ let validationState;
652
+
653
+ validateHTML.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
654
+ extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
655
+ isContentSafe.mockImplementationOnce(() => false);
656
+ // position 12 in "line1\nline2\nX" → line 3, column 1
657
+ const content = 'line1\nline2\njavascript:alert(1)';
658
+ findUnsafeContent.mockImplementationOnce(() => [{ type: 'JavaScript Protocol', position: 12 }]);
659
+
660
+ render(
661
+ <TestComponent
662
+ content={content}
663
+ options={{ enableRealTime: false }}
664
+ onStateChange={(state) => { validationState = state; }}
665
+ />
666
+ );
667
+
668
+ await waitFor(() => { expect(validationState).toBeDefined(); });
669
+ await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
670
+
671
+ await waitFor(() => {
672
+ const issues = validationState.getAllIssues();
673
+ const secIssue = issues.find((i) => i.source === 'security');
674
+ expect(secIssue).toBeDefined();
675
+ expect(secIssue.line).toBe(3);
676
+ expect(secIssue.column).toBe(1);
677
+ });
678
+ });
679
+
680
+ it('defaults line/column to 1 when security issue has no position', async () => {
681
+ const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
682
+ const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
683
+ let validationState;
684
+
685
+ validateHTML.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
686
+ extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
687
+ isContentSafe.mockImplementationOnce(() => false);
688
+ findUnsafeContent.mockImplementationOnce(() => [{ type: 'JavaScript Protocol' }]); // no position
689
+
690
+ render(
691
+ <TestComponent
692
+ content="<a href='javascript:x'>x</a>"
693
+ options={{ enableRealTime: false }}
694
+ onStateChange={(state) => { validationState = state; }}
695
+ />
696
+ );
697
+
698
+ await waitFor(() => { expect(validationState).toBeDefined(); });
699
+ await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
700
+
701
+ await waitFor(() => {
702
+ const issues = validationState.getAllIssues();
703
+ const secIssue = issues.find((i) => i.source === 'security');
704
+ expect(secIssue).toBeDefined();
705
+ expect(secIssue.line).toBe(1);
706
+ expect(secIssue.column).toBe(1);
707
+ });
708
+ });
709
+
710
+ it('handles negative or undefined position in getLineAndColumnFromPosition', async () => {
711
+ const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
712
+ const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
713
+ let validationState;
714
+
715
+ validateHTML.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
716
+ extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
717
+ isContentSafe.mockImplementationOnce(() => false);
718
+ findUnsafeContent.mockImplementationOnce(() => [{ type: 'JavaScript Protocol', position: -5 }]);
719
+
720
+ render(
721
+ <TestComponent
722
+ content="<a href='javascript:x'>x</a>"
723
+ options={{ enableRealTime: false }}
724
+ onStateChange={(state) => { validationState = state; }}
725
+ />
726
+ );
727
+
728
+ await waitFor(() => { expect(validationState).toBeDefined(); });
729
+ await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
730
+
731
+ await waitFor(() => {
732
+ const issues = validationState.getAllIssues();
733
+ const secIssue = issues.find((i) => i.source === 'security');
734
+ expect(secIssue).toBeDefined();
735
+ // negative position falls back to line 1, column 1
736
+ expect(secIssue.line).toBe(1);
737
+ expect(secIssue.column).toBe(1);
738
+ });
739
+ });
740
+ });
741
+
742
+ describe('getAllIssues spread — all six arrays included (lines 401-411)', () => {
743
+ it('includes items from htmlErrors, htmlWarnings, htmlInfo, cssErrors, cssWarnings, cssInfo', async () => {
744
+ const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
745
+ const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
746
+ let validationState;
747
+
748
+ validateHTML.mockImplementationOnce(() => ({
749
+ isValid: false,
750
+ errors: [{ type: 'error', message: 'html error', line: 1, column: 1, rule: 'r1', severity: 'error', source: 'htmlhint' }],
751
+ warnings: [{ type: 'warning', message: 'html warning', line: 2, column: 1, rule: 'r2', severity: 'warning', source: 'htmlhint' }],
752
+ info: [{ type: 'info', message: 'html info', line: 3, column: 1, rule: 'r3', severity: 'info', source: 'htmlhint' }],
753
+ }));
754
+ extractAndValidateCSS.mockImplementationOnce(() => ({
755
+ isValid: false,
756
+ errors: [{ type: 'error', message: 'css error', line: 4, column: 1, rule: 'r4', severity: 'error', source: 'css-validator' }],
757
+ warnings: [{ type: 'warning', message: 'css warning', line: 5, column: 1, rule: 'r5', severity: 'warning', source: 'css-validator' }],
758
+ info: [{ type: 'info', message: 'css info', line: 6, column: 1, rule: 'r6', severity: 'info', source: 'css-validator' }],
759
+ }));
760
+ isContentSafe.mockImplementationOnce(() => true);
761
+ findUnsafeContent.mockImplementationOnce(() => []);
762
+
763
+ render(
764
+ <TestComponent
765
+ content="<div>test</div>"
766
+ options={{ enableRealTime: false, enableSanitization: false }}
767
+ onStateChange={(state) => { validationState = state; }}
768
+ />
769
+ );
770
+
771
+ await waitFor(() => { expect(validationState).toBeDefined(); });
772
+ await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
773
+
774
+ await waitFor(() => {
775
+ const issues = validationState.getAllIssues();
776
+ const rules = issues.map((i) => i.rule);
777
+ expect(rules).toContain('r1'); // htmlErrors
778
+ expect(rules).toContain('r2'); // htmlWarnings
779
+ expect(rules).toContain('r3'); // htmlInfo
780
+ expect(rules).toContain('r4'); // cssErrors
781
+ expect(rules).toContain('r5'); // cssWarnings
782
+ expect(rules).toContain('r6'); // cssInfo
783
+ expect(issues.length).toBeGreaterThanOrEqual(6);
784
+ });
785
+ });
786
+
787
+ it('sorts issues: errors before warnings before info', async () => {
788
+ const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
789
+ const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
790
+ let validationState;
791
+
792
+ validateHTML.mockImplementationOnce(() => ({
793
+ isValid: false,
794
+ errors: [{ type: 'error', message: 'html error', line: 10, column: 1, rule: 're', severity: 'error', source: 'htmlhint' }],
795
+ warnings: [{ type: 'warning', message: 'html warning', line: 1, column: 1, rule: 'rw', severity: 'warning', source: 'htmlhint' }],
796
+ info: [{ type: 'info', message: 'html info', line: 1, column: 1, rule: 'ri', severity: 'info', source: 'htmlhint' }],
797
+ }));
798
+ extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
799
+ isContentSafe.mockImplementationOnce(() => true);
800
+ findUnsafeContent.mockImplementationOnce(() => []);
801
+
802
+ render(
803
+ <TestComponent
804
+ content="<div>test</div>"
805
+ options={{ enableRealTime: false, enableSanitization: false }}
806
+ onStateChange={(state) => { validationState = state; }}
807
+ />
808
+ );
809
+
810
+ await waitFor(() => { expect(validationState).toBeDefined(); });
811
+ await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
812
+
813
+ await waitFor(() => {
814
+ const issues = validationState.getAllIssues();
815
+ const severities = issues.map((i) => i.severity);
816
+ // Errors should come before warnings, warnings before info
817
+ const firstError = severities.indexOf('error');
818
+ const firstWarning = severities.indexOf('warning');
819
+ const firstInfo = severities.indexOf('info');
820
+ if (firstError !== -1 && firstWarning !== -1) expect(firstError).toBeLessThan(firstWarning);
821
+ if (firstWarning !== -1 && firstInfo !== -1) expect(firstWarning).toBeLessThan(firstInfo);
822
+ });
823
+ });
824
+ });
825
+
826
+ describe('hasClientSideLiquidErrors and hasBlockingErrors (lines 455-456)', () => {
827
+ it('sets hasBlockingErrors=true when htmlErrors has a liquid-validator error', async () => {
828
+ const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
829
+ const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
830
+ let validationState;
831
+
832
+ validateHTML.mockImplementationOnce(() => ({
833
+ isValid: false,
834
+ errors: [{
835
+ type: 'error',
836
+ message: 'Liquid syntax error',
837
+ line: 1,
838
+ column: 1,
839
+ rule: 'liquid-syntax',
840
+ severity: 'error',
841
+ source: 'liquid-validator', // ISSUE_SOURCES.LIQUID
842
+ }],
843
+ warnings: [],
844
+ info: [],
845
+ }));
846
+ extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
847
+ isContentSafe.mockImplementationOnce(() => true);
848
+ findUnsafeContent.mockImplementationOnce(() => []);
849
+
850
+ render(
851
+ <TestComponent
852
+ content="{{ invalid liquid }}"
853
+ options={{ enableRealTime: false, enableSanitization: false }}
854
+ onStateChange={(state) => { validationState = state; }}
855
+ />
856
+ );
857
+
858
+ await waitFor(() => { expect(validationState).toBeDefined(); });
859
+ await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
860
+
861
+ await waitFor(() => {
862
+ // hasClientSideLiquidErrors → true because htmlErrors has liquid-validator+error item
863
+ // therefore hasBlockingErrors → true
864
+ expect(validationState.hasBlockingErrors).toBe(true);
865
+ });
866
+ });
867
+
868
+ it('does NOT set hasBlockingErrors from liquid-validator warning (non-error severity)', async () => {
869
+ const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
870
+ const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
871
+ let validationState;
872
+
873
+ validateHTML.mockImplementationOnce(() => ({
874
+ isValid: true,
875
+ errors: [],
876
+ warnings: [{
877
+ type: 'warning',
878
+ message: 'Liquid warning',
879
+ line: 1,
880
+ column: 1,
881
+ rule: 'liquid-warning',
882
+ severity: 'warning',
883
+ source: 'liquid-validator',
884
+ }],
885
+ info: [],
886
+ }));
887
+ extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
888
+ isContentSafe.mockImplementationOnce(() => true);
889
+ findUnsafeContent.mockImplementationOnce(() => []);
890
+
891
+ render(
892
+ <TestComponent
893
+ content="<div>test</div>"
894
+ options={{ enableRealTime: false, enableSanitization: false }}
895
+ onStateChange={(state) => { validationState = state; }}
896
+ />
897
+ );
898
+
899
+ await waitFor(() => { expect(validationState).toBeDefined(); });
900
+ await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
901
+
902
+ await waitFor(() => {
903
+ // hasClientSideLiquidErrors → false (severity is warning, not error)
904
+ // No other blocking conditions → hasBlockingErrors → false
905
+ expect(validationState.hasBlockingErrors).toBe(false);
906
+ });
907
+ });
908
+
909
+ it('sets hasBlockingErrors=true via API liquid errors', async () => {
910
+ const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
911
+ const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
912
+ let validationState;
913
+
914
+ validateHTML.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
915
+ extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
916
+ isContentSafe.mockImplementationOnce(() => true);
917
+ findUnsafeContent.mockImplementationOnce(() => []);
918
+
919
+ render(
920
+ <TestComponent
921
+ content="<div>test</div>"
922
+ options={{
923
+ enableRealTime: false,
924
+ enableSanitization: false,
925
+ apiValidationErrors: { liquidErrors: ['API liquid error'], standardErrors: [] },
926
+ }}
927
+ onStateChange={(state) => { validationState = state; }}
928
+ />
929
+ );
930
+
931
+ await waitFor(() => { expect(validationState).toBeDefined(); });
932
+ await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
933
+
934
+ await waitFor(() => {
935
+ // hasApiErrors → true → hasBlockingErrors → true
936
+ expect(validationState.hasBlockingErrors).toBe(true);
937
+ });
938
+ });
939
+
940
+ it('hasBlockingErrors=false when no blocking conditions are present', async () => {
941
+ const { validateHTML, extractAndValidateCSS } = require('../../utils/htmlValidator');
942
+ const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
943
+ let validationState;
944
+
945
+ validateHTML.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
946
+ extractAndValidateCSS.mockImplementationOnce(() => ({ isValid: true, errors: [], warnings: [], info: [] }));
947
+ isContentSafe.mockImplementationOnce(() => true);
948
+ findUnsafeContent.mockImplementationOnce(() => []);
949
+
950
+ render(
951
+ <TestComponent
952
+ content="<div>clean</div>"
953
+ options={{ enableRealTime: false, enableSanitization: false }}
954
+ onStateChange={(state) => { validationState = state; }}
955
+ />
956
+ );
957
+
958
+ await waitFor(() => { expect(validationState).toBeDefined(); });
959
+ await act(async () => { validationState.forceValidation(); await Promise.resolve(); });
960
+
961
+ await waitFor(() => {
962
+ expect(validationState.hasBlockingErrors).toBe(false);
963
+ });
964
+ });
965
+ });
966
+
647
967
  describe('Blocking errors', () => {
648
968
  it('treats protocol security issues as blocking errors', async () => {
649
969
  const { isContentSafe, findUnsafeContent } = require('../../utils/contentSanitizer');
@@ -1076,6 +1076,138 @@ line4`;
1076
1076
  expect(result).toBeDefined();
1077
1077
  expect(Array.isArray(result.warnings)).toBe(true);
1078
1078
  });
1079
+
1080
+ it('routes warning-severity issues to warnings array — exact item check (line 112-113)', () => {
1081
+ // Mock HTMLHint to return an issue with a warningRule ruleId
1082
+ // getSeverityLevel('error', 'tag-pair') → 'warning' (warningRules match)
1083
+ const htmlhint = require('htmlhint');
1084
+ const original = htmlhint.HTMLHint.verify;
1085
+ htmlhint.HTMLHint.verify = jest.fn(() => ([
1086
+ {
1087
+ type: 'error',
1088
+ message: 'Tag must be paired',
1089
+ line: 1,
1090
+ col: 5,
1091
+ rule: { id: 'tag-pair' },
1092
+ },
1093
+ ]));
1094
+
1095
+ const result = validateHTML('<p>unclosed');
1096
+
1097
+ expect(result.warnings.length).toBeGreaterThanOrEqual(1);
1098
+ const item = result.warnings.find((w) => w.rule === 'tag-pair');
1099
+ expect(item).toBeDefined();
1100
+ expect(item.severity).toBe('warning');
1101
+
1102
+ htmlhint.HTMLHint.verify = original;
1103
+ });
1104
+
1105
+ it('routes info-severity issues to info array — exact item check (line 114-115)', () => {
1106
+ // getSeverityLevel('warning', 'space-tab-mixed-disabled') → 'info'
1107
+ // (not in warningRules, type !== 'error')
1108
+ const htmlhint = require('htmlhint');
1109
+ const original = htmlhint.HTMLHint.verify;
1110
+ htmlhint.HTMLHint.verify = jest.fn(() => ([
1111
+ {
1112
+ type: 'warning',
1113
+ message: 'Mixed spaces and tabs',
1114
+ line: 1,
1115
+ col: 1,
1116
+ rule: { id: 'space-tab-mixed-disabled' },
1117
+ },
1118
+ ]));
1119
+
1120
+ const result = validateHTML('<div>test</div>');
1121
+
1122
+ expect(result.info.length).toBeGreaterThanOrEqual(1);
1123
+ const item = result.info.find((i) => i.rule === 'space-tab-mixed-disabled');
1124
+ expect(item).toBeDefined();
1125
+ expect(item.severity).toBe('info');
1126
+
1127
+ htmlhint.HTMLHint.verify = original;
1128
+ });
1129
+
1130
+ it('downgrades HTMLHint error type to warning for non-warningRule (line 163-164) — exact item check', () => {
1131
+ // getSeverityLevel('error', 'src-not-empty') → 'warning' (not in warningRules, but type === 'error')
1132
+ const htmlhint = require('htmlhint');
1133
+ const original = htmlhint.HTMLHint.verify;
1134
+ htmlhint.HTMLHint.verify = jest.fn(() => ([
1135
+ {
1136
+ type: 'error',
1137
+ message: 'The src attribute cannot be empty',
1138
+ line: 1,
1139
+ col: 1,
1140
+ rule: { id: 'src-not-empty' },
1141
+ },
1142
+ ]));
1143
+
1144
+ const result = validateHTML('<img src="">');
1145
+
1146
+ // Should be downgraded to warning (not error)
1147
+ expect(result.warnings.length).toBeGreaterThanOrEqual(1);
1148
+ const item = result.warnings.find((w) => w.rule === 'src-not-empty');
1149
+ expect(item).toBeDefined();
1150
+ expect(item.severity).toBe('warning');
1151
+ // Should NOT be in errors
1152
+ expect(result.errors.find((e) => e.rule === 'src-not-empty')).toBeUndefined();
1153
+
1154
+ htmlhint.HTMLHint.verify = original;
1155
+ });
1156
+
1157
+ it('returns info for HTMLHint warning type with unknown ruleId (line 165-166)', () => {
1158
+ // getSeverityLevel('warning', 'unknown-rule') → 'info' (no warningRules match, type !== 'error')
1159
+ const htmlhint = require('htmlhint');
1160
+ const original = htmlhint.HTMLHint.verify;
1161
+ htmlhint.HTMLHint.verify = jest.fn(() => ([
1162
+ {
1163
+ type: 'warning',
1164
+ message: 'Some custom warning',
1165
+ line: 2,
1166
+ col: 3,
1167
+ rule: { id: 'unknown-custom-rule' },
1168
+ },
1169
+ ]));
1170
+
1171
+ const result = validateHTML('<div>test</div>');
1172
+
1173
+ expect(result.info.length).toBeGreaterThanOrEqual(1);
1174
+ const item = result.info.find((i) => i.rule === 'unknown-custom-rule');
1175
+ expect(item).toBeDefined();
1176
+ expect(item.severity).toBe('info');
1177
+ expect(item.line).toBe(2);
1178
+ expect(item.column).toBe(3);
1179
+ // Should NOT be in errors or warnings
1180
+ expect(result.errors.find((e) => e.rule === 'unknown-custom-rule')).toBeUndefined();
1181
+ expect(result.warnings.find((w) => w.rule === 'unknown-custom-rule')).toBeUndefined();
1182
+
1183
+ htmlhint.HTMLHint.verify = original;
1184
+ });
1185
+
1186
+ it('processes multiple HTMLHint issues with mixed severity correctly (lines 101-118)', () => {
1187
+ const htmlhint = require('htmlhint');
1188
+ const original = htmlhint.HTMLHint.verify;
1189
+ htmlhint.HTMLHint.verify = jest.fn(() => ([
1190
+ { type: 'error', message: 'Tag pair', line: 1, col: 1, rule: { id: 'tag-pair' } }, // → warning
1191
+ { type: 'error', message: 'Src empty', line: 2, col: 1, rule: { id: 'src-not-empty' } }, // → warning (downgrade)
1192
+ { type: 'warning', message: 'Mixed tabs', line: 3, col: 1, rule: { id: 'space-tab-mixed-disabled' } }, // → info
1193
+ ]));
1194
+
1195
+ const result = validateHTML('<div>test</div>');
1196
+
1197
+ const tagPair = result.warnings.find((w) => w.rule === 'tag-pair');
1198
+ expect(tagPair).toBeDefined();
1199
+ expect(tagPair.severity).toBe('warning');
1200
+
1201
+ const srcEmpty = result.warnings.find((w) => w.rule === 'src-not-empty');
1202
+ expect(srcEmpty).toBeDefined();
1203
+ expect(srcEmpty.severity).toBe('warning');
1204
+
1205
+ const mixedTabs = result.info.find((i) => i.rule === 'space-tab-mixed-disabled');
1206
+ expect(mixedTabs).toBeDefined();
1207
+ expect(mixedTabs.severity).toBe('info');
1208
+
1209
+ htmlhint.HTMLHint.verify = original;
1210
+ });
1079
1211
  });
1080
1212
 
1081
1213
  describe('extractAndValidateCSS Advanced Tests', () => {