aria-ease 6.6.0 → 6.8.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/README.md +81 -16
  2. package/bin/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +87 -35
  3. package/bin/chunk-VPBHLMAS.js +127 -0
  4. package/bin/cli.cjs +380 -231
  5. package/bin/cli.js +8 -123
  6. package/bin/configLoader-XRF6VM4J.js +7 -0
  7. package/{dist/contractTestRunnerPlaywright-PC6JOYYV.js → bin/contractTestRunnerPlaywright-UAOFNS7Z.js} +98 -59
  8. package/bin/{test-LP723IXM.js → test-WRIJHN6H.js} +65 -24
  9. package/dist/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +87 -35
  10. package/dist/configLoader-IT4PWCJB.js +128 -0
  11. package/{bin/contractTestRunnerPlaywright-PC6JOYYV.js → dist/contractTestRunnerPlaywright-UAOFNS7Z.js} +98 -59
  12. package/dist/index.cjs +404 -125
  13. package/dist/index.d.cts +6 -1
  14. package/dist/index.d.ts +6 -1
  15. package/dist/index.js +83 -29
  16. package/dist/src/menu/index.cjs +18 -5
  17. package/dist/src/menu/index.js +18 -5
  18. package/dist/src/utils/test/aria-contracts/accordion/accordion.contract.json +13 -19
  19. package/dist/src/utils/test/aria-contracts/combobox/combobox.listbox.contract.json +5 -5
  20. package/dist/src/utils/test/aria-contracts/menu/menu.contract.json +45 -20
  21. package/dist/src/utils/test/aria-contracts/tabs/tabs.contract.json +3 -3
  22. package/dist/src/utils/test/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +85 -36
  23. package/dist/src/utils/test/configLoader-LD4RV2WQ.js +126 -0
  24. package/dist/src/utils/test/{contractTestRunnerPlaywright-RGKMGXND.js → contractTestRunnerPlaywright-IRJOAEMT.js} +94 -58
  25. package/dist/src/utils/test/index.cjs +380 -119
  26. package/dist/src/utils/test/index.d.cts +7 -1
  27. package/dist/src/utils/test/index.d.ts +7 -1
  28. package/dist/src/utils/test/index.js +61 -23
  29. package/package.json +1 -1
package/bin/cli.cjs CHANGED
@@ -31,6 +31,140 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  mod
32
32
  ));
33
33
 
34
+ // src/utils/cli/configLoader.ts
35
+ var configLoader_exports = {};
36
+ __export(configLoader_exports, {
37
+ loadConfig: () => loadConfig
38
+ });
39
+ function validateConfig(config) {
40
+ const errors = [];
41
+ if (!config || typeof config !== "object") {
42
+ errors.push("Config must be an object");
43
+ return { valid: false, errors };
44
+ }
45
+ const cfg = config;
46
+ if (cfg.audit !== void 0) {
47
+ if (typeof cfg.audit !== "object" || cfg.audit === null) {
48
+ errors.push("audit must be an object");
49
+ } else {
50
+ if (cfg.audit.urls !== void 0) {
51
+ if (!Array.isArray(cfg.audit.urls)) {
52
+ errors.push("audit.urls must be an array");
53
+ } else if (cfg.audit.urls.some((url) => typeof url !== "string")) {
54
+ errors.push("audit.urls must contain only strings");
55
+ }
56
+ }
57
+ if (cfg.audit.output !== void 0) {
58
+ if (typeof cfg.audit.output !== "object") {
59
+ errors.push("audit.output must be an object");
60
+ } else {
61
+ const output = cfg.audit.output;
62
+ if (output.format !== void 0) {
63
+ if (!["json", "csv", "html", "all"].includes(output.format)) {
64
+ errors.push("audit.output.format must be one of: json, csv, html, all");
65
+ }
66
+ }
67
+ if (output.out !== void 0 && typeof output.out !== "string") {
68
+ errors.push("audit.output.out must be a string");
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ if (cfg.test !== void 0) {
75
+ if (typeof cfg.test !== "object" || cfg.test === null) {
76
+ errors.push("test must be an object");
77
+ } else {
78
+ if (cfg.test.components !== void 0) {
79
+ if (!Array.isArray(cfg.test.components)) {
80
+ errors.push("test.components must be an array");
81
+ } else {
82
+ cfg.test.components.forEach((comp, idx) => {
83
+ if (typeof comp !== "object" || comp === null) {
84
+ errors.push(`test.components[${idx}] must be an object`);
85
+ } else {
86
+ if (typeof comp.name !== "string") {
87
+ errors.push(`test.components[${idx}].name must be a string`);
88
+ }
89
+ if (comp.path !== void 0 && typeof comp.path !== "string") {
90
+ errors.push(`test.components[${idx}].path must be a string when provided`);
91
+ }
92
+ if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
93
+ errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
94
+ }
95
+ }
96
+ });
97
+ }
98
+ }
99
+ if (cfg.test.strictness !== void 0) {
100
+ if (!["minimal", "balanced", "strict", "paranoid"].includes(cfg.test.strictness)) {
101
+ errors.push("test.strictness must be one of: minimal, balanced, strict, paranoid");
102
+ }
103
+ }
104
+ }
105
+ }
106
+ return { valid: errors.length === 0, errors };
107
+ }
108
+ async function loadConfigFile(filePath) {
109
+ try {
110
+ const ext = import_path.default.extname(filePath);
111
+ if (ext === ".json") {
112
+ const content = await import_fs_extra.default.readFile(filePath, "utf-8");
113
+ return JSON.parse(content);
114
+ } else if ([".js", ".mjs", ".cjs", ".ts"].includes(ext)) {
115
+ const imported = await import(filePath);
116
+ return imported.default || imported;
117
+ }
118
+ return null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+ async function loadConfig(cwd = process.cwd()) {
124
+ const configNames = [
125
+ "ariaease.config.js",
126
+ "ariaease.config.mjs",
127
+ "ariaease.config.cjs",
128
+ "ariaease.config.json",
129
+ "ariaease.config.ts"
130
+ ];
131
+ let loadedConfig = null;
132
+ let foundPath = null;
133
+ const errors = [];
134
+ for (const name of configNames) {
135
+ const configPath = import_path.default.resolve(cwd, name);
136
+ if (await import_fs_extra.default.pathExists(configPath)) {
137
+ foundPath = configPath;
138
+ loadedConfig = await loadConfigFile(configPath);
139
+ if (loadedConfig === null) {
140
+ errors.push(`Found config at ${name} but failed to load it. Check for syntax errors.`);
141
+ continue;
142
+ }
143
+ const validation = validateConfig(loadedConfig);
144
+ if (!validation.valid) {
145
+ errors.push(`Config validation failed in ${name}:`);
146
+ errors.push(...validation.errors.map((err) => ` - ${err}`));
147
+ loadedConfig = null;
148
+ continue;
149
+ }
150
+ break;
151
+ }
152
+ }
153
+ return {
154
+ config: loadedConfig || {},
155
+ configPath: loadedConfig ? foundPath : null,
156
+ errors
157
+ };
158
+ }
159
+ var import_path, import_fs_extra;
160
+ var init_configLoader = __esm({
161
+ "src/utils/cli/configLoader.ts"() {
162
+ "use strict";
163
+ import_path = __toESM(require("path"), 1);
164
+ import_fs_extra = __toESM(require("fs-extra"), 1);
165
+ }
166
+ });
167
+
34
168
  // src/utils/cli/badgeHelper.ts
35
169
  var badgeHelper_exports = {};
36
170
  __export(badgeHelper_exports, {
@@ -432,10 +566,11 @@ var init_ContractReporter = __esm({
432
566
  componentName = "";
433
567
  staticPasses = 0;
434
568
  staticFailures = 0;
569
+ staticWarnings = 0;
435
570
  dynamicResults = [];
436
571
  totalTests = 0;
437
572
  skipped = 0;
438
- optionalSuggestions = 0;
573
+ warnings = 0;
439
574
  isPlaywright = false;
440
575
  apgUrl = "https://www.w3.org/WAI/ARIA/apg/";
441
576
  hasPrintedStaticSection = false;
@@ -462,23 +597,27 @@ ${"\u2550".repeat(60)}`);
462
597
  this.log(`${"\u2550".repeat(60)}
463
598
  `);
464
599
  }
465
- reportStatic(passes, failures) {
600
+ reportStatic(passes, failures, warnings = 0) {
466
601
  this.staticPasses = passes;
467
602
  this.staticFailures = failures;
603
+ this.staticWarnings = warnings;
468
604
  }
469
605
  /**
470
606
  * Report individual static test pass
471
607
  */
472
- reportStaticTest(description, passed, failureMessage) {
608
+ reportStaticTest(description, status, failureMessage, level) {
473
609
  if (!this.hasPrintedStaticSection) {
474
610
  this.log(`${"\u2500".repeat(60)}`);
475
611
  this.log(`\u{1F9EA} Static Assertions`);
476
612
  this.log(`${"\u2500".repeat(60)}`);
477
613
  this.hasPrintedStaticSection = true;
478
614
  }
479
- const icon = passed ? "\u2713" : "\u2717";
615
+ const icon = status === "pass" ? "\u2713" : status === "warn" ? "\u26A0" : status === "skip" ? "\u25CB" : "\u2717";
480
616
  this.log(` ${icon} ${description}`);
481
- if (!passed && failureMessage) {
617
+ if (level) {
618
+ this.log(` \u21B3 level=${level}`);
619
+ }
620
+ if ((status === "fail" || status === "warn" || status === "skip") && failureMessage) {
482
621
  this.log(` \u21B3 ${failureMessage}`);
483
622
  }
484
623
  }
@@ -497,23 +636,26 @@ ${"\u2550".repeat(60)}`);
497
636
  description: test.description,
498
637
  status,
499
638
  failureMessage,
500
- isOptional: test.isOptional
639
+ level: test.level
501
640
  };
502
641
  if (status === "skip") {
503
642
  result.skipReason = "Requires real browser (addEventListener events)";
504
643
  }
505
644
  this.dynamicResults.push(result);
506
- const icons = { pass: "\u2713", fail: "\u2717", skip: "\u25CB", "optional-fail": "\u25CB" };
507
- const prefix = test.isOptional ? "[OPTIONAL] " : "";
508
- this.log(` ${icons[status]} ${prefix}${test.description}`);
645
+ const icons = { pass: "\u2713", fail: "\u2717", warn: "\u26A0", skip: "\u25CB" };
646
+ const levelPrefix = test.level ? `[${test.level.toUpperCase()}] ` : "";
647
+ this.log(` ${icons[status]} ${levelPrefix}${test.description}`);
509
648
  if (status === "skip" && !this.isPlaywright) {
510
649
  this.log(` \u21B3 Skipped in jsdom (runs in Playwright)`);
511
650
  }
512
- if (status === "fail" && failureMessage && !test.isOptional) {
651
+ if (status === "fail" && failureMessage) {
513
652
  this.log(` \u21B3 ${failureMessage}`);
514
653
  }
515
- if (status === "optional-fail") {
516
- this.log(` \u21B3 Not implemented (recommended for enhanced UX)`);
654
+ if (status === "warn" && failureMessage) {
655
+ this.log(` \u21B3 ${failureMessage}`);
656
+ }
657
+ if (status === "skip" && failureMessage) {
658
+ this.log(` \u21B3 ${failureMessage}`);
517
659
  }
518
660
  }
519
661
  /**
@@ -537,29 +679,29 @@ ${"\u2500".repeat(60)}`);
537
679
  this.log("");
538
680
  });
539
681
  }
540
- /**
541
- * Report optional features that aren't implemented
542
- */
543
- reportOptionalSuggestions() {
544
- const suggestions = this.dynamicResults.filter((r) => r.status === "optional-fail");
545
- if (suggestions.length === 0) return;
682
+ reportWarnings() {
683
+ const warnings = this.dynamicResults.filter((r) => r.status === "warn");
684
+ if (warnings.length === 0 && this.staticWarnings === 0) return;
546
685
  this.log(`
547
686
  ${"\u2500".repeat(60)}`);
548
- this.log(`\u{1F4A1} Optional Enhancements (${suggestions.length}):
687
+ this.log(`\u26A0\uFE0F Warnings (${this.staticWarnings + warnings.length}):
549
688
  `);
550
- this.log(`These features are optional per APG guidelines but recommended`);
551
- this.log(`for improved user experience and keyboard interaction:
689
+ this.log(`These checks are failing but treated as warnings under the active strictness mode.
552
690
  `);
553
- suggestions.forEach((test, index) => {
691
+ warnings.forEach((test, index) => {
554
692
  this.log(`${index + 1}. ${test.description}`);
555
693
  if (test.failureMessage) {
556
694
  this.log(` \u21B3 ${test.failureMessage}`);
557
695
  }
696
+ if (test.level) {
697
+ this.log(` \u21B3 level=${test.level}`);
698
+ }
558
699
  });
559
- this.log(`
560
- \u2728 Consider implementing these for better accessibility`);
561
- this.log(` Reference: ${this.apgUrl}
700
+ if (this.apgUrl) {
701
+ this.log(`
702
+ Reference: ${this.apgUrl}
562
703
  `);
704
+ }
563
705
  }
564
706
  /**
565
707
  * Report skipped tests with helpful context
@@ -589,41 +731,42 @@ ${"\u2500".repeat(60)}`);
589
731
  const duration = Date.now() - this.startTime;
590
732
  const dynamicPasses = this.dynamicResults.filter((r) => r.status === "pass").length;
591
733
  const dynamicFailures = this.dynamicResults.filter((r) => r.status === "fail").length;
734
+ const dynamicWarnings = this.dynamicResults.filter((r) => r.status === "warn").length;
592
735
  this.skipped = this.dynamicResults.filter((r) => r.status === "skip").length;
593
- this.optionalSuggestions = this.dynamicResults.filter((r) => r.status === "optional-fail").length;
736
+ this.warnings = this.staticWarnings + dynamicWarnings;
594
737
  const totalPasses = this.staticPasses + dynamicPasses;
595
738
  const totalFailures = this.staticFailures + dynamicFailures;
596
- const totalRun = totalPasses + totalFailures;
739
+ const totalRun = totalPasses + totalFailures + this.warnings;
597
740
  if (failures.length > 0) {
598
741
  this.reportFailures(failures);
599
742
  }
600
- this.reportOptionalSuggestions();
743
+ this.reportWarnings();
601
744
  this.reportSkipped();
602
745
  this.log(`
603
746
  ${"\u2550".repeat(60)}`);
604
747
  this.log(`\u{1F4CA} Summary
605
748
  `);
606
- if (totalFailures === 0 && this.skipped === 0 && this.optionalSuggestions === 0) {
749
+ if (totalFailures === 0 && this.skipped === 0 && this.warnings === 0) {
607
750
  this.log(`\u2705 All ${totalRun} tests passed!`);
608
751
  this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
609
752
  } else if (totalFailures === 0) {
610
- this.log(`\u2705 ${totalPasses}/${totalRun} required tests passed`);
753
+ this.log(`\u2705 ${totalPasses}/${totalRun} tests passed`);
611
754
  if (this.skipped > 0) {
612
755
  this.log(`\u25CB ${this.skipped} tests skipped`);
613
756
  }
614
- if (this.optionalSuggestions > 0) {
615
- this.log(`\u{1F4A1} ${this.optionalSuggestions} optional enhancement${this.optionalSuggestions > 1 ? "s" : ""} suggested`);
757
+ if (this.warnings > 0) {
758
+ this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
616
759
  }
617
760
  this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
618
761
  } else {
619
762
  this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
620
763
  this.log(`\u2705 ${totalPasses} test${totalPasses > 1 ? "s" : ""} passed`);
764
+ if (this.warnings > 0) {
765
+ this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
766
+ }
621
767
  if (this.skipped > 0) {
622
768
  this.log(`\u25CB ${this.skipped} test${this.skipped > 1 ? "s" : ""} skipped`);
623
769
  }
624
- if (this.optionalSuggestions > 0) {
625
- this.log(`\u{1F4A1} ${this.optionalSuggestions} optional enhancement${this.optionalSuggestions > 1 ? "s" : ""} suggested`);
626
- }
627
770
  }
628
771
  this.log(`\u23F1\uFE0F Duration: ${duration}ms`);
629
772
  this.log(`${"\u2550".repeat(60)}
@@ -660,9 +803,56 @@ ${"\u2550".repeat(60)}`);
660
803
  }
661
804
  });
662
805
 
806
+ // src/utils/test/src/strictness.ts
807
+ function normalizeLevel(level) {
808
+ if (level === "required" || level === "recommended" || level === "optional") {
809
+ return level;
810
+ }
811
+ return FALLBACK_LEVEL;
812
+ }
813
+ function normalizeStrictness(strictness) {
814
+ if (strictness === "minimal" || strictness === "balanced" || strictness === "strict" || strictness === "paranoid") {
815
+ return strictness;
816
+ }
817
+ return "balanced";
818
+ }
819
+ function resolveEnforcement(level, strictness) {
820
+ const matrix = {
821
+ minimal: {
822
+ required: "error",
823
+ recommended: "ignore",
824
+ optional: "ignore"
825
+ },
826
+ balanced: {
827
+ required: "error",
828
+ recommended: "warning",
829
+ optional: "ignore"
830
+ },
831
+ strict: {
832
+ required: "error",
833
+ recommended: "error",
834
+ optional: "warning"
835
+ },
836
+ paranoid: {
837
+ required: "error",
838
+ recommended: "error",
839
+ optional: "error"
840
+ }
841
+ };
842
+ return matrix[strictness][level];
843
+ }
844
+ var FALLBACK_LEVEL;
845
+ var init_strictness = __esm({
846
+ "src/utils/test/src/strictness.ts"() {
847
+ "use strict";
848
+ FALLBACK_LEVEL = "required";
849
+ }
850
+ });
851
+
663
852
  // src/utils/test/src/contractTestRunner.ts
664
- async function runContractTests(componentName, component) {
853
+ async function runContractTests(componentName, component, strictness) {
665
854
  const reporter = new ContractReporter(false);
855
+ const strictnessMode = normalizeStrictness(strictness);
666
856
  const contractTyped = contract_default;
667
857
  const contractPath = contractTyped[componentName]?.path;
668
858
  if (!contractPath) {
@@ -676,19 +866,42 @@ async function runContractTests(componentName, component) {
676
866
  const failures = [];
677
867
  const passes = [];
678
868
  const skipped = [];
679
- const failuresBeforeStatic = failures.length;
869
+ const warnings = [];
870
+ const classifyFailure = (message, levelRaw) => {
871
+ const level = normalizeLevel(levelRaw);
872
+ const enforcement = resolveEnforcement(level, strictnessMode);
873
+ if (enforcement === "error") {
874
+ failures.push(message);
875
+ return { status: "fail", level, detail: message };
876
+ }
877
+ if (enforcement === "warning") {
878
+ warnings.push(message);
879
+ return { status: "warn", level, detail: message };
880
+ }
881
+ const ignoredMessage = `${message} (ignored by strictness=${strictnessMode}, level=${level})`;
882
+ skipped.push(ignoredMessage);
883
+ return { status: "skip", level, detail: ignoredMessage };
884
+ };
885
+ let staticPassed = 0;
886
+ let staticFailed = 0;
887
+ let staticWarnings = 0;
680
888
  for (const test of componentContract.static[0].assertions) {
681
889
  if (test.target !== "relative") {
890
+ const staticLevel = normalizeLevel(test.level);
682
891
  const selector = componentContract.selectors[test.target];
683
892
  if (!selector) {
684
- failures.push(`Selector for target ${test.target} not found.`);
685
- reporter.reportStaticTest(`${test.target} has required ARIA attributes`, false, `Selector for target ${test.target} not found.`);
893
+ const outcome = classifyFailure(`Selector for target ${test.target} not found.`, test.level);
894
+ if (outcome.status === "fail") staticFailed += 1;
895
+ if (outcome.status === "warn") staticWarnings += 1;
896
+ reporter.reportStaticTest(`${test.target} has required ARIA attributes`, outcome.status, outcome.detail, outcome.level);
686
897
  continue;
687
898
  }
688
899
  const target = component.querySelector(selector);
689
900
  if (!target) {
690
- failures.push(`Target ${test.target} not found.`);
691
- reporter.reportStaticTest(`${test.target} has required ARIA attributes`, false, `Target ${test.target} not found.`);
901
+ const outcome = classifyFailure(`Target ${test.target} not found.`, test.level);
902
+ if (outcome.status === "fail") staticFailed += 1;
903
+ if (outcome.status === "warn") staticWarnings += 1;
904
+ reporter.reportStaticTest(`${test.target} has required ARIA attributes`, outcome.status, outcome.detail, outcome.level);
692
905
  continue;
693
906
  }
694
907
  const attributeValue = target.getAttribute(test.attribute);
@@ -696,31 +909,34 @@ async function runContractTests(componentName, component) {
696
909
  const attributes = test.attribute.split(" | ");
697
910
  const hasAnyAttribute = attributes.some((attr) => target.hasAttribute(attr));
698
911
  if (!hasAnyAttribute) {
699
- failures.push(test.failureMessage + ` None of the attributes "${test.attribute}" are present.`);
700
- reporter.reportStaticTest(`${test.target} has ${test.attribute}`, false, test.failureMessage);
912
+ const outcome = classifyFailure(test.failureMessage + ` None of the attributes "${test.attribute}" are present.`, test.level);
913
+ if (outcome.status === "fail") staticFailed += 1;
914
+ if (outcome.status === "warn") staticWarnings += 1;
915
+ reporter.reportStaticTest(`${test.target} has ${test.attribute}`, outcome.status, outcome.detail, outcome.level);
701
916
  } else {
702
917
  passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
703
- reporter.reportStaticTest(`${test.target} has ${test.attribute}`, true);
918
+ staticPassed += 1;
919
+ reporter.reportStaticTest(`${test.target} has ${test.attribute}`, "pass", void 0, staticLevel);
704
920
  }
705
921
  } else if (!attributeValue || !test.expectedValue.split(" | ").includes(attributeValue)) {
706
- failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
707
- reporter.reportStaticTest(`${test.target} has ${test.attribute}="${test.expectedValue}"`, false, test.failureMessage);
922
+ const outcome = classifyFailure(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`, test.level);
923
+ if (outcome.status === "fail") staticFailed += 1;
924
+ if (outcome.status === "warn") staticWarnings += 1;
925
+ reporter.reportStaticTest(`${test.target} has ${test.attribute}="${test.expectedValue}"`, outcome.status, outcome.detail, outcome.level);
708
926
  } else {
709
927
  passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
710
- reporter.reportStaticTest(`${test.target} has ${test.attribute}="${attributeValue}"`, true);
928
+ staticPassed += 1;
929
+ reporter.reportStaticTest(`${test.target} has ${test.attribute}="${attributeValue}"`, "pass", void 0, staticLevel);
711
930
  }
712
931
  }
713
932
  }
714
933
  for (const dynamicTest of componentContract.dynamic) {
715
934
  skipped.push(dynamicTest.description);
716
- reporter.reportTest(dynamicTest, "skip");
935
+ reporter.reportTest({ description: dynamicTest.description, level: dynamicTest.level }, "skip");
717
936
  }
718
- const staticTotal = componentContract.static[0].assertions.length;
719
- const staticFailed = failures.length - failuresBeforeStatic;
720
- const staticPassed = Math.max(0, staticTotal - staticFailed);
721
- reporter.reportStatic(staticPassed, staticFailed);
937
+ reporter.reportStatic(staticPassed, staticFailed, staticWarnings);
722
938
  reporter.summary(failures);
723
- return { passes, failures, skipped };
939
+ return { passes, failures, skipped, warnings };
724
940
  }
725
941
  var import_promises, import_meta;
726
942
  var init_contractTestRunner = __esm({
@@ -729,6 +945,7 @@ var init_contractTestRunner = __esm({
729
945
  init_contract();
730
946
  import_promises = __toESM(require("fs/promises"), 1);
731
947
  init_ContractReporter();
948
+ init_strictness();
732
949
  import_meta = {};
733
950
  }
734
951
  });
@@ -1118,9 +1335,6 @@ var init_ActionExecutor = __esm({
1118
1335
  this.selectors = selectors;
1119
1336
  this.timeoutMs = timeoutMs;
1120
1337
  }
1121
- isOptionalMenuTarget(target) {
1122
- return ["submenu", "submenuTrigger", "submenuItems"].includes(target);
1123
- }
1124
1338
  /**
1125
1339
  * Check if error is due to browser/page being closed
1126
1340
  */
@@ -1252,10 +1466,9 @@ var init_ActionExecutor = __esm({
1252
1466
  const locator = this.page.locator(selector).first();
1253
1467
  const elementCount = await locator.count();
1254
1468
  if (elementCount === 0) {
1255
- const optionalMenuTarget = this.isOptionalMenuTarget(target);
1256
1469
  return {
1257
1470
  success: false,
1258
- error: optionalMenuTarget ? `${target} element not found (optional submenu test)` : `${target} element not found.`,
1471
+ error: `${target} element not found.`,
1259
1472
  shouldBreak: true
1260
1473
  // Signal to skip this test
1261
1474
  };
@@ -1569,10 +1782,11 @@ var contractTestRunnerPlaywright_exports = {};
1569
1782
  __export(contractTestRunnerPlaywright_exports, {
1570
1783
  runContractTestsPlaywright: () => runContractTestsPlaywright
1571
1784
  });
1572
- async function runContractTestsPlaywright(componentName, url) {
1785
+ async function runContractTestsPlaywright(componentName, url, strictness) {
1573
1786
  const reporter = new ContractReporter(true);
1574
1787
  const actionTimeoutMs = 400;
1575
1788
  const assertionTimeoutMs = 400;
1789
+ const strictnessMode = normalizeStrictness(strictness);
1576
1790
  const contractTyped = contract_default;
1577
1791
  const contractPath = contractTyped[componentName]?.path;
1578
1792
  const resolvedPath = new URL(contractPath, import_meta3.url).pathname;
@@ -1581,9 +1795,25 @@ async function runContractTestsPlaywright(componentName, url) {
1581
1795
  const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
1582
1796
  const apgUrl = componentContract.meta?.source?.apg;
1583
1797
  const failures = [];
1798
+ const warnings = [];
1584
1799
  const passes = [];
1585
1800
  const skipped = [];
1586
1801
  let page = null;
1802
+ const classifyFailure = (message, levelRaw) => {
1803
+ const level = normalizeLevel(levelRaw);
1804
+ const enforcement = resolveEnforcement(level, strictnessMode);
1805
+ if (enforcement === "error") {
1806
+ failures.push(message);
1807
+ return { status: "fail", level, detail: message };
1808
+ }
1809
+ if (enforcement === "warning") {
1810
+ warnings.push(message);
1811
+ return { status: "warn", level, detail: message };
1812
+ }
1813
+ const ignoredMessage = `${message} (ignored by strictness=${strictnessMode}, level=${level})`;
1814
+ skipped.push(ignoredMessage);
1815
+ return { status: "skip", level, detail: ignoredMessage };
1816
+ };
1587
1817
  try {
1588
1818
  page = await createTestPage();
1589
1819
  if (url) {
@@ -1629,35 +1859,37 @@ This usually means:
1629
1859
  });
1630
1860
  }
1631
1861
  const hasSubmenuCapability = componentName === "menu" && !!componentContract.selectors.submenuTrigger ? await page.locator(componentContract.selectors.submenuTrigger).count() > 0 : false;
1862
+ let staticPassed = 0;
1632
1863
  let staticFailed = 0;
1864
+ let staticWarnings = 0;
1633
1865
  const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1634
1866
  for (const test of componentContract.static[0]?.assertions || []) {
1635
1867
  if (test.target === "relative") continue;
1636
1868
  const staticDescription = `${test.target}${test.attribute ? ` (${test.attribute})` : ""}`;
1869
+ const staticLevel = normalizeLevel(test.level);
1637
1870
  if (componentName === "menu" && test.target === "submenuTrigger" && !hasSubmenuCapability) {
1638
- passes.push(`Skipping submenu static assertion for ${test.target}: no submenu capability detected in rendered component.`);
1639
- reporter.reportStaticTest(staticDescription, true);
1871
+ const skipMessage = `Skipping submenu static assertion for ${test.target}: no submenu capability detected in rendered component.`;
1872
+ skipped.push(skipMessage);
1873
+ reporter.reportStaticTest(staticDescription, "skip", skipMessage, staticLevel);
1640
1874
  continue;
1641
1875
  }
1642
1876
  const targetSelector = componentContract.selectors[test.target];
1643
1877
  if (!targetSelector) {
1644
1878
  const failure = `Selector for target ${test.target} not found.`;
1645
- failures.push(failure);
1646
- staticFailed += 1;
1647
- reporter.reportStaticTest(staticDescription, false, failure);
1879
+ const outcome = classifyFailure(failure, test.level);
1880
+ if (outcome.status === "fail") staticFailed += 1;
1881
+ if (outcome.status === "warn") staticWarnings += 1;
1882
+ reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
1648
1883
  continue;
1649
1884
  }
1650
1885
  const target = page.locator(targetSelector).first();
1651
1886
  const exists = await target.count() > 0;
1652
1887
  if (!exists) {
1653
- if (test.isOptional === true) {
1654
- reporter.reportStaticTest(staticDescription, true);
1655
- continue;
1656
- }
1657
1888
  const failure = `Target ${test.target} not found.`;
1658
- failures.push(failure);
1659
- staticFailed += 1;
1660
- reporter.reportStaticTest(staticDescription, false, failure);
1889
+ const outcome = classifyFailure(failure, test.level);
1890
+ if (outcome.status === "fail") staticFailed += 1;
1891
+ if (outcome.status === "warn") staticWarnings += 1;
1892
+ reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
1661
1893
  continue;
1662
1894
  }
1663
1895
  const isRedundantCheck = (selector, attrName, expectedVal) => {
@@ -1692,19 +1924,23 @@ This usually means:
1692
1924
  }
1693
1925
  if (!hasAny && !allRedundant) {
1694
1926
  const failure = test.failureMessage + ` None of the attributes "${test.attribute}" are present.`;
1695
- failures.push(failure);
1696
- staticFailed += 1;
1697
- reporter.reportStaticTest(staticDescription, false, failure);
1927
+ const outcome = classifyFailure(failure, test.level);
1928
+ if (outcome.status === "fail") staticFailed += 1;
1929
+ if (outcome.status === "warn") staticWarnings += 1;
1930
+ reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
1698
1931
  } else if (!allRedundant && hasAny) {
1699
1932
  passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
1700
- reporter.reportStaticTest(staticDescription, true);
1933
+ staticPassed += 1;
1934
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1701
1935
  } else {
1702
- reporter.reportStaticTest(staticDescription, true);
1936
+ staticPassed += 1;
1937
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1703
1938
  }
1704
1939
  } else {
1705
1940
  if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
1706
1941
  passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
1707
- reporter.reportStaticTest(staticDescription, true);
1942
+ staticPassed += 1;
1943
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1708
1944
  } else {
1709
1945
  const result = await staticAssertionRunner.validateAttribute(
1710
1946
  target,
@@ -1716,11 +1952,13 @@ This usually means:
1716
1952
  );
1717
1953
  if (result.success && result.passMessage) {
1718
1954
  passes.push(result.passMessage);
1719
- reporter.reportStaticTest(staticDescription, true);
1955
+ staticPassed += 1;
1956
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1720
1957
  } else if (!result.success && result.failMessage) {
1721
- failures.push(result.failMessage);
1722
- staticFailed += 1;
1723
- reporter.reportStaticTest(staticDescription, false, result.failMessage);
1958
+ const outcome = classifyFailure(result.failMessage, test.level);
1959
+ if (outcome.status === "fail") staticFailed += 1;
1960
+ if (outcome.status === "warn") staticWarnings += 1;
1961
+ reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
1724
1962
  }
1725
1963
  }
1726
1964
  }
@@ -1735,6 +1973,9 @@ This usually means:
1735
1973
  }
1736
1974
  const { action, assertions } = dynamicTest;
1737
1975
  const failuresBeforeTest = failures.length;
1976
+ const warningsBeforeTest = warnings.length;
1977
+ const skippedBeforeTest = skipped.length;
1978
+ const dynamicLevel = normalizeLevel(dynamicTest.level);
1738
1979
  try {
1739
1980
  await strategy.resetState(page);
1740
1981
  } catch (error) {
@@ -1744,13 +1985,15 @@ This usually means:
1744
1985
  }
1745
1986
  const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
1746
1987
  if (shouldSkipTest) {
1747
- reporter.reportTest(dynamicTest, "skip", `Skipping test - component-specific conditions not met`);
1988
+ const skipMessage = `Skipping test - component-specific conditions not met`;
1989
+ skipped.push(skipMessage);
1990
+ reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "skip", skipMessage);
1748
1991
  continue;
1749
1992
  }
1750
1993
  const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
1751
1994
  const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1752
- let shouldSkipCurrentTest = false;
1753
1995
  let shouldAbortCurrentTest = false;
1996
+ let actionOutcome = null;
1754
1997
  for (const act of action) {
1755
1998
  if (!page || page.isClosed()) {
1756
1999
  failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
@@ -1772,27 +2015,20 @@ This usually means:
1772
2015
  continue;
1773
2016
  }
1774
2017
  if (!result.success) {
1775
- if (result.shouldBreak) {
1776
- if (result.error?.includes("optional submenu test")) {
1777
- reporter.reportTest(dynamicTest, "skip", result.error);
1778
- shouldSkipCurrentTest = true;
1779
- } else if (result.error) {
1780
- failures.push(result.error);
1781
- shouldAbortCurrentTest = true;
1782
- }
1783
- break;
1784
- }
1785
2018
  if (result.error) {
1786
- failures.push(result.error);
2019
+ const outcome = classifyFailure(result.error, dynamicTest.level);
2020
+ actionOutcome = { status: outcome.status, detail: outcome.detail };
1787
2021
  }
1788
- continue;
2022
+ shouldAbortCurrentTest = true;
2023
+ break;
1789
2024
  }
1790
2025
  }
1791
- if (shouldSkipCurrentTest) {
1792
- continue;
1793
- }
1794
2026
  if (shouldAbortCurrentTest) {
1795
- reporter.reportTest(dynamicTest, "fail", failures[failures.length - 1]);
2027
+ reporter.reportTest(
2028
+ { description: dynamicTest.description, level: dynamicLevel },
2029
+ actionOutcome?.status || "fail",
2030
+ actionOutcome?.detail || failures[failures.length - 1]
2031
+ );
1796
2032
  continue;
1797
2033
  }
1798
2034
  for (const assertion of assertions) {
@@ -1800,22 +2036,39 @@ This usually means:
1800
2036
  if (result.success && result.passMessage) {
1801
2037
  passes.push(result.passMessage);
1802
2038
  } else if (!result.success && result.failMessage) {
1803
- failures.push(result.failMessage);
2039
+ const assertionLevel = normalizeLevel(assertion.level || dynamicTest.level);
2040
+ const outcome = classifyFailure(result.failMessage, assertionLevel);
2041
+ if (outcome.status === "skip") {
2042
+ continue;
2043
+ }
1804
2044
  }
1805
2045
  }
1806
2046
  const failuresAfterTest = failures.length;
1807
- const testPassed = failuresAfterTest === failuresBeforeTest;
1808
- const failureMessage = testPassed ? void 0 : failures[failures.length - 1];
1809
- if (dynamicTest.isOptional === true && !testPassed) {
1810
- failures.pop();
1811
- reporter.reportTest(dynamicTest, "optional-fail", failureMessage);
2047
+ const warningsAfterTest = warnings.length;
2048
+ const skippedAfterTest = skipped.length;
2049
+ if (failuresAfterTest > failuresBeforeTest) {
2050
+ reporter.reportTest(
2051
+ { description: dynamicTest.description, level: dynamicLevel },
2052
+ "fail",
2053
+ failures[failures.length - 1]
2054
+ );
2055
+ } else if (warningsAfterTest > warningsBeforeTest) {
2056
+ reporter.reportTest(
2057
+ { description: dynamicTest.description, level: dynamicLevel },
2058
+ "warn",
2059
+ warnings[warnings.length - 1]
2060
+ );
2061
+ } else if (skippedAfterTest > skippedBeforeTest) {
2062
+ reporter.reportTest(
2063
+ { description: dynamicTest.description, level: dynamicLevel },
2064
+ "skip",
2065
+ skipped[skipped.length - 1]
2066
+ );
1812
2067
  } else {
1813
- reporter.reportTest(dynamicTest, testPassed ? "pass" : "fail", failureMessage);
2068
+ reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "pass");
1814
2069
  }
1815
2070
  }
1816
- const staticTotal = componentContract.static[0].assertions.length;
1817
- const staticPassed = Math.max(0, staticTotal - staticFailed);
1818
- reporter.reportStatic(staticPassed, staticFailed);
2071
+ reporter.reportStatic(staticPassed, staticFailed, staticWarnings);
1819
2072
  reporter.summary(failures);
1820
2073
  } catch (error) {
1821
2074
  if (error instanceof Error) {
@@ -1840,7 +2093,7 @@ Make sure your dev server is running at ${url}`);
1840
2093
  } finally {
1841
2094
  if (page) await page.close();
1842
2095
  }
1843
- return { passes, failures, skipped };
2096
+ return { passes, failures, skipped, warnings };
1844
2097
  }
1845
2098
  var import_fs2, import_meta3;
1846
2099
  var init_contractTestRunnerPlaywright = __esm({
@@ -1853,12 +2106,13 @@ var init_contractTestRunnerPlaywright = __esm({
1853
2106
  init_ContractReporter();
1854
2107
  init_ActionExecutor();
1855
2108
  init_AssertionRunner();
2109
+ init_strictness();
1856
2110
  import_meta3 = {};
1857
2111
  }
1858
2112
  });
1859
2113
 
1860
2114
  // src/utils/test/src/test.ts
1861
- async function testUiComponent(componentName, component, url) {
2115
+ async function testUiComponent(componentName, component, url, options = {}) {
1862
2116
  if (!componentName || typeof componentName !== "string") {
1863
2117
  throw new Error("\u274C testUiComponent requires a valid componentName (string)");
1864
2118
  }
@@ -1895,6 +2149,17 @@ Error: ${error instanceof Error ? error.message : String(error)}`
1895
2149
  }
1896
2150
  return null;
1897
2151
  }
2152
+ let strictness = normalizeStrictness(options.strictness);
2153
+ if (options.strictness === void 0 && typeof window === "undefined") {
2154
+ try {
2155
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_configLoader(), configLoader_exports));
2156
+ const { config } = await loadConfig2(process.cwd());
2157
+ const componentStrictness = config.test?.components?.find((comp) => comp?.name === componentName)?.strictness;
2158
+ strictness = normalizeStrictness(componentStrictness ?? config.test?.strictness);
2159
+ } catch {
2160
+ strictness = "balanced";
2161
+ }
2162
+ }
1898
2163
  let contract;
1899
2164
  try {
1900
2165
  if (url) {
@@ -1902,7 +2167,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
1902
2167
  if (devServerUrl) {
1903
2168
  console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
1904
2169
  const { runContractTestsPlaywright: runContractTestsPlaywright2 } = await Promise.resolve().then(() => (init_contractTestRunnerPlaywright(), contractTestRunnerPlaywright_exports));
1905
- contract = await runContractTestsPlaywright2(componentName, devServerUrl);
2170
+ contract = await runContractTestsPlaywright2(componentName, devServerUrl, strictness);
1906
2171
  } else {
1907
2172
  throw new Error(
1908
2173
  `\u274C Dev server not running at ${url}
@@ -1911,7 +2176,7 @@ Please start your dev server and try again.`
1911
2176
  }
1912
2177
  } else if (component) {
1913
2178
  console.log(`\u{1F3AD} Running component contract tests in JSDOM mode`);
1914
- contract = await runContractTests(componentName, component);
2179
+ contract = await runContractTests(componentName, component, strictness);
1915
2180
  } else {
1916
2181
  throw new Error("\u274C Either component or URL must be provided");
1917
2182
  }
@@ -1965,6 +2230,7 @@ var init_test2 = __esm({
1965
2230
  import_jest_axe = require("jest-axe");
1966
2231
  init_contractTestRunner();
1967
2232
  init_playwrightTestHarness();
2233
+ init_strictness();
1968
2234
  runTest = async () => {
1969
2235
  return {
1970
2236
  passes: [],
@@ -2032,124 +2298,7 @@ var import_commander = require("commander");
2032
2298
  var import_chalk2 = __toESM(require("chalk"), 1);
2033
2299
  var import_path3 = __toESM(require("path"), 1);
2034
2300
  var import_fs_extra3 = __toESM(require("fs-extra"), 1);
2035
-
2036
- // src/utils/cli/configLoader.ts
2037
- var import_path = __toESM(require("path"), 1);
2038
- var import_fs_extra = __toESM(require("fs-extra"), 1);
2039
- function validateConfig(config) {
2040
- const errors = [];
2041
- if (!config || typeof config !== "object") {
2042
- errors.push("Config must be an object");
2043
- return { valid: false, errors };
2044
- }
2045
- const cfg = config;
2046
- if (cfg.audit !== void 0) {
2047
- if (typeof cfg.audit !== "object" || cfg.audit === null) {
2048
- errors.push("audit must be an object");
2049
- } else {
2050
- if (cfg.audit.urls !== void 0) {
2051
- if (!Array.isArray(cfg.audit.urls)) {
2052
- errors.push("audit.urls must be an array");
2053
- } else if (cfg.audit.urls.some((url) => typeof url !== "string")) {
2054
- errors.push("audit.urls must contain only strings");
2055
- }
2056
- }
2057
- if (cfg.audit.output !== void 0) {
2058
- if (typeof cfg.audit.output !== "object") {
2059
- errors.push("audit.output must be an object");
2060
- } else {
2061
- const output = cfg.audit.output;
2062
- if (output.format !== void 0) {
2063
- if (!["json", "csv", "html", "all"].includes(output.format)) {
2064
- errors.push("audit.output.format must be one of: json, csv, html, all");
2065
- }
2066
- }
2067
- if (output.out !== void 0 && typeof output.out !== "string") {
2068
- errors.push("audit.output.out must be a string");
2069
- }
2070
- }
2071
- }
2072
- }
2073
- }
2074
- if (cfg.test !== void 0) {
2075
- if (typeof cfg.test !== "object" || cfg.test === null) {
2076
- errors.push("test must be an object");
2077
- } else {
2078
- if (cfg.test.components !== void 0) {
2079
- if (!Array.isArray(cfg.test.components)) {
2080
- errors.push("test.components must be an array");
2081
- } else {
2082
- cfg.test.components.forEach((comp, idx) => {
2083
- if (typeof comp !== "object" || comp === null) {
2084
- errors.push(`test.components[${idx}] must be an object`);
2085
- } else {
2086
- if (typeof comp.name !== "string") {
2087
- errors.push(`test.components[${idx}].name must be a string`);
2088
- }
2089
- if (typeof comp.path !== "string") {
2090
- errors.push(`test.components[${idx}].path must be a string`);
2091
- }
2092
- }
2093
- });
2094
- }
2095
- }
2096
- }
2097
- }
2098
- return { valid: errors.length === 0, errors };
2099
- }
2100
- async function loadConfigFile(filePath) {
2101
- try {
2102
- const ext = import_path.default.extname(filePath);
2103
- if (ext === ".json") {
2104
- const content = await import_fs_extra.default.readFile(filePath, "utf-8");
2105
- return JSON.parse(content);
2106
- } else if ([".js", ".mjs", ".cjs", ".ts"].includes(ext)) {
2107
- const imported = await import(filePath);
2108
- return imported.default || imported;
2109
- }
2110
- return null;
2111
- } catch {
2112
- return null;
2113
- }
2114
- }
2115
- async function loadConfig(cwd = process.cwd()) {
2116
- const configNames = [
2117
- "ariaease.config.js",
2118
- "ariaease.config.mjs",
2119
- "ariaease.config.cjs",
2120
- "ariaease.config.json",
2121
- "ariaease.config.ts"
2122
- ];
2123
- let loadedConfig = null;
2124
- let foundPath = null;
2125
- const errors = [];
2126
- for (const name of configNames) {
2127
- const configPath = import_path.default.resolve(cwd, name);
2128
- if (await import_fs_extra.default.pathExists(configPath)) {
2129
- foundPath = configPath;
2130
- loadedConfig = await loadConfigFile(configPath);
2131
- if (loadedConfig === null) {
2132
- errors.push(`Found config at ${name} but failed to load it. Check for syntax errors.`);
2133
- continue;
2134
- }
2135
- const validation = validateConfig(loadedConfig);
2136
- if (!validation.valid) {
2137
- errors.push(`Config validation failed in ${name}:`);
2138
- errors.push(...validation.errors.map((err) => ` - ${err}`));
2139
- loadedConfig = null;
2140
- continue;
2141
- }
2142
- break;
2143
- }
2144
- }
2145
- return {
2146
- config: loadedConfig || {},
2147
- configPath: loadedConfig ? foundPath : null,
2148
- errors
2149
- };
2150
- }
2151
-
2152
- // src/utils/cli/cli.ts
2301
+ init_configLoader();
2153
2302
  init_badgeHelper();
2154
2303
  var program = new import_commander.Command();
2155
2304
  program.name("aria-ease").description("Run accessibility tests and audits").version("2.2.3");