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.
- package/README.md +68 -6
- package/bin/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
- package/bin/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
- package/bin/MenuComponentStrategy-JAMTCSNF.js +81 -0
- package/bin/TabsComponentStrategy-3SQURPMX.js +29 -0
- package/bin/buildContracts-GBOY7UXG.js +437 -0
- package/bin/{chunk-VPBHLMAS.js → chunk-LMSKLN5O.js} +21 -0
- package/bin/chunk-PK5L2SAF.js +17 -0
- package/bin/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
- package/bin/cli.cjs +991 -128
- package/bin/cli.js +33 -2
- package/bin/{configLoader-XRF6VM4J.js → configLoader-Q6A4JLKW.js} +1 -1
- package/{dist/contractTestRunnerPlaywright-UAOFNS7Z.js → bin/contractTestRunnerPlaywright-ZZNWDUYP.js} +270 -219
- package/bin/{test-WRIJHN6H.js → test-OND56UUL.js} +97 -10
- package/dist/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
- package/dist/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
- package/dist/MenuComponentStrategy-JAMTCSNF.js +81 -0
- package/dist/TabsComponentStrategy-3SQURPMX.js +29 -0
- package/dist/chunk-PK5L2SAF.js +17 -0
- package/dist/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
- package/dist/{configLoader-IT4PWCJB.js → configLoader-WTGJAP4Z.js} +21 -0
- package/{bin/contractTestRunnerPlaywright-UAOFNS7Z.js → dist/contractTestRunnerPlaywright-XBWJZMR3.js} +270 -219
- package/dist/index.cjs +794 -90
- package/dist/index.d.cts +136 -1
- package/dist/index.d.ts +136 -1
- package/dist/index.js +415 -10
- package/dist/src/utils/test/AccordionComponentStrategy-WRHZOEN6.js +38 -0
- package/dist/src/utils/test/ComboboxComponentStrategy-5AECQSRN.js +60 -0
- package/dist/src/utils/test/MenuComponentStrategy-VKZQYLBE.js +77 -0
- package/dist/src/utils/test/TabsComponentStrategy-BKG53SEV.js +26 -0
- package/dist/src/utils/test/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
- package/dist/src/utils/test/{configLoader-LD4RV2WQ.js → configLoader-YE2CYGDG.js} +21 -0
- package/dist/src/utils/test/{contractTestRunnerPlaywright-IRJOAEMT.js → contractTestRunnerPlaywright-LC5OAVXB.js} +262 -200
- package/dist/src/utils/test/dsl/index.cjs +320 -0
- package/dist/src/utils/test/dsl/index.d.cts +136 -0
- package/dist/src/utils/test/dsl/index.d.ts +136 -0
- package/dist/src/utils/test/dsl/index.js +318 -0
- package/dist/src/utils/test/index.cjs +472 -88
- package/dist/src/utils/test/index.js +97 -12
- 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(` ${
|
|
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(` ${
|
|
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]
|
|
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/
|
|
1018
|
-
var
|
|
1019
|
-
|
|
1020
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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/
|
|
1124
|
-
var
|
|
1125
|
-
|
|
1126
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
1288
|
+
listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
1154
1289
|
}
|
|
1155
|
-
if (!
|
|
1156
|
-
const
|
|
1157
|
-
await
|
|
1158
|
-
|
|
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 (!
|
|
1295
|
+
if (!listBoxClosed) {
|
|
1161
1296
|
await page.mouse.click(10, 10);
|
|
1162
|
-
|
|
1297
|
+
listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
1163
1298
|
}
|
|
1164
|
-
if (!
|
|
1299
|
+
if (!listBoxClosed) {
|
|
1165
1300
|
throw new Error(
|
|
1166
|
-
`\u274C FATAL: Cannot close
|
|
1301
|
+
`\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
|
|
1167
1302
|
1. Escape key
|
|
1168
|
-
2. Clicking
|
|
1303
|
+
2. Clicking button
|
|
1169
1304
|
3. Clicking outside
|
|
1170
|
-
This indicates a problem with the
|
|
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(
|
|
1182
|
-
|
|
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
|
|
1249
|
-
|
|
1250
|
-
|
|
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 =
|
|
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
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
return
|
|
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
|
-
|
|
1272
|
-
return new TabsComponentStrategy(mainSelector, selectors);
|
|
1516
|
+
return new strategyClass(mainSelector, selectors);
|
|
1273
1517
|
}
|
|
1274
|
-
return
|
|
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
|
|
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
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
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]
|
|
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
|
-
|
|
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
|
|
2157
|
-
|
|
2158
|
-
|
|
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
|
|
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
|
|
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(
|
|
2260
|
-
console.log(
|
|
2261
|
-
console.log(
|
|
2262
|
-
console.log(
|
|
2263
|
-
console.log(
|
|
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
|
|
2299
|
-
var
|
|
2300
|
-
var
|
|
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(
|
|
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(
|
|
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(
|
|
2316
|
-
errors.forEach((err) => console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
3196
|
+
console.log(import_chalk3.default.red("\u274C No pages were successfully audited."));
|
|
2365
3197
|
process.exit(1);
|
|
2366
3198
|
}
|
|
2367
|
-
console.log(
|
|
2368
|
-
console.log(
|
|
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(
|
|
2373
|
-
console.log(
|
|
2374
|
-
console.log(
|
|
2375
|
-
console.log(
|
|
2376
|
-
console.log(
|
|
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
|
|
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 =
|
|
2388
|
-
await
|
|
2389
|
-
console.log(
|
|
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(
|
|
3231
|
+
console.log(import_chalk3.default.red(`
|
|
2400
3232
|
\u274C Accessibility violations found!`));
|
|
2401
|
-
console.log(
|
|
2402
|
-
console.log(
|
|
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(
|
|
2405
|
-
console.log(
|
|
2406
|
-
console.log(
|
|
2407
|
-
console.log(
|
|
2408
|
-
console.log(
|
|
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
|
});
|