aria-ease 6.8.0 → 6.9.1

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 (40) hide show
  1. package/README.md +68 -6
  2. package/bin/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
  3. package/bin/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
  4. package/bin/MenuComponentStrategy-JAMTCSNF.js +81 -0
  5. package/bin/TabsComponentStrategy-3SQURPMX.js +29 -0
  6. package/bin/buildContracts-GBOY7UXG.js +437 -0
  7. package/bin/{chunk-VPBHLMAS.js → chunk-LMSKLN5O.js} +21 -0
  8. package/bin/chunk-PK5L2SAF.js +17 -0
  9. package/bin/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
  10. package/bin/cli.cjs +991 -128
  11. package/bin/cli.js +33 -2
  12. package/bin/{configLoader-XRF6VM4J.js → configLoader-Q6A4JLKW.js} +1 -1
  13. package/{dist/contractTestRunnerPlaywright-UAOFNS7Z.js → bin/contractTestRunnerPlaywright-ZZNWDUYP.js} +270 -219
  14. package/bin/{test-WRIJHN6H.js → test-OND56UUL.js} +97 -10
  15. package/dist/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
  16. package/dist/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
  17. package/dist/MenuComponentStrategy-JAMTCSNF.js +81 -0
  18. package/dist/TabsComponentStrategy-3SQURPMX.js +29 -0
  19. package/dist/chunk-PK5L2SAF.js +17 -0
  20. package/dist/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
  21. package/dist/{configLoader-IT4PWCJB.js → configLoader-WTGJAP4Z.js} +21 -0
  22. package/{bin/contractTestRunnerPlaywright-UAOFNS7Z.js → dist/contractTestRunnerPlaywright-XBWJZMR3.js} +270 -219
  23. package/dist/index.cjs +794 -90
  24. package/dist/index.d.cts +136 -1
  25. package/dist/index.d.ts +136 -1
  26. package/dist/index.js +415 -10
  27. package/dist/src/utils/test/AccordionComponentStrategy-WRHZOEN6.js +38 -0
  28. package/dist/src/utils/test/ComboboxComponentStrategy-5AECQSRN.js +60 -0
  29. package/dist/src/utils/test/MenuComponentStrategy-VKZQYLBE.js +77 -0
  30. package/dist/src/utils/test/TabsComponentStrategy-BKG53SEV.js +26 -0
  31. package/dist/src/utils/test/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
  32. package/dist/src/utils/test/{configLoader-LD4RV2WQ.js → configLoader-YE2CYGDG.js} +21 -0
  33. package/dist/src/utils/test/{contractTestRunnerPlaywright-IRJOAEMT.js → contractTestRunnerPlaywright-LC5OAVXB.js} +262 -200
  34. package/dist/src/utils/test/dsl/index.cjs +320 -0
  35. package/dist/src/utils/test/dsl/index.d.cts +136 -0
  36. package/dist/src/utils/test/dsl/index.d.ts +136 -0
  37. package/dist/src/utils/test/dsl/index.js +318 -0
  38. package/dist/src/utils/test/index.cjs +472 -88
  39. package/dist/src/utils/test/index.js +97 -12
  40. package/package.json +9 -3
package/bin/cli.cjs CHANGED
@@ -89,6 +89,9 @@ function validateConfig(config) {
89
89
  if (comp.path !== void 0 && typeof comp.path !== "string") {
90
90
  errors.push(`test.components[${idx}].path must be a string when provided`);
91
91
  }
92
+ if (comp.strategyPath !== void 0 && typeof comp.strategyPath !== "string") {
93
+ errors.push(`test.components[${idx}].strategyPath must be a string when provided`);
94
+ }
92
95
  if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
93
96
  errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
94
97
  }
@@ -103,6 +106,24 @@ function validateConfig(config) {
103
106
  }
104
107
  }
105
108
  }
109
+ if (cfg.contracts !== void 0) {
110
+ if (!Array.isArray(cfg.contracts)) {
111
+ errors.push("contracts must be an array");
112
+ } else {
113
+ cfg.contracts.forEach((contract, idx) => {
114
+ if (typeof contract !== "object" || contract === null) {
115
+ errors.push(`contracts[${idx}] must be an object`);
116
+ } else {
117
+ if (typeof contract.src !== "string") {
118
+ errors.push(`contracts[${idx}].src is required and must be a string`);
119
+ }
120
+ if (contract.out !== void 0 && typeof contract.out !== "string") {
121
+ errors.push(`contracts[${idx}].out must be a string`);
122
+ }
123
+ }
124
+ });
125
+ }
126
+ }
106
127
  return { valid: errors.length === 0, errors };
107
128
  }
108
129
  async function loadConfigFile(filePath) {
@@ -572,11 +593,13 @@ var init_ContractReporter = __esm({
572
593
  skipped = 0;
573
594
  warnings = 0;
574
595
  isPlaywright = false;
596
+ isCustomContract = false;
575
597
  apgUrl = "https://www.w3.org/WAI/ARIA/apg/";
576
598
  hasPrintedStaticSection = false;
577
599
  hasPrintedDynamicSection = false;
578
- constructor(isPlaywright = false) {
600
+ constructor(isPlaywright = false, isCustomContract = false) {
579
601
  this.isPlaywright = isPlaywright;
602
+ this.isCustomContract = isCustomContract;
580
603
  }
581
604
  log(message) {
582
605
  process.stderr.write(message + "\n");
@@ -737,6 +760,13 @@ ${"\u2500".repeat(60)}`);
737
760
  const totalPasses = this.staticPasses + dynamicPasses;
738
761
  const totalFailures = this.staticFailures + dynamicFailures;
739
762
  const totalRun = totalPasses + totalFailures + this.warnings;
763
+ const getComponentMessage = () => {
764
+ const componentDisplayName = `${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)}`;
765
+ if (this.isCustomContract) {
766
+ return `${componentDisplayName} component validates against your custom accessibility policy \u2713`;
767
+ }
768
+ return `${componentDisplayName} component meets Aria-Ease baseline WAI-ARIA expectations \u2713`;
769
+ };
740
770
  if (failures.length > 0) {
741
771
  this.reportFailures(failures);
742
772
  }
@@ -748,7 +778,7 @@ ${"\u2550".repeat(60)}`);
748
778
  `);
749
779
  if (totalFailures === 0 && this.skipped === 0 && this.warnings === 0) {
750
780
  this.log(`\u2705 All ${totalRun} tests passed!`);
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`);
781
+ this.log(` ${getComponentMessage()}`);
752
782
  } else if (totalFailures === 0) {
753
783
  this.log(`\u2705 ${totalPasses}/${totalRun} tests passed`);
754
784
  if (this.skipped > 0) {
@@ -757,7 +787,7 @@ ${"\u2550".repeat(60)}`);
757
787
  if (this.warnings > 0) {
758
788
  this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
759
789
  }
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`);
790
+ this.log(` ${getComponentMessage()}`);
761
791
  } else {
762
792
  this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
763
793
  this.log(`\u2705 ${totalPasses} test${totalPasses > 1 ? "s" : ""} passed`);
@@ -861,7 +891,7 @@ async function runContractTests(componentName, component, strictness) {
861
891
  const resolvedPath = new URL(contractPath, import_meta.url).pathname;
862
892
  const contractData = await import_promises.default.readFile(resolvedPath, "utf-8");
863
893
  const componentContract = JSON.parse(contractData);
864
- const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
894
+ const totalTests = (componentContract.relationships?.length || 0) + (componentContract.static[0]?.assertions.length || 0) + componentContract.dynamic.length;
865
895
  reporter.start(componentName, totalTests);
866
896
  const failures = [];
867
897
  const passes = [];
@@ -885,6 +915,82 @@ async function runContractTests(componentName, component, strictness) {
885
915
  let staticPassed = 0;
886
916
  let staticFailed = 0;
887
917
  let staticWarnings = 0;
918
+ for (const rel of componentContract.relationships || []) {
919
+ const relationshipLevel = normalizeLevel(rel.level);
920
+ if (rel.type === "aria-reference") {
921
+ const fromSelector = componentContract.selectors[rel.from];
922
+ const toSelector = componentContract.selectors[rel.to];
923
+ const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
924
+ if (!fromSelector || !toSelector) {
925
+ const outcome = classifyFailure(`Relationship selector missing: from="${rel.from}" or to="${rel.to}" not found in selectors.`, rel.level);
926
+ if (outcome.status === "fail") staticFailed += 1;
927
+ if (outcome.status === "warn") staticWarnings += 1;
928
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
929
+ continue;
930
+ }
931
+ const fromTarget = component.querySelector(fromSelector);
932
+ const toTarget = component.querySelector(toSelector);
933
+ if (!fromTarget || !toTarget) {
934
+ const outcome = classifyFailure(`Relationship target not found: ${!fromTarget ? rel.from : rel.to}.`, rel.level);
935
+ if (outcome.status === "fail") staticFailed += 1;
936
+ if (outcome.status === "warn") staticWarnings += 1;
937
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
938
+ continue;
939
+ }
940
+ const toId = toTarget.getAttribute("id");
941
+ const attrValue = fromTarget.getAttribute(rel.attribute) || "";
942
+ if (!toId) {
943
+ const outcome = classifyFailure(`Relationship target "${rel.to}" must have an id for ${rel.attribute} validation.`, rel.level);
944
+ if (outcome.status === "fail") staticFailed += 1;
945
+ if (outcome.status === "warn") staticWarnings += 1;
946
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
947
+ continue;
948
+ }
949
+ const references = attrValue.split(/\s+/).filter(Boolean);
950
+ if (!references.includes(toId)) {
951
+ const outcome = classifyFailure(`Expected ${rel.from} ${rel.attribute} to reference id "${toId}", found "${attrValue}".`, rel.level);
952
+ if (outcome.status === "fail") staticFailed += 1;
953
+ if (outcome.status === "warn") staticWarnings += 1;
954
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
955
+ continue;
956
+ }
957
+ passes.push(`Relationship valid: ${rel.from}.${rel.attribute} -> ${rel.to} (id=${toId}).`);
958
+ staticPassed += 1;
959
+ reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
960
+ continue;
961
+ }
962
+ if (rel.type === "contains") {
963
+ const parentSelector = componentContract.selectors[rel.parent];
964
+ const childSelector = componentContract.selectors[rel.child];
965
+ const relDescription = `${rel.parent} contains ${rel.child}`;
966
+ if (!parentSelector || !childSelector) {
967
+ const outcome = classifyFailure(`Relationship selector missing: parent="${rel.parent}" or child="${rel.child}" not found in selectors.`, rel.level);
968
+ if (outcome.status === "fail") staticFailed += 1;
969
+ if (outcome.status === "warn") staticWarnings += 1;
970
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
971
+ continue;
972
+ }
973
+ const parentTarget = component.querySelector(parentSelector);
974
+ if (!parentTarget) {
975
+ const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
976
+ if (outcome.status === "fail") staticFailed += 1;
977
+ if (outcome.status === "warn") staticWarnings += 1;
978
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
979
+ continue;
980
+ }
981
+ const nestedChild = parentTarget.querySelector(childSelector);
982
+ if (!nestedChild) {
983
+ const outcome = classifyFailure(`Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`, rel.level);
984
+ if (outcome.status === "fail") staticFailed += 1;
985
+ if (outcome.status === "warn") staticWarnings += 1;
986
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
987
+ continue;
988
+ }
989
+ passes.push(`Relationship valid: ${rel.parent} contains ${rel.child}.`);
990
+ staticPassed += 1;
991
+ reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
992
+ }
993
+ }
888
994
  for (const test of componentContract.static[0].assertions) {
889
995
  if (test.target !== "relative") {
890
996
  const staticLevel = normalizeLevel(test.level);
@@ -1014,13 +1120,17 @@ var init_test = __esm({
1014
1120
  }
1015
1121
  });
1016
1122
 
1017
- // src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts
1018
- var ComboboxComponentStrategy;
1019
- var init_ComboboxComponentStrategy = __esm({
1020
- "src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts"() {
1123
+ // src/utils/test/src/component-strategies/MenuComponentStrategy.ts
1124
+ var MenuComponentStrategy_exports = {};
1125
+ __export(MenuComponentStrategy_exports, {
1126
+ MenuComponentStrategy: () => MenuComponentStrategy
1127
+ });
1128
+ var MenuComponentStrategy;
1129
+ var init_MenuComponentStrategy = __esm({
1130
+ "src/utils/test/src/component-strategies/MenuComponentStrategy.ts"() {
1021
1131
  "use strict";
1022
1132
  init_test();
1023
- ComboboxComponentStrategy = class {
1133
+ MenuComponentStrategy = class {
1024
1134
  constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
1025
1135
  this.mainSelector = mainSelector;
1026
1136
  this.selectors = selectors;
@@ -1057,19 +1167,36 @@ var init_ComboboxComponentStrategy = __esm({
1057
1167
  }
1058
1168
  if (!menuClosed) {
1059
1169
  throw new Error(
1060
- `\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
1170
+ `\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
1061
1171
  1. Escape key
1062
1172
  2. Clicking trigger
1063
1173
  3. Clicking outside
1064
- This indicates a problem with the combobox component's close functionality.`
1174
+ This indicates a problem with the menu component's close functionality.`
1065
1175
  );
1066
1176
  }
1067
1177
  if (this.selectors.input) {
1068
1178
  await page.locator(this.selectors.input).first().clear();
1069
1179
  }
1180
+ if (this.selectors.trigger) {
1181
+ const triggerElement = page.locator(this.selectors.trigger).first();
1182
+ await triggerElement.focus();
1183
+ }
1070
1184
  }
1071
- async shouldSkipTest() {
1072
- return false;
1185
+ async shouldSkipTest(test, page) {
1186
+ const requiresSubmenu = test.action.some(
1187
+ (act) => act.target === "submenu" || act.target === "submenuTrigger" || act.target === "submenuItems"
1188
+ ) || test.assertions.some(
1189
+ (assertion) => assertion.target === "submenu" || assertion.target === "submenuTrigger" || assertion.target === "submenuItems"
1190
+ );
1191
+ if (!requiresSubmenu) {
1192
+ return false;
1193
+ }
1194
+ const submenuTriggerSelector = this.selectors.submenuTrigger;
1195
+ if (!submenuTriggerSelector) {
1196
+ return true;
1197
+ }
1198
+ const submenuTriggerCount = await page.locator(submenuTriggerSelector).count();
1199
+ return submenuTriggerCount === 0;
1073
1200
  }
1074
1201
  getMainSelector() {
1075
1202
  return this.mainSelector;
@@ -1079,6 +1206,10 @@ This indicates a problem with the combobox component's close functionality.`
1079
1206
  });
1080
1207
 
1081
1208
  // src/utils/test/src/component-strategies/AccordionComponentStrategy.ts
1209
+ var AccordionComponentStrategy_exports = {};
1210
+ __export(AccordionComponentStrategy_exports, {
1211
+ AccordionComponentStrategy: () => AccordionComponentStrategy
1212
+ });
1082
1213
  var AccordionComponentStrategy;
1083
1214
  var init_AccordionComponentStrategy = __esm({
1084
1215
  "src/utils/test/src/component-strategies/AccordionComponentStrategy.ts"() {
@@ -1120,13 +1251,17 @@ var init_AccordionComponentStrategy = __esm({
1120
1251
  }
1121
1252
  });
1122
1253
 
1123
- // src/utils/test/src/component-strategies/MenuComponentStrategy.ts
1124
- var MenuComponentStrategy;
1125
- var init_MenuComponentStrategy = __esm({
1126
- "src/utils/test/src/component-strategies/MenuComponentStrategy.ts"() {
1254
+ // src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts
1255
+ var ComboboxComponentStrategy_exports = {};
1256
+ __export(ComboboxComponentStrategy_exports, {
1257
+ ComboboxComponentStrategy: () => ComboboxComponentStrategy
1258
+ });
1259
+ var ComboboxComponentStrategy;
1260
+ var init_ComboboxComponentStrategy = __esm({
1261
+ "src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts"() {
1127
1262
  "use strict";
1128
1263
  init_test();
1129
- MenuComponentStrategy = class {
1264
+ ComboboxComponentStrategy = class {
1130
1265
  constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
1131
1266
  this.mainSelector = mainSelector;
1132
1267
  this.selectors = selectors;
@@ -1139,60 +1274,43 @@ var init_MenuComponentStrategy = __esm({
1139
1274
  const popupElement = page.locator(popupSelector).first();
1140
1275
  const isPopupVisible = await popupElement.isVisible().catch(() => false);
1141
1276
  if (!isPopupVisible) return;
1142
- let menuClosed = false;
1277
+ let listBoxClosed = false;
1143
1278
  let closeSelector = this.selectors.input;
1144
1279
  if (!closeSelector && this.selectors.focusable) {
1145
1280
  closeSelector = this.selectors.focusable;
1146
1281
  } else if (!closeSelector) {
1147
- closeSelector = this.selectors.trigger;
1282
+ closeSelector = this.selectors.button;
1148
1283
  }
1149
1284
  if (closeSelector) {
1150
1285
  const closeElement = page.locator(closeSelector).first();
1151
1286
  await closeElement.focus();
1152
1287
  await page.keyboard.press("Escape");
1153
- menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
1288
+ listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
1154
1289
  }
1155
- if (!menuClosed && this.selectors.trigger) {
1156
- const triggerElement = page.locator(this.selectors.trigger).first();
1157
- await triggerElement.click({ timeout: this.actionTimeoutMs });
1158
- menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
1290
+ if (!listBoxClosed && this.selectors.button) {
1291
+ const buttonElement = page.locator(this.selectors.button).first();
1292
+ await buttonElement.click({ timeout: this.actionTimeoutMs });
1293
+ listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
1159
1294
  }
1160
- if (!menuClosed) {
1295
+ if (!listBoxClosed) {
1161
1296
  await page.mouse.click(10, 10);
1162
- menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
1297
+ listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
1163
1298
  }
1164
- if (!menuClosed) {
1299
+ if (!listBoxClosed) {
1165
1300
  throw new Error(
1166
- `\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
1301
+ `\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
1167
1302
  1. Escape key
1168
- 2. Clicking trigger
1303
+ 2. Clicking button
1169
1304
  3. Clicking outside
1170
- This indicates a problem with the menu component's close functionality.`
1305
+ This indicates a problem with the combobox component's close functionality.`
1171
1306
  );
1172
1307
  }
1173
1308
  if (this.selectors.input) {
1174
1309
  await page.locator(this.selectors.input).first().clear();
1175
1310
  }
1176
- if (this.selectors.trigger) {
1177
- const triggerElement = page.locator(this.selectors.trigger).first();
1178
- await triggerElement.focus();
1179
- }
1180
1311
  }
1181
- async shouldSkipTest(test, page) {
1182
- const requiresSubmenu = test.action.some(
1183
- (act) => act.target === "submenu" || act.target === "submenuTrigger" || act.target === "submenuItems"
1184
- ) || test.assertions.some(
1185
- (assertion) => assertion.target === "submenu" || assertion.target === "submenuTrigger" || assertion.target === "submenuItems"
1186
- );
1187
- if (!requiresSubmenu) {
1188
- return false;
1189
- }
1190
- const submenuTriggerSelector = this.selectors.submenuTrigger;
1191
- if (!submenuTriggerSelector) {
1192
- return true;
1193
- }
1194
- const submenuTriggerCount = await page.locator(submenuTriggerSelector).count();
1195
- return submenuTriggerCount === 0;
1312
+ async shouldSkipTest() {
1313
+ return false;
1196
1314
  }
1197
1315
  getMainSelector() {
1198
1316
  return this.mainSelector;
@@ -1202,6 +1320,10 @@ This indicates a problem with the menu component's close functionality.`
1202
1320
  });
1203
1321
 
1204
1322
  // src/utils/test/src/component-strategies/TabsComponentStrategy.ts
1323
+ var TabsComponentStrategy_exports = {};
1324
+ __export(TabsComponentStrategy_exports, {
1325
+ TabsComponentStrategy: () => TabsComponentStrategy
1326
+ });
1205
1327
  var TabsComponentStrategy;
1206
1328
  var init_TabsComponentStrategy = __esm({
1207
1329
  "src/utils/test/src/component-strategies/TabsComponentStrategy.ts"() {
@@ -1232,46 +1354,173 @@ var init_TabsComponentStrategy = __esm({
1232
1354
  }
1233
1355
  });
1234
1356
 
1357
+ // src/utils/test/src/StrategyRegistry.ts
1358
+ var import_path3, import_url, StrategyRegistry;
1359
+ var init_StrategyRegistry = __esm({
1360
+ "src/utils/test/src/StrategyRegistry.ts"() {
1361
+ "use strict";
1362
+ import_path3 = __toESM(require("path"), 1);
1363
+ import_url = require("url");
1364
+ StrategyRegistry = class {
1365
+ builtInStrategies = /* @__PURE__ */ new Map();
1366
+ constructor() {
1367
+ this.registerBuiltInStrategies();
1368
+ }
1369
+ /**
1370
+ * Register built-in strategies
1371
+ */
1372
+ registerBuiltInStrategies() {
1373
+ this.builtInStrategies.set(
1374
+ "menu",
1375
+ () => Promise.resolve().then(() => (init_MenuComponentStrategy(), MenuComponentStrategy_exports)).then(
1376
+ (m) => m.MenuComponentStrategy
1377
+ )
1378
+ );
1379
+ this.builtInStrategies.set(
1380
+ "accordion",
1381
+ () => Promise.resolve().then(() => (init_AccordionComponentStrategy(), AccordionComponentStrategy_exports)).then(
1382
+ (m) => m.AccordionComponentStrategy
1383
+ )
1384
+ );
1385
+ this.builtInStrategies.set(
1386
+ "combobox",
1387
+ () => Promise.resolve().then(() => (init_ComboboxComponentStrategy(), ComboboxComponentStrategy_exports)).then(
1388
+ (m) => m.ComboboxComponentStrategy
1389
+ )
1390
+ );
1391
+ this.builtInStrategies.set(
1392
+ "tabs",
1393
+ () => Promise.resolve().then(() => (init_TabsComponentStrategy(), TabsComponentStrategy_exports)).then(
1394
+ (m) => m.TabsComponentStrategy
1395
+ )
1396
+ );
1397
+ this.builtInStrategies.set(
1398
+ "combobox.listbox",
1399
+ () => Promise.resolve().then(() => (init_ComboboxComponentStrategy(), ComboboxComponentStrategy_exports)).then(
1400
+ (m) => m.ComboboxComponentStrategy
1401
+ )
1402
+ );
1403
+ }
1404
+ /**
1405
+ * Load a strategy - either from custom path or built-in registry
1406
+ * @param componentName - Component name (e.g., "menu", "accordion")
1407
+ * @param customStrategyPath - Optional custom strategy file path
1408
+ * @returns Strategy constructor function or null if not found
1409
+ */
1410
+ async loadStrategy(componentName, customStrategyPath, configBaseDir) {
1411
+ try {
1412
+ if (customStrategyPath) {
1413
+ try {
1414
+ const resolvedCustomPath = import_path3.default.isAbsolute(customStrategyPath) ? customStrategyPath : import_path3.default.resolve(configBaseDir || process.cwd(), customStrategyPath);
1415
+ const customModule = await import((0, import_url.pathToFileURL)(resolvedCustomPath).href);
1416
+ const strategy = customModule.default || customModule;
1417
+ if (!strategy) {
1418
+ throw new Error(`No default export found in ${customStrategyPath}`);
1419
+ }
1420
+ return strategy;
1421
+ } catch (error) {
1422
+ throw new Error(
1423
+ `Failed to load custom strategy from ${customStrategyPath}: ${error instanceof Error ? error.message : String(error)}`
1424
+ );
1425
+ }
1426
+ }
1427
+ const builtInLoader = this.builtInStrategies.get(componentName);
1428
+ if (!builtInLoader) {
1429
+ return null;
1430
+ }
1431
+ return builtInLoader();
1432
+ } catch (error) {
1433
+ throw new Error(
1434
+ `Strategy loading failed for ${componentName}: ${error instanceof Error ? error.message : String(error)}`
1435
+ );
1436
+ }
1437
+ }
1438
+ /**
1439
+ * Check if a strategy exists (either built-in or custom path provided)
1440
+ */
1441
+ has(componentName, customStrategyPath) {
1442
+ return !!customStrategyPath || this.builtInStrategies.has(componentName);
1443
+ }
1444
+ };
1445
+ }
1446
+ });
1447
+
1235
1448
  // src/utils/test/src/ComponentDetector.ts
1236
- var import_fs, import_meta2, ComponentDetector;
1449
+ var import_fs, import_path4, import_meta2, ComponentDetector;
1237
1450
  var init_ComponentDetector = __esm({
1238
1451
  "src/utils/test/src/ComponentDetector.ts"() {
1239
1452
  "use strict";
1240
- init_ComboboxComponentStrategy();
1241
- init_AccordionComponentStrategy();
1242
- init_MenuComponentStrategy();
1243
- init_TabsComponentStrategy();
1244
1453
  import_fs = require("fs");
1454
+ import_path4 = __toESM(require("path"), 1);
1245
1455
  init_contract();
1456
+ init_StrategyRegistry();
1246
1457
  import_meta2 = {};
1247
1458
  ComponentDetector = class {
1248
- static detect(componentName, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
1249
- const contractTyped = contract_default;
1250
- const contractPath = contractTyped[componentName]?.path;
1459
+ static strategyRegistry = new StrategyRegistry();
1460
+ static isComponentConfig(value) {
1461
+ return typeof value === "object" && value !== null;
1462
+ }
1463
+ /**
1464
+ * Detect and instantiate a component strategy
1465
+ * Supports:
1466
+ * - Built-in strategies (menu, accordion, combobox, tabs)
1467
+ * - Custom strategies via config (strategyPath)
1468
+ * - Custom contract paths via config (path)
1469
+ * @param componentName - Component name
1470
+ * @param componentConfig - Component config from ariaease.config.js
1471
+ * @param actionTimeoutMs - Action timeout in milliseconds
1472
+ * @param assertionTimeoutMs - Assertion timeout in milliseconds
1473
+ * @returns Instantiated ComponentStrategy or null
1474
+ */
1475
+ static async detect(componentName, componentConfig, actionTimeoutMs = 400, assertionTimeoutMs = 400, configBaseDir) {
1476
+ const typedComponentConfig = this.isComponentConfig(componentConfig) ? componentConfig : void 0;
1477
+ let contractPath = typedComponentConfig?.path;
1478
+ if (!contractPath) {
1479
+ const contractTyped = contract_default;
1480
+ contractPath = contractTyped[componentName]?.path;
1481
+ }
1251
1482
  if (!contractPath) {
1252
1483
  throw new Error(`Contract path not found for component: ${componentName}`);
1253
1484
  }
1254
- const resolvedPath = new URL(contractPath, import_meta2.url).pathname;
1485
+ const resolvedPath = (() => {
1486
+ if (import_path4.default.isAbsolute(contractPath)) return contractPath;
1487
+ if (configBaseDir) {
1488
+ const configResolved = import_path4.default.resolve(configBaseDir, contractPath);
1489
+ try {
1490
+ (0, import_fs.readFileSync)(configResolved, "utf-8");
1491
+ return configResolved;
1492
+ } catch {
1493
+ }
1494
+ }
1495
+ const cwdResolved = import_path4.default.resolve(process.cwd(), contractPath);
1496
+ try {
1497
+ (0, import_fs.readFileSync)(cwdResolved, "utf-8");
1498
+ return cwdResolved;
1499
+ } catch {
1500
+ return new URL(contractPath, import_meta2.url).pathname;
1501
+ }
1502
+ })();
1255
1503
  const contractData = (0, import_fs.readFileSync)(resolvedPath, "utf-8");
1256
1504
  const componentContract = JSON.parse(contractData);
1257
1505
  const selectors = componentContract.selectors;
1258
- if (componentName.includes("combobox")) {
1259
- const mainSelector = selectors.input || selectors.container;
1260
- return new ComboboxComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
1261
- }
1262
- if (componentName === "accordion") {
1263
- const mainSelector = selectors.trigger || selectors.container;
1264
- return new AccordionComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
1265
- }
1266
- if (componentName === "menu") {
1267
- const mainSelector = selectors.trigger || selectors.container;
1268
- return new MenuComponentStrategy(mainSelector, selectors, actionTimeoutMs, assertionTimeoutMs);
1506
+ const strategyClass = await this.strategyRegistry.loadStrategy(
1507
+ componentName,
1508
+ typedComponentConfig?.strategyPath,
1509
+ configBaseDir
1510
+ );
1511
+ if (!strategyClass) {
1512
+ return null;
1269
1513
  }
1514
+ const mainSelector = selectors.trigger || selectors.input || selectors.tablist || selectors.container;
1270
1515
  if (componentName === "tabs") {
1271
- const mainSelector = selectors.tablist || selectors.tab;
1272
- return new TabsComponentStrategy(mainSelector, selectors);
1516
+ return new strategyClass(mainSelector, selectors);
1273
1517
  }
1274
- return null;
1518
+ return new strategyClass(
1519
+ mainSelector,
1520
+ selectors,
1521
+ actionTimeoutMs,
1522
+ assertionTimeoutMs
1523
+ );
1275
1524
  }
1276
1525
  };
1277
1526
  }
@@ -1782,17 +2031,42 @@ var contractTestRunnerPlaywright_exports = {};
1782
2031
  __export(contractTestRunnerPlaywright_exports, {
1783
2032
  runContractTestsPlaywright: () => runContractTestsPlaywright
1784
2033
  });
1785
- async function runContractTestsPlaywright(componentName, url, strictness) {
1786
- const reporter = new ContractReporter(true);
2034
+ async function runContractTestsPlaywright(componentName, url, strictness, config, configBaseDir) {
2035
+ const componentConfig = config?.test?.components?.find((c) => c.name === componentName);
2036
+ const isCustomContract = !!componentConfig?.path;
2037
+ const reporter = new ContractReporter(true, isCustomContract);
1787
2038
  const actionTimeoutMs = 400;
1788
2039
  const assertionTimeoutMs = 400;
1789
2040
  const strictnessMode = normalizeStrictness(strictness);
1790
- const contractTyped = contract_default;
1791
- const contractPath = contractTyped[componentName]?.path;
1792
- const resolvedPath = new URL(contractPath, import_meta3.url).pathname;
2041
+ let contractPath = componentConfig?.path;
2042
+ if (!contractPath) {
2043
+ const contractTyped = contract_default;
2044
+ contractPath = contractTyped[componentName]?.path;
2045
+ }
2046
+ if (!contractPath) {
2047
+ throw new Error(`Contract path not found for component: ${componentName}`);
2048
+ }
2049
+ const resolvedPath = (() => {
2050
+ if (import_path5.default.isAbsolute(contractPath)) return contractPath;
2051
+ if (configBaseDir) {
2052
+ const configResolved = import_path5.default.resolve(configBaseDir, contractPath);
2053
+ try {
2054
+ (0, import_fs2.readFileSync)(configResolved, "utf-8");
2055
+ return configResolved;
2056
+ } catch {
2057
+ }
2058
+ }
2059
+ const cwdResolved = import_path5.default.resolve(process.cwd(), contractPath);
2060
+ try {
2061
+ (0, import_fs2.readFileSync)(cwdResolved, "utf-8");
2062
+ return cwdResolved;
2063
+ } catch {
2064
+ return new URL(contractPath, import_meta3.url).pathname;
2065
+ }
2066
+ })();
1793
2067
  const contractData = (0, import_fs2.readFileSync)(resolvedPath, "utf-8");
1794
2068
  const componentContract = JSON.parse(contractData);
1795
- const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
2069
+ const totalTests = (componentContract.relationships?.length || 0) + (componentContract.static[0]?.assertions.length || 0) + componentContract.dynamic.length;
1796
2070
  const apgUrl = componentContract.meta?.source?.apg;
1797
2071
  const failures = [];
1798
2072
  const warnings = [];
@@ -1829,7 +2103,7 @@ async function runContractTestsPlaywright(componentName, url, strictness) {
1829
2103
  }
1830
2104
  await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
1831
2105
  }
1832
- const strategy = ComponentDetector.detect(componentName, actionTimeoutMs, assertionTimeoutMs);
2106
+ const strategy = await ComponentDetector.detect(componentName, componentConfig, actionTimeoutMs, assertionTimeoutMs, configBaseDir);
1833
2107
  if (!strategy) {
1834
2108
  throw new Error(`Unsupported component: ${componentName}`);
1835
2109
  }
@@ -1862,6 +2136,105 @@ This usually means:
1862
2136
  let staticPassed = 0;
1863
2137
  let staticFailed = 0;
1864
2138
  let staticWarnings = 0;
2139
+ for (const rel of componentContract.relationships || []) {
2140
+ const relationshipLevel = normalizeLevel(rel.level);
2141
+ if (rel.type === "aria-reference") {
2142
+ const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
2143
+ const fromSelector = componentContract.selectors[rel.from];
2144
+ const toSelector = componentContract.selectors[rel.to];
2145
+ if (!fromSelector || !toSelector) {
2146
+ const outcome = classifyFailure(
2147
+ `Relationship selector missing: from="${rel.from}" or to="${rel.to}" not found in selectors.`,
2148
+ rel.level
2149
+ );
2150
+ if (outcome.status === "fail") staticFailed += 1;
2151
+ if (outcome.status === "warn") staticWarnings += 1;
2152
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
2153
+ continue;
2154
+ }
2155
+ const fromTarget = page.locator(fromSelector).first();
2156
+ const toTarget = page.locator(toSelector).first();
2157
+ const fromExists = await fromTarget.count() > 0;
2158
+ const toExists = await toTarget.count() > 0;
2159
+ if (!fromExists || !toExists) {
2160
+ const outcome = classifyFailure(
2161
+ `Relationship target not found: ${!fromExists ? rel.from : rel.to}.`,
2162
+ rel.level
2163
+ );
2164
+ if (outcome.status === "fail") staticFailed += 1;
2165
+ if (outcome.status === "warn") staticWarnings += 1;
2166
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
2167
+ continue;
2168
+ }
2169
+ const attrValue = await fromTarget.getAttribute(rel.attribute);
2170
+ const toId = await toTarget.getAttribute("id");
2171
+ if (!toId) {
2172
+ const outcome = classifyFailure(
2173
+ `Relationship target "${rel.to}" must have an id for ${rel.attribute} validation.`,
2174
+ rel.level
2175
+ );
2176
+ if (outcome.status === "fail") staticFailed += 1;
2177
+ if (outcome.status === "warn") staticWarnings += 1;
2178
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
2179
+ continue;
2180
+ }
2181
+ const references = (attrValue || "").split(/\s+/).filter(Boolean);
2182
+ const matches = references.includes(toId);
2183
+ if (!matches) {
2184
+ const outcome = classifyFailure(
2185
+ `Expected ${rel.from} ${rel.attribute} to reference id "${toId}", found "${attrValue || ""}".`,
2186
+ rel.level
2187
+ );
2188
+ if (outcome.status === "fail") staticFailed += 1;
2189
+ if (outcome.status === "warn") staticWarnings += 1;
2190
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
2191
+ continue;
2192
+ }
2193
+ passes.push(`Relationship valid: ${rel.from}.${rel.attribute} -> ${rel.to} (id=${toId}).`);
2194
+ staticPassed += 1;
2195
+ reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
2196
+ continue;
2197
+ }
2198
+ if (rel.type === "contains") {
2199
+ const relDescription = `${rel.parent} contains ${rel.child}`;
2200
+ const parentSelector = componentContract.selectors[rel.parent];
2201
+ const childSelector = componentContract.selectors[rel.child];
2202
+ if (!parentSelector || !childSelector) {
2203
+ const outcome = classifyFailure(
2204
+ `Relationship selector missing: parent="${rel.parent}" or child="${rel.child}" not found in selectors.`,
2205
+ rel.level
2206
+ );
2207
+ if (outcome.status === "fail") staticFailed += 1;
2208
+ if (outcome.status === "warn") staticWarnings += 1;
2209
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
2210
+ continue;
2211
+ }
2212
+ const parent = page.locator(parentSelector).first();
2213
+ const parentExists = await parent.count() > 0;
2214
+ if (!parentExists) {
2215
+ const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
2216
+ if (outcome.status === "fail") staticFailed += 1;
2217
+ if (outcome.status === "warn") staticWarnings += 1;
2218
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
2219
+ continue;
2220
+ }
2221
+ const descendants = parent.locator(childSelector);
2222
+ const descendantCount = await descendants.count();
2223
+ if (descendantCount < 1) {
2224
+ const outcome = classifyFailure(
2225
+ `Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`,
2226
+ rel.level
2227
+ );
2228
+ if (outcome.status === "fail") staticFailed += 1;
2229
+ if (outcome.status === "warn") staticWarnings += 1;
2230
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
2231
+ continue;
2232
+ }
2233
+ passes.push(`Relationship valid: ${rel.parent} contains ${rel.child}.`);
2234
+ staticPassed += 1;
2235
+ reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
2236
+ }
2237
+ }
1865
2238
  const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1866
2239
  for (const test of componentContract.static[0]?.assertions || []) {
1867
2240
  if (test.target === "relative") continue;
@@ -2095,11 +2468,12 @@ Make sure your dev server is running at ${url}`);
2095
2468
  }
2096
2469
  return { passes, failures, skipped, warnings };
2097
2470
  }
2098
- var import_fs2, import_meta3;
2471
+ var import_fs2, import_path5, import_meta3;
2099
2472
  var init_contractTestRunnerPlaywright = __esm({
2100
2473
  "src/utils/test/src/contractTestRunnerPlaywright.ts"() {
2101
2474
  "use strict";
2102
2475
  import_fs2 = require("fs");
2476
+ import_path5 = __toESM(require("path"), 1);
2103
2477
  init_contract();
2104
2478
  init_playwrightTestHarness();
2105
2479
  init_ComponentDetector();
@@ -2150,14 +2524,24 @@ Error: ${error instanceof Error ? error.message : String(error)}`
2150
2524
  return null;
2151
2525
  }
2152
2526
  let strictness = normalizeStrictness(options.strictness);
2153
- if (options.strictness === void 0 && typeof window === "undefined") {
2527
+ let config = {};
2528
+ let configBaseDir = typeof process !== "undefined" ? process.cwd() : "";
2529
+ if (typeof process !== "undefined" && typeof process.cwd === "function") {
2154
2530
  try {
2155
2531
  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);
2532
+ const result2 = await loadConfig2(process.cwd());
2533
+ config = result2.config;
2534
+ if (result2.configPath) {
2535
+ configBaseDir = import_path6.default.dirname(result2.configPath);
2536
+ }
2537
+ if (options.strictness === void 0) {
2538
+ const componentStrictness = config.test?.components?.find((comp) => comp?.name === componentName)?.strictness;
2539
+ strictness = normalizeStrictness(componentStrictness ?? config.test?.strictness);
2540
+ }
2159
2541
  } catch {
2160
- strictness = "balanced";
2542
+ if (options.strictness === void 0) {
2543
+ strictness = "balanced";
2544
+ }
2161
2545
  }
2162
2546
  }
2163
2547
  let contract;
@@ -2167,7 +2551,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
2167
2551
  if (devServerUrl) {
2168
2552
  console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
2169
2553
  const { runContractTestsPlaywright: runContractTestsPlaywright2 } = await Promise.resolve().then(() => (init_contractTestRunnerPlaywright(), contractTestRunnerPlaywright_exports));
2170
- contract = await runContractTestsPlaywright2(componentName, devServerUrl, strictness);
2554
+ contract = await runContractTestsPlaywright2(componentName, devServerUrl, strictness, config, configBaseDir);
2171
2555
  } else {
2172
2556
  throw new Error(
2173
2557
  `\u274C Dev server not running at ${url}
@@ -2223,7 +2607,7 @@ ${violationDetails}
2223
2607
  async function cleanupTests() {
2224
2608
  await closeSharedBrowser();
2225
2609
  }
2226
- var import_jest_axe, runTest;
2610
+ var import_jest_axe, import_path6, runTest;
2227
2611
  var init_test2 = __esm({
2228
2612
  "src/utils/test/src/test.ts"() {
2229
2613
  "use strict";
@@ -2231,6 +2615,7 @@ var init_test2 = __esm({
2231
2615
  init_contractTestRunner();
2232
2616
  init_playwrightTestHarness();
2233
2617
  init_strictness();
2618
+ import_path6 = __toESM(require("path"), 1);
2234
2619
  runTest = async () => {
2235
2620
  return {
2236
2621
  passes: [],
@@ -2243,7 +2628,7 @@ var init_test2 = __esm({
2243
2628
  console.log(`\u{1F680} Running component accessibility tests...
2244
2629
  `);
2245
2630
  const { exec } = await import("child_process");
2246
- const chalk3 = (await import("chalk")).default;
2631
+ const chalk4 = (await import("chalk")).default;
2247
2632
  return new Promise((resolve, reject) => {
2248
2633
  exec(
2249
2634
  `npx vitest --run --reporter verbose`,
@@ -2256,11 +2641,11 @@ var init_test2 = __esm({
2256
2641
  const { displayBadgeInfo: displayBadgeInfo2, promptAddBadge: promptAddBadge2 } = await Promise.resolve().then(() => (init_badgeHelper(), badgeHelper_exports));
2257
2642
  displayBadgeInfo2("component");
2258
2643
  await promptAddBadge2("component", process.cwd());
2259
- console.log(chalk3.dim("\n" + "\u2500".repeat(60)));
2260
- console.log(chalk3.cyan("\u{1F499} Found aria-ease helpful?"));
2261
- console.log(chalk3.white(" \u2022 Star us on GitHub: ") + chalk3.blue.underline("https://github.com/aria-ease/aria-ease"));
2262
- console.log(chalk3.white(" \u2022 Share feedback: ") + chalk3.blue.underline("https://github.com/aria-ease/aria-ease/discussions"));
2263
- console.log(chalk3.dim("\u2500".repeat(60) + "\n"));
2644
+ console.log(chalk4.dim("\n" + "\u2500".repeat(60)));
2645
+ console.log(chalk4.cyan("\u{1F499} Found aria-ease helpful?"));
2646
+ console.log(chalk4.white(" \u2022 Star us on GitHub: ") + chalk4.blue.underline("https://github.com/aria-ease/aria-ease"));
2647
+ console.log(chalk4.white(" \u2022 Share feedback: ") + chalk4.blue.underline("https://github.com/aria-ease/aria-ease/discussions"));
2648
+ console.log(chalk4.dim("\u2500".repeat(60) + "\n"));
2264
2649
  } catch (badgeError) {
2265
2650
  console.error("Warning: Could not display badge prompt:", badgeError);
2266
2651
  }
@@ -2293,33 +2678,480 @@ var init_test3 = __esm({
2293
2678
  }
2294
2679
  });
2295
2680
 
2681
+ // src/utils/cli/contractValidator.ts
2682
+ function validateContractSchema(contract) {
2683
+ const errors = [];
2684
+ if (!contract || typeof contract !== "object") {
2685
+ return {
2686
+ valid: false,
2687
+ errors: [{ path: "$", message: "Contract must be an object" }]
2688
+ };
2689
+ }
2690
+ const c = contract;
2691
+ if (!c.selectors) {
2692
+ errors.push({ path: "$.selectors", message: "selectors is required" });
2693
+ } else if (typeof c.selectors !== "object" || c.selectors === null || Array.isArray(c.selectors)) {
2694
+ errors.push({ path: "$.selectors", message: "selectors must be an object" });
2695
+ } else {
2696
+ const selectors = c.selectors;
2697
+ Object.entries(selectors).forEach(([key, value]) => {
2698
+ if (typeof value !== "string") {
2699
+ errors.push({ path: `$.selectors['${key}']`, message: "All selectors must be strings" });
2700
+ }
2701
+ });
2702
+ }
2703
+ if (!Array.isArray(c.static)) {
2704
+ errors.push({ path: "$.static", message: "static must be an array" });
2705
+ } else {
2706
+ c.static.forEach((item, idx) => {
2707
+ if (typeof item !== "object" || item === null) {
2708
+ errors.push({ path: `$.static[${idx}]`, message: "static item must be an object" });
2709
+ return;
2710
+ }
2711
+ const staticItem = item;
2712
+ if (!Array.isArray(staticItem.assertions)) {
2713
+ errors.push({
2714
+ path: `$.static[${idx}].assertions`,
2715
+ message: "assertions must be an array"
2716
+ });
2717
+ return;
2718
+ }
2719
+ staticItem.assertions.forEach((assertion, assertIdx) => {
2720
+ if (typeof assertion !== "object" || assertion === null) {
2721
+ errors.push({
2722
+ path: `$.static[${idx}].assertions[${assertIdx}]`,
2723
+ message: "assertion must be an object"
2724
+ });
2725
+ return;
2726
+ }
2727
+ const a = assertion;
2728
+ if (typeof a.target !== "string") {
2729
+ errors.push({
2730
+ path: `$.static[${idx}].assertions[${assertIdx}].target`,
2731
+ message: "target is required and must be a string"
2732
+ });
2733
+ }
2734
+ if (typeof a.attribute !== "string") {
2735
+ errors.push({
2736
+ path: `$.static[${idx}].assertions[${assertIdx}].attribute`,
2737
+ message: "attribute is required and must be a string"
2738
+ });
2739
+ }
2740
+ if (typeof a.failureMessage !== "string") {
2741
+ errors.push({
2742
+ path: `$.static[${idx}].assertions[${assertIdx}].failureMessage`,
2743
+ message: "failureMessage is required and must be a string"
2744
+ });
2745
+ }
2746
+ if (a.level !== void 0 && !["required", "recommended", "optional"].includes(a.level)) {
2747
+ errors.push({
2748
+ path: `$.static[${idx}].assertions[${assertIdx}].level`,
2749
+ message: "level must be one of: required, recommended, optional"
2750
+ });
2751
+ }
2752
+ });
2753
+ });
2754
+ }
2755
+ if (!Array.isArray(c.dynamic)) {
2756
+ errors.push({ path: "$.dynamic", message: "dynamic must be an array" });
2757
+ } else {
2758
+ c.dynamic.forEach((item, idx) => {
2759
+ if (typeof item !== "object" || item === null) {
2760
+ errors.push({ path: `$.dynamic[${idx}]`, message: "dynamic item must be an object" });
2761
+ return;
2762
+ }
2763
+ const dynamicItem = item;
2764
+ if (typeof dynamicItem.description !== "string") {
2765
+ errors.push({
2766
+ path: `$.dynamic[${idx}].description`,
2767
+ message: "description is required and must be a string"
2768
+ });
2769
+ }
2770
+ if (!Array.isArray(dynamicItem.action)) {
2771
+ errors.push({
2772
+ path: `$.dynamic[${idx}].action`,
2773
+ message: "action is required and must be an array"
2774
+ });
2775
+ } else {
2776
+ dynamicItem.action.forEach((action, actIdx) => {
2777
+ if (typeof action !== "object" || action === null) {
2778
+ errors.push({
2779
+ path: `$.dynamic[${idx}].action[${actIdx}]`,
2780
+ message: "action item must be an object"
2781
+ });
2782
+ return;
2783
+ }
2784
+ const a = action;
2785
+ if (typeof a.type !== "string") {
2786
+ errors.push({
2787
+ path: `$.dynamic[${idx}].action[${actIdx}].type`,
2788
+ message: "type is required and must be a string"
2789
+ });
2790
+ }
2791
+ if (typeof a.target !== "string") {
2792
+ errors.push({
2793
+ path: `$.dynamic[${idx}].action[${actIdx}].target`,
2794
+ message: "target is required and must be a string"
2795
+ });
2796
+ }
2797
+ });
2798
+ }
2799
+ if (!Array.isArray(dynamicItem.assertions)) {
2800
+ errors.push({
2801
+ path: `$.dynamic[${idx}].assertions`,
2802
+ message: "assertions is required and must be an array"
2803
+ });
2804
+ } else {
2805
+ dynamicItem.assertions.forEach((assertion, assertIdx) => {
2806
+ if (typeof assertion !== "object" || assertion === null) {
2807
+ errors.push({
2808
+ path: `$.dynamic[${idx}].assertions[${assertIdx}]`,
2809
+ message: "assertion must be an object"
2810
+ });
2811
+ return;
2812
+ }
2813
+ const a = assertion;
2814
+ if (typeof a.target !== "string") {
2815
+ errors.push({
2816
+ path: `$.dynamic[${idx}].assertions[${assertIdx}].target`,
2817
+ message: "target is required and must be a string"
2818
+ });
2819
+ }
2820
+ if (typeof a.assertion !== "string") {
2821
+ errors.push({
2822
+ path: `$.dynamic[${idx}].assertions[${assertIdx}].assertion`,
2823
+ message: "assertion is required and must be a string"
2824
+ });
2825
+ }
2826
+ if (a.level !== void 0 && !["required", "recommended", "optional"].includes(a.level)) {
2827
+ errors.push({
2828
+ path: `$.dynamic[${idx}].assertions[${assertIdx}].level`,
2829
+ message: "level must be one of: required, recommended, optional"
2830
+ });
2831
+ }
2832
+ });
2833
+ }
2834
+ });
2835
+ }
2836
+ if (c.relationships !== void 0) {
2837
+ if (!Array.isArray(c.relationships)) {
2838
+ errors.push({ path: "$.relationships", message: "relationships must be an array" });
2839
+ } else {
2840
+ c.relationships.forEach((rel, idx) => {
2841
+ if (typeof rel !== "object" || rel === null) {
2842
+ errors.push({ path: `$.relationships[${idx}]`, message: "relationship must be an object" });
2843
+ return;
2844
+ }
2845
+ const r = rel;
2846
+ const type = r.type;
2847
+ if (!["aria-reference", "contains"].includes(type)) {
2848
+ errors.push({
2849
+ path: `$.relationships[${idx}].type`,
2850
+ message: "type must be one of: aria-reference, contains"
2851
+ });
2852
+ }
2853
+ if (type === "aria-reference") {
2854
+ if (typeof r.from !== "string") {
2855
+ errors.push({
2856
+ path: `$.relationships[${idx}].from`,
2857
+ message: "from is required for aria-reference and must be a string"
2858
+ });
2859
+ }
2860
+ if (typeof r.attribute !== "string") {
2861
+ errors.push({
2862
+ path: `$.relationships[${idx}].attribute`,
2863
+ message: "attribute is required for aria-reference and must be a string"
2864
+ });
2865
+ }
2866
+ if (typeof r.to !== "string") {
2867
+ errors.push({
2868
+ path: `$.relationships[${idx}].to`,
2869
+ message: "to is required for aria-reference and must be a string"
2870
+ });
2871
+ }
2872
+ } else if (type === "contains") {
2873
+ if (typeof r.parent !== "string") {
2874
+ errors.push({
2875
+ path: `$.relationships[${idx}].parent`,
2876
+ message: "parent is required for contains and must be a string"
2877
+ });
2878
+ }
2879
+ if (typeof r.child !== "string") {
2880
+ errors.push({
2881
+ path: `$.relationships[${idx}].child`,
2882
+ message: "child is required for contains and must be a string"
2883
+ });
2884
+ }
2885
+ }
2886
+ });
2887
+ }
2888
+ }
2889
+ return {
2890
+ valid: errors.length === 0,
2891
+ errors
2892
+ };
2893
+ }
2894
+ function validateRelationshipReferences(contract, selectorKeys) {
2895
+ const errors = [];
2896
+ if (!contract || typeof contract !== "object") {
2897
+ return errors;
2898
+ }
2899
+ const c = contract;
2900
+ const relationships = c.relationships;
2901
+ if (!Array.isArray(relationships)) {
2902
+ return errors;
2903
+ }
2904
+ relationships.forEach((rel, idx) => {
2905
+ const type = rel.type;
2906
+ if (type === "aria-reference") {
2907
+ const from = rel.from;
2908
+ const to = rel.to;
2909
+ if (from && !selectorKeys.has(from)) {
2910
+ errors.push({
2911
+ path: `$.relationships[${idx}].from`,
2912
+ message: `Selector '${from}' not found in selectors`
2913
+ });
2914
+ }
2915
+ if (to && !selectorKeys.has(to)) {
2916
+ errors.push({
2917
+ path: `$.relationships[${idx}].to`,
2918
+ message: `Selector '${to}' not found in selectors`
2919
+ });
2920
+ }
2921
+ } else if (type === "contains") {
2922
+ const parent = rel.parent;
2923
+ const child = rel.child;
2924
+ if (parent && !selectorKeys.has(parent)) {
2925
+ errors.push({
2926
+ path: `$.relationships[${idx}].parent`,
2927
+ message: `Selector '${parent}' not found in selectors`
2928
+ });
2929
+ }
2930
+ if (child && !selectorKeys.has(child)) {
2931
+ errors.push({
2932
+ path: `$.relationships[${idx}].child`,
2933
+ message: `Selector '${child}' not found in selectors`
2934
+ });
2935
+ }
2936
+ }
2937
+ });
2938
+ return errors;
2939
+ }
2940
+ function validateTargetReferences(contract, selectorKeys) {
2941
+ const errors = [];
2942
+ if (!contract || typeof contract !== "object") {
2943
+ return errors;
2944
+ }
2945
+ const c = contract;
2946
+ const staticItems = c.static;
2947
+ if (Array.isArray(staticItems)) {
2948
+ staticItems.forEach((item, itemIdx) => {
2949
+ const assertions = item.assertions;
2950
+ if (Array.isArray(assertions)) {
2951
+ assertions.forEach((assertion, assertIdx) => {
2952
+ const target = assertion.target;
2953
+ if (target && !selectorKeys.has(target)) {
2954
+ errors.push({
2955
+ path: `$.static[${itemIdx}].assertions[${assertIdx}].target`,
2956
+ message: `Selector '${target}' not found in selectors`
2957
+ });
2958
+ }
2959
+ });
2960
+ }
2961
+ });
2962
+ }
2963
+ const dynamicItems = c.dynamic;
2964
+ if (Array.isArray(dynamicItems)) {
2965
+ dynamicItems.forEach((item, itemIdx) => {
2966
+ const actions = item.action;
2967
+ if (Array.isArray(actions)) {
2968
+ actions.forEach((action, actIdx) => {
2969
+ const target = action.target;
2970
+ if (target && target !== "document" && !selectorKeys.has(target)) {
2971
+ errors.push({
2972
+ path: `$.dynamic[${itemIdx}].action[${actIdx}].target`,
2973
+ message: `Selector '${target}' not found in selectors (or use 'document')`
2974
+ });
2975
+ }
2976
+ });
2977
+ }
2978
+ const assertions = item.assertions;
2979
+ if (Array.isArray(assertions)) {
2980
+ assertions.forEach((assertion, assertIdx) => {
2981
+ const target = assertion.target;
2982
+ if (target && target !== "relative" && !selectorKeys.has(target)) {
2983
+ errors.push({
2984
+ path: `$.dynamic[${itemIdx}].assertions[${assertIdx}].target`,
2985
+ message: `Selector '${target}' not found in selectors (or use 'relative')`
2986
+ });
2987
+ }
2988
+ });
2989
+ }
2990
+ });
2991
+ }
2992
+ return errors;
2993
+ }
2994
+ var init_contractValidator = __esm({
2995
+ "src/utils/cli/contractValidator.ts"() {
2996
+ "use strict";
2997
+ }
2998
+ });
2999
+
3000
+ // src/utils/cli/buildContracts.ts
3001
+ var buildContracts_exports = {};
3002
+ __export(buildContracts_exports, {
3003
+ buildContracts: () => buildContracts
3004
+ });
3005
+ async function buildContracts(cwd, config) {
3006
+ const errors = [];
3007
+ const built = [];
3008
+ if (!config.contracts || config.contracts.length === 0) {
3009
+ console.log(import_chalk2.default.yellow('\u2139\uFE0F No contracts configured. Add "contracts" array to ariaease.config.js'));
3010
+ return { success: true, built, errors };
3011
+ }
3012
+ try {
3013
+ console.log(import_chalk2.default.cyanBright("\n\u{1F3D7}\uFE0F Building contracts...\n"));
3014
+ for (const contractSource of config.contracts) {
3015
+ const srcPattern = import_path7.default.resolve(cwd, contractSource.src);
3016
+ const outDir = contractSource.out ? import_path7.default.resolve(cwd, contractSource.out) : void 0;
3017
+ console.log(import_chalk2.default.gray(` Pattern: ${contractSource.src}`));
3018
+ if (outDir) {
3019
+ console.log(import_chalk2.default.gray(` Output: ${contractSource.out}
3020
+ `));
3021
+ } else {
3022
+ console.log(import_chalk2.default.gray(` Output: Same directory as source
3023
+ `));
3024
+ }
3025
+ const contractFiles = [];
3026
+ for await (const file of (0, import_promises2.glob)(srcPattern, { cwd })) {
3027
+ contractFiles.push(file);
3028
+ }
3029
+ if (contractFiles.length === 0) {
3030
+ console.log(import_chalk2.default.yellow(`\u26A0\uFE0F No contract files found matching pattern: ${contractSource.src}`));
3031
+ continue;
3032
+ }
3033
+ if (outDir) {
3034
+ await import_fs_extra3.default.ensureDir(outDir);
3035
+ }
3036
+ for (const contractFile of contractFiles) {
3037
+ try {
3038
+ const absolutePath = contractFile.startsWith("/") ? contractFile : import_path7.default.resolve(cwd, contractFile);
3039
+ const module2 = await import(`file://${absolutePath}`);
3040
+ let contract = module2.default;
3041
+ if (!contract) {
3042
+ const namedExports = Object.entries(module2).find(
3043
+ ([, value]) => value && typeof value === "object" && "toJSON" in value
3044
+ );
3045
+ if (namedExports) {
3046
+ contract = namedExports[1];
3047
+ }
3048
+ }
3049
+ if (!contract || typeof contract.toJSON !== "function") {
3050
+ errors.push(`${import_path7.default.basename(contractFile)}: No contract with toJSON() method found`);
3051
+ continue;
3052
+ }
3053
+ const json = contract.toJSON();
3054
+ const schemaValidation = validateContractSchema(json);
3055
+ if (!schemaValidation.valid) {
3056
+ const errorLines = schemaValidation.errors.map((err) => ` ${import_chalk2.default.red("\u2717")} ${err.path}: ${err.message}`).join("\n");
3057
+ const errorMsg = `Schema validation failed:
3058
+ ${errorLines}`;
3059
+ errors.push(`${import_path7.default.basename(contractFile)}: ${errorMsg}`);
3060
+ console.log(import_chalk2.default.red(`\u274C ${import_path7.default.basename(contractFile)}`));
3061
+ console.log(import_chalk2.default.red(` ${errorMsg}`));
3062
+ continue;
3063
+ }
3064
+ const selectorKeys = /* @__PURE__ */ new Set();
3065
+ if (json && typeof json === "object" && "selectors" in json) {
3066
+ const selectors = json.selectors;
3067
+ Object.keys(selectors).forEach((key) => selectorKeys.add(key));
3068
+ }
3069
+ const refErrors = [
3070
+ ...validateRelationshipReferences(json, selectorKeys),
3071
+ ...validateTargetReferences(json, selectorKeys)
3072
+ ];
3073
+ if (refErrors.length > 0) {
3074
+ const errorLines = refErrors.map((err) => ` ${import_chalk2.default.red("\u2717")} ${err.path}: ${err.message}`).join("\n");
3075
+ const errorMsg = `Reference validation failed:
3076
+ ${errorLines}`;
3077
+ errors.push(`${import_path7.default.basename(contractFile)}: ${errorMsg}`);
3078
+ console.log(import_chalk2.default.red(`\u274C ${import_path7.default.basename(contractFile)}`));
3079
+ console.log(import_chalk2.default.red(` ${errorMsg}`));
3080
+ continue;
3081
+ }
3082
+ const contractName = import_path7.default.basename(contractFile, import_path7.default.extname(contractFile));
3083
+ const outputFile = outDir ? import_path7.default.join(outDir, `${contractName}.json`) : import_path7.default.join(import_path7.default.dirname(absolutePath), `${contractName}.json`);
3084
+ await import_fs_extra3.default.writeFile(outputFile, JSON.stringify(json, null, 2) + "\n", "utf-8");
3085
+ const relativePath = import_path7.default.relative(cwd, outputFile);
3086
+ built.push(relativePath);
3087
+ console.log(import_chalk2.default.green(`\u2705 ${import_path7.default.basename(contractFile)} \u2192 ${import_path7.default.basename(outputFile)}`));
3088
+ } catch (error) {
3089
+ const errorMsg = error instanceof Error ? error.message : String(error);
3090
+ const filename = import_path7.default.basename(contractFile);
3091
+ errors.push(`${filename}: ${errorMsg}`);
3092
+ console.log(import_chalk2.default.red(`\u274C ${filename}`));
3093
+ console.log(import_chalk2.default.red(` Error: ${errorMsg}`));
3094
+ }
3095
+ }
3096
+ }
3097
+ console.log("");
3098
+ if (errors.length === 0) {
3099
+ console.log(import_chalk2.default.green(`\u2705 Built ${built.length} contract${built.length !== 1 ? "s" : ""} successfully
3100
+ `));
3101
+ return { success: true, built, errors };
3102
+ } else {
3103
+ console.log(import_chalk2.default.yellow(`\u26A0\uFE0F Built ${built.length} contracts with ${errors.length} error${errors.length !== 1 ? "s" : ""}
3104
+ `));
3105
+ return { success: false, built, errors };
3106
+ }
3107
+ } catch (error) {
3108
+ const errorMsg = error instanceof Error ? error.message : String(error);
3109
+ const msg = `Failed to build contracts: ${errorMsg}`;
3110
+ errors.push(msg);
3111
+ console.log(import_chalk2.default.red(`\u274C ${msg}
3112
+ `));
3113
+ return { success: false, built, errors };
3114
+ }
3115
+ }
3116
+ var import_path7, import_fs_extra3, import_promises2, import_chalk2;
3117
+ var init_buildContracts = __esm({
3118
+ "src/utils/cli/buildContracts.ts"() {
3119
+ "use strict";
3120
+ import_path7 = __toESM(require("path"), 1);
3121
+ import_fs_extra3 = __toESM(require("fs-extra"), 1);
3122
+ import_promises2 = require("fs/promises");
3123
+ import_chalk2 = __toESM(require("chalk"), 1);
3124
+ init_contractValidator();
3125
+ }
3126
+ });
3127
+
2296
3128
  // src/utils/cli/cli.ts
2297
3129
  var import_commander = require("commander");
2298
- var import_chalk2 = __toESM(require("chalk"), 1);
2299
- var import_path3 = __toESM(require("path"), 1);
2300
- var import_fs_extra3 = __toESM(require("fs-extra"), 1);
3130
+ var import_chalk3 = __toESM(require("chalk"), 1);
3131
+ var import_path8 = __toESM(require("path"), 1);
3132
+ var import_fs_extra4 = __toESM(require("fs-extra"), 1);
2301
3133
  init_configLoader();
2302
3134
  init_badgeHelper();
2303
3135
  var program = new import_commander.Command();
2304
3136
  program.name("aria-ease").description("Run accessibility tests and audits").version("2.2.3");
2305
3137
  program.command("audit").description("Run axe-core powered accessibility audit on webpages").option("-u, --url <url>", "Single URL to audit").option("-f, --format <format>", "Output format for the audit report: json | csv | html | all", "all").option("-o, --out <path>", "Directory to save the audit report", "./accessibility-reports/audit").action(async (opts) => {
2306
- console.log(import_chalk2.default.cyanBright("\u{1F680} Starting accessibility audit...\n"));
3138
+ console.log(import_chalk3.default.cyanBright("\u{1F680} Starting accessibility audit...\n"));
2307
3139
  const { runAudit: runAudit2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
2308
3140
  const { formatResults: formatResults2 } = await Promise.resolve().then(() => (init_formatters(), formatters_exports));
2309
3141
  const needsConfig = !opts.url;
2310
3142
  const { config, configPath, errors } = await loadConfig(process.cwd());
2311
3143
  if (configPath) {
2312
- console.log(import_chalk2.default.green(`\u2705 Loaded config from ${import_path3.default.basename(configPath)}
3144
+ console.log(import_chalk3.default.green(`\u2705 Loaded config from ${import_path8.default.basename(configPath)}
2313
3145
  `));
2314
3146
  } else if (errors.length > 0 && needsConfig) {
2315
- console.log(import_chalk2.default.red("\u274C Config file errors:\n"));
2316
- errors.forEach((err) => console.log(import_chalk2.default.red(` ${err}`)));
3147
+ console.log(import_chalk3.default.red("\u274C Config file errors:\n"));
3148
+ errors.forEach((err) => console.log(import_chalk3.default.red(` ${err}`)));
2317
3149
  console.log("");
2318
3150
  process.exit(1);
2319
3151
  } else if (errors.length > 0) {
2320
- console.log(import_chalk2.default.yellow("\u26A0\uFE0F Config file has errors (ignored, using CLI options)\n"));
3152
+ console.log(import_chalk3.default.yellow("\u26A0\uFE0F Config file has errors (ignored, using CLI options)\n"));
2321
3153
  } else if (!configPath && needsConfig) {
2322
- console.log(import_chalk2.default.yellow("\u2139\uFE0F No config file found, using CLI options.\n"));
3154
+ console.log(import_chalk3.default.yellow("\u2139\uFE0F No config file found, using CLI options.\n"));
2323
3155
  }
2324
3156
  const urls = [];
2325
3157
  if (opts.url) {
@@ -2329,11 +3161,11 @@ program.command("audit").description("Run axe-core powered accessibility audit o
2329
3161
  }
2330
3162
  const format = config.audit?.output && config.audit.output.format || opts.format;
2331
3163
  if (!["json", "csv", "html", "all"].includes(format)) {
2332
- console.log(import_chalk2.default.red('\u274C Invalid format. Use "json", "csv", "html" or "all".'));
3164
+ console.log(import_chalk3.default.red('\u274C Invalid format. Use "json", "csv", "html" or "all".'));
2333
3165
  process.exit(1);
2334
3166
  }
2335
3167
  if (urls.length === 0) {
2336
- console.log(import_chalk2.default.red('\u274C No URLs provided. Use --url option or add "urls" in config file'));
3168
+ console.log(import_chalk3.default.red('\u274C No URLs provided. Use --url option or add "urls" in config file'));
2337
3169
  process.exit(1);
2338
3170
  }
2339
3171
  const allResults = [];
@@ -2342,15 +3174,15 @@ program.command("audit").description("Run axe-core powered accessibility audit o
2342
3174
  try {
2343
3175
  const auditOptions = { browser };
2344
3176
  for (const url of urls) {
2345
- console.log(import_chalk2.default.yellow(`\u{1F50E} Auditing: ${url}`));
3177
+ console.log(import_chalk3.default.yellow(`\u{1F50E} Auditing: ${url}`));
2346
3178
  try {
2347
3179
  const result = await runAudit2(url, auditOptions);
2348
3180
  allResults.push({ url, result });
2349
- console.log(import_chalk2.default.green(`\u2705 Completed audit for ${url}
3181
+ console.log(import_chalk3.default.green(`\u2705 Completed audit for ${url}
2350
3182
  `));
2351
3183
  } catch (error) {
2352
3184
  if (error instanceof Error && error.message) {
2353
- console.log(import_chalk2.default.red(`\u274C Failed auditing ${url}: ${error.message}`));
3185
+ console.log(import_chalk3.default.red(`\u274C Failed auditing ${url}: ${error.message}`));
2354
3186
  }
2355
3187
  }
2356
3188
  }
@@ -2361,32 +3193,32 @@ program.command("audit").description("Run axe-core powered accessibility audit o
2361
3193
  if (!hasResults) {
2362
3194
  const auditedCount = allResults.filter((r) => r.result).length;
2363
3195
  if (auditedCount === 0) {
2364
- console.log(import_chalk2.default.red("\u274C No pages were successfully audited."));
3196
+ console.log(import_chalk3.default.red("\u274C No pages were successfully audited."));
2365
3197
  process.exit(1);
2366
3198
  }
2367
- console.log(import_chalk2.default.green("\n\u{1F389} Great news! No static accessibility violations found!"));
2368
- console.log(import_chalk2.default.gray(` Audited ${auditedCount} page${auditedCount > 1 ? "s" : ""} successfully.
3199
+ console.log(import_chalk3.default.green("\n\u{1F389} Great news! No static accessibility violations found!"));
3200
+ console.log(import_chalk3.default.gray(` Audited ${auditedCount} page${auditedCount > 1 ? "s" : ""} successfully.
2369
3201
  `));
2370
3202
  displayBadgeInfo("audit");
2371
3203
  await promptAddBadge("audit", process.cwd());
2372
- console.log(import_chalk2.default.dim("\n" + "\u2500".repeat(60)));
2373
- console.log(import_chalk2.default.cyan("\u{1F499} Found aria-ease helpful?"));
2374
- console.log(import_chalk2.default.white(" \u2022 Star us on GitHub: ") + import_chalk2.default.blue.underline("https://github.com/aria-ease/aria-ease"));
2375
- console.log(import_chalk2.default.white(" \u2022 Share feedback: ") + import_chalk2.default.blue.underline("https://github.com/aria-ease/aria-ease/discussions"));
2376
- console.log(import_chalk2.default.dim("\u2500".repeat(60) + "\n"));
3204
+ console.log(import_chalk3.default.dim("\n" + "\u2500".repeat(60)));
3205
+ console.log(import_chalk3.default.cyan("\u{1F499} Found aria-ease helpful?"));
3206
+ console.log(import_chalk3.default.white(" \u2022 Star us on GitHub: ") + import_chalk3.default.blue.underline("https://github.com/aria-ease/aria-ease"));
3207
+ console.log(import_chalk3.default.white(" \u2022 Share feedback: ") + import_chalk3.default.blue.underline("https://github.com/aria-ease/aria-ease/discussions"));
3208
+ console.log(import_chalk3.default.dim("\u2500".repeat(60) + "\n"));
2377
3209
  process.exit(0);
2378
3210
  }
2379
3211
  async function createReport(format2) {
2380
3212
  const formatted = formatResults2(allResults, format2);
2381
3213
  const out = config.audit?.output && config.audit.output.out || opts.out;
2382
- await import_fs_extra3.default.ensureDir(out);
3214
+ await import_fs_extra4.default.ensureDir(out);
2383
3215
  const d = /* @__PURE__ */ new Date();
2384
3216
  const pad = (n) => String(n).padStart(2, "0");
2385
3217
  const timestamp = `${pad(d.getDate())}-${pad(d.getMonth() + 1)}-${d.getFullYear()} ${pad(d.getHours())}h ${pad(d.getMinutes())}m ${pad(d.getSeconds())}s`;
2386
3218
  const fileName = `ariaease-report-${timestamp}.${format2}`;
2387
- const filePath = import_path3.default.join(out, fileName);
2388
- await import_fs_extra3.default.writeFile(filePath, formatted, "utf-8");
2389
- console.log(import_chalk2.default.magentaBright(`\u{1F4C1} Report saved to ${filePath}`));
3219
+ const filePath = import_path8.default.join(out, fileName);
3220
+ await import_fs_extra4.default.writeFile(filePath, formatted, "utf-8");
3221
+ console.log(import_chalk3.default.magentaBright(`\u{1F4C1} Report saved to ${filePath}`));
2390
3222
  }
2391
3223
  if (["json", "csv", "html"].includes(format)) {
2392
3224
  await createReport(format);
@@ -2396,22 +3228,53 @@ program.command("audit").description("Run axe-core powered accessibility audit o
2396
3228
  const totalViolations = allResults.reduce((sum, r) => {
2397
3229
  return sum + (r.result?.violations?.length || 0);
2398
3230
  }, 0);
2399
- console.log(import_chalk2.default.red(`
3231
+ console.log(import_chalk3.default.red(`
2400
3232
  \u274C Accessibility violations found!`));
2401
- console.log(import_chalk2.default.yellow(` ${totalViolations} violation${totalViolations !== 1 ? "s" : ""} detected across ${allResults.length} page${allResults.length !== 1 ? "s" : ""}.`));
2402
- console.log(import_chalk2.default.gray(` Review the generated report for details.
3233
+ console.log(import_chalk3.default.yellow(` ${totalViolations} violation${totalViolations !== 1 ? "s" : ""} detected across ${allResults.length} page${allResults.length !== 1 ? "s" : ""}.`));
3234
+ console.log(import_chalk3.default.gray(` Review the generated report for details.
2403
3235
  `));
2404
- console.log(import_chalk2.default.dim("\n" + "\u2500".repeat(60)));
2405
- console.log(import_chalk2.default.cyan("\u{1F499} Found aria-ease helpful?"));
2406
- console.log(import_chalk2.default.white(" \u2022 Star us on GitHub: ") + import_chalk2.default.blue.underline("https://github.com/aria-ease/aria-ease"));
2407
- console.log(import_chalk2.default.white(" \u2022 Share feedback: ") + import_chalk2.default.blue.underline("https://github.com/aria-ease/aria-ease/discussions"));
2408
- console.log(import_chalk2.default.dim("\u2500".repeat(60) + "\n"));
3236
+ console.log(import_chalk3.default.dim("\n" + "\u2500".repeat(60)));
3237
+ console.log(import_chalk3.default.cyan("\u{1F499} Found aria-ease helpful?"));
3238
+ console.log(import_chalk3.default.white(" \u2022 Star us on GitHub: ") + import_chalk3.default.blue.underline("https://github.com/aria-ease/aria-ease"));
3239
+ console.log(import_chalk3.default.white(" \u2022 Share feedback: ") + import_chalk3.default.blue.underline("https://github.com/aria-ease/aria-ease/discussions"));
3240
+ console.log(import_chalk3.default.dim("\u2500".repeat(60) + "\n"));
2409
3241
  process.exit(1);
2410
3242
  });
2411
3243
  program.command("test").description("Run core a11y accessibility standard tests on UI components").action(async () => {
2412
3244
  const { runTest: runTest2 } = await Promise.resolve().then(() => (init_test3(), test_exports2));
2413
3245
  runTest2();
2414
3246
  });
3247
+ program.command("build").description("Build accessibility artifacts").addCommand(
3248
+ new import_commander.Command("contracts").description("Build DSL contracts to JSON").action(async () => {
3249
+ const { buildContracts: buildContracts2 } = await Promise.resolve().then(() => (init_buildContracts(), buildContracts_exports));
3250
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_configLoader(), configLoader_exports));
3251
+ const cwd = process.cwd();
3252
+ const { config, configPath, errors } = await loadConfig2(cwd);
3253
+ if (configPath) {
3254
+ console.log(import_chalk3.default.green(`\u2705 Loaded config from ${import_path8.default.basename(configPath)}
3255
+ `));
3256
+ } else if (errors.length > 0) {
3257
+ console.log(import_chalk3.default.red("\u274C Config file has errors:\n"));
3258
+ errors.forEach((err) => console.log(import_chalk3.default.red(` ${err}`)));
3259
+ console.log("");
3260
+ process.exit(1);
3261
+ }
3262
+ const result = await buildContracts2(cwd, config);
3263
+ if (!result.success && result.errors.length > 0) {
3264
+ console.log(import_chalk3.default.red(`\u274C ${result.errors.length} error${result.errors.length !== 1 ? "s" : ""} occurred during build
3265
+ `));
3266
+ process.exit(1);
3267
+ }
3268
+ if (result.built.length === 0 && (!config.contracts || config.contracts.length === 0)) {
3269
+ process.exit(0);
3270
+ }
3271
+ if (result.built.length === 0) {
3272
+ console.log(import_chalk3.default.yellow("\u26A0\uFE0F No contracts were built\n"));
3273
+ process.exit(1);
3274
+ }
3275
+ process.exit(0);
3276
+ })
3277
+ );
2415
3278
  program.command("help").description("Display help information").action(() => {
2416
3279
  program.outputHelp();
2417
3280
  });