aria-ease 6.6.0 → 6.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -16
- package/bin/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +87 -35
- package/bin/chunk-VPBHLMAS.js +127 -0
- package/bin/cli.cjs +380 -231
- package/bin/cli.js +8 -123
- package/bin/configLoader-XRF6VM4J.js +7 -0
- package/{dist/contractTestRunnerPlaywright-PC6JOYYV.js → bin/contractTestRunnerPlaywright-UAOFNS7Z.js} +98 -59
- package/bin/{test-LP723IXM.js → test-WRIJHN6H.js} +65 -24
- package/dist/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +87 -35
- package/dist/configLoader-IT4PWCJB.js +128 -0
- package/{bin/contractTestRunnerPlaywright-PC6JOYYV.js → dist/contractTestRunnerPlaywright-UAOFNS7Z.js} +98 -59
- package/dist/index.cjs +404 -125
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +83 -29
- package/dist/src/menu/index.cjs +18 -5
- package/dist/src/menu/index.js +18 -5
- package/dist/src/utils/test/aria-contracts/accordion/accordion.contract.json +13 -19
- package/dist/src/utils/test/aria-contracts/combobox/combobox.listbox.contract.json +5 -5
- package/dist/src/utils/test/aria-contracts/menu/menu.contract.json +45 -20
- package/dist/src/utils/test/aria-contracts/tabs/tabs.contract.json +3 -3
- package/dist/src/utils/test/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +85 -36
- package/dist/src/utils/test/configLoader-LD4RV2WQ.js +126 -0
- package/dist/src/utils/test/{contractTestRunnerPlaywright-RGKMGXND.js → contractTestRunnerPlaywright-IRJOAEMT.js} +94 -58
- package/dist/src/utils/test/index.cjs +380 -119
- package/dist/src/utils/test/index.d.cts +7 -1
- package/dist/src/utils/test/index.d.ts +7 -1
- package/dist/src/utils/test/index.js +61 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,19 +13,15 @@ Stop treating accessibility as an afterthought. Aria-Ease engineers accessibilit
|
|
|
13
13
|
|
|
14
14
|
Aria-Ease isn't a utility library. **It's an accessibility infrastructure** that integrates into every phase of your frontend engineering lifecycle:
|
|
15
15
|
|
|
16
|
-
| Phase | Feature | Status | Impact
|
|
17
|
-
| ------------------ | --------------------------------------------- | ------------ |
|
|
18
|
-
| **🔧 Development** | Component utilities for accessible patterns | ✅ Available | Build it right from the start
|
|
19
|
-
| **⚡ Linting** | ESLint rules to enforce accessible coding | 🚧 Roadmap | Catch mistakes as you type
|
|
20
|
-
| **🔍 Pre-Deploy** | Axe-core powered static accessibility audit | ✅ Available | Verify before it ships
|
|
21
|
-
| **🧪 Testing** | WAI-ARIA APG contract testing with Playwright | ✅ Available |
|
|
22
|
-
| **🚀 CI/CD** | Accessibility as deployment gatekeeper | ✅ Available | Block inaccessible code from production
|
|
23
|
-
| **📊 Production** | Real user signal monitoring and replay | 🚧 Roadmap | Understand how users actually interact
|
|
24
|
-
| **📈 Insights** | Dashboard for reporting and analytics | 🚧 Roadmap | Visualize accessibility health
|
|
25
|
-
|
|
26
|
-
**The philosophy:** By the time your app reaches manual testing, there should only be minute, non-automatable aspects left to verify.
|
|
27
|
-
|
|
28
|
-
**The reality:** Code that fails accessibility checks cannot reach production. Period.
|
|
16
|
+
| Phase | Feature | Status | Impact |
|
|
17
|
+
| ------------------ | --------------------------------------------- | ------------ | --------------------------------------------- |
|
|
18
|
+
| **🔧 Development** | Component utilities for accessible patterns | ✅ Available | Build it right from the start |
|
|
19
|
+
| **⚡ Linting** | ESLint rules to enforce accessible coding | 🚧 Roadmap | Catch mistakes as you type |
|
|
20
|
+
| **🔍 Pre-Deploy** | Axe-core powered static accessibility audit | ✅ Available | Verify before it ships |
|
|
21
|
+
| **🧪 Testing** | WAI-ARIA APG contract testing with Playwright | ✅ Available | Fast, determinic component accessibility test |
|
|
22
|
+
| **🚀 CI/CD** | Accessibility as deployment gatekeeper | ✅ Available | Block inaccessible code from production |
|
|
23
|
+
| **📊 Production** | Real user signal monitoring and replay | 🚧 Roadmap | Understand how users actually interact |
|
|
24
|
+
| **📈 Insights** | Dashboard for reporting and analytics | 🚧 Roadmap | Visualize accessibility health |
|
|
29
25
|
|
|
30
26
|
---
|
|
31
27
|
|
|
@@ -35,7 +31,7 @@ Aria-Ease isn't a utility library. **It's an accessibility infrastructure** that
|
|
|
35
31
|
|
|
36
32
|
**Traditional approach:** Build features → Manual testing → Find accessibility issues → Fix them → Manual testing again → Ship (maybe)
|
|
37
33
|
|
|
38
|
-
**Aria-Ease approach:** Build with accessible utilities → Automated audits catch issues → Contract tests verify
|
|
34
|
+
**Aria-Ease approach:** Build with accessible utilities → Automated audits catch issues → Contract tests verify deterministic component behaviors → CI/CD gates deployment → Ship with confidence
|
|
39
35
|
|
|
40
36
|
### What Makes This Different?
|
|
41
37
|
|
|
@@ -74,7 +70,7 @@ npx aria-ease test
|
|
|
74
70
|
# ✓ 26 assertions in ~1 second in CI
|
|
75
71
|
```
|
|
76
72
|
|
|
77
|
-
**Why this matters:** Before, verifying a combobox meant
|
|
73
|
+
**Why this matters:** Before, verifying a combobox meant testing every interaction manually. Now, Aria-Ease automates the deterministic aspects of testing a combobox; keyboard interaction, ARIA states update, visibility, semantic roles.
|
|
78
74
|
|
|
79
75
|
#### 4. **CI/CD Integration** (Available Now)
|
|
80
76
|
|
|
@@ -158,6 +154,9 @@ export default {
|
|
|
158
154
|
out: "./accessibility-reports",
|
|
159
155
|
},
|
|
160
156
|
},
|
|
157
|
+
test: {
|
|
158
|
+
strictness: "balanced", // 'minimal' | 'balanced' | 'strict' | 'paranoid'
|
|
159
|
+
},
|
|
161
160
|
};
|
|
162
161
|
```
|
|
163
162
|
|
|
@@ -662,6 +661,72 @@ describe("Shopify User Menu Accessibility Test", () => {
|
|
|
662
661
|
});
|
|
663
662
|
```
|
|
664
663
|
|
|
664
|
+
### Strictness Modes
|
|
665
|
+
|
|
666
|
+
Aria-Ease supports strictness policies to define how APG specs are enforced.
|
|
667
|
+
|
|
668
|
+
- `minimal`
|
|
669
|
+
- `required` -> error
|
|
670
|
+
- `recommended` -> ignore
|
|
671
|
+
- `optional` -> ignore
|
|
672
|
+
- `balanced` (default)
|
|
673
|
+
- `required` -> error
|
|
674
|
+
- `recommended` -> warning
|
|
675
|
+
- `optional` -> ignore
|
|
676
|
+
- `strict`
|
|
677
|
+
- `required` -> error
|
|
678
|
+
- `recommended` -> error
|
|
679
|
+
- `optional` -> warning
|
|
680
|
+
- `paranoid`
|
|
681
|
+
- `required` -> error
|
|
682
|
+
- `recommended` -> error
|
|
683
|
+
- `optional` -> error
|
|
684
|
+
|
|
685
|
+
Configure strictness globally in `ariaease.config.*`:
|
|
686
|
+
|
|
687
|
+
```javascript
|
|
688
|
+
export default {
|
|
689
|
+
test: {
|
|
690
|
+
strictness: "balanced",
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Or define strictness per component (recommended when components have different maturity levels):
|
|
696
|
+
|
|
697
|
+
```javascript
|
|
698
|
+
export default {
|
|
699
|
+
test: {
|
|
700
|
+
strictness: "balanced", // fallback
|
|
701
|
+
components: [
|
|
702
|
+
{
|
|
703
|
+
name: "menu",
|
|
704
|
+
strictness: "strict",
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
name: "accordion",
|
|
708
|
+
strictness: "minimal",
|
|
709
|
+
},
|
|
710
|
+
],
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
`path` is optional and not required for strictness resolution.
|
|
716
|
+
|
|
717
|
+
Or override per test call:
|
|
718
|
+
|
|
719
|
+
```javascript
|
|
720
|
+
await testUiComponent(
|
|
721
|
+
"menu",
|
|
722
|
+
null,
|
|
723
|
+
"http://localhost:5173/test-harness?component=menu",
|
|
724
|
+
{
|
|
725
|
+
strictness: "strict",
|
|
726
|
+
},
|
|
727
|
+
);
|
|
728
|
+
```
|
|
729
|
+
|
|
665
730
|
---
|
|
666
731
|
|
|
667
732
|
## 📦 Bundle Size
|
|
@@ -989,7 +1054,7 @@ npx aria-ease test
|
|
|
989
1054
|
- Deterministic JSON contracts (no flaky selectors)
|
|
990
1055
|
- Runs headless in CI
|
|
991
1056
|
|
|
992
|
-
**Compare to manual testing:** Manually verifying keyboard interactions for a combobox (arrow keys, typing, Enter, Escape, Home/End, etc.) across browsers, verifying WAI-ARIA roles, states and properties = 20-30 minutes. Aria-Ease =
|
|
1057
|
+
**Compare to manual testing:** Manually verifying keyboard interactions for a combobox listbox (arrow keys, typing, Enter, Escape, Home/End, etc.) across browsers, verifying WAI-ARIA roles, states and properties = 20-30 minutes. Aria-Ease = ~2 seconds.
|
|
993
1058
|
|
|
994
1059
|
### The Moment of Truth
|
|
995
1060
|
|
|
@@ -24,10 +24,11 @@ var ContractReporter = class {
|
|
|
24
24
|
componentName = "";
|
|
25
25
|
staticPasses = 0;
|
|
26
26
|
staticFailures = 0;
|
|
27
|
+
staticWarnings = 0;
|
|
27
28
|
dynamicResults = [];
|
|
28
29
|
totalTests = 0;
|
|
29
30
|
skipped = 0;
|
|
30
|
-
|
|
31
|
+
warnings = 0;
|
|
31
32
|
isPlaywright = false;
|
|
32
33
|
apgUrl = "https://www.w3.org/WAI/ARIA/apg/";
|
|
33
34
|
hasPrintedStaticSection = false;
|
|
@@ -54,23 +55,27 @@ ${"\u2550".repeat(60)}`);
|
|
|
54
55
|
this.log(`${"\u2550".repeat(60)}
|
|
55
56
|
`);
|
|
56
57
|
}
|
|
57
|
-
reportStatic(passes, failures) {
|
|
58
|
+
reportStatic(passes, failures, warnings = 0) {
|
|
58
59
|
this.staticPasses = passes;
|
|
59
60
|
this.staticFailures = failures;
|
|
61
|
+
this.staticWarnings = warnings;
|
|
60
62
|
}
|
|
61
63
|
/**
|
|
62
64
|
* Report individual static test pass
|
|
63
65
|
*/
|
|
64
|
-
reportStaticTest(description,
|
|
66
|
+
reportStaticTest(description, status, failureMessage, level) {
|
|
65
67
|
if (!this.hasPrintedStaticSection) {
|
|
66
68
|
this.log(`${"\u2500".repeat(60)}`);
|
|
67
69
|
this.log(`\u{1F9EA} Static Assertions`);
|
|
68
70
|
this.log(`${"\u2500".repeat(60)}`);
|
|
69
71
|
this.hasPrintedStaticSection = true;
|
|
70
72
|
}
|
|
71
|
-
const icon =
|
|
73
|
+
const icon = status === "pass" ? "\u2713" : status === "warn" ? "\u26A0" : status === "skip" ? "\u25CB" : "\u2717";
|
|
72
74
|
this.log(` ${icon} ${description}`);
|
|
73
|
-
if (
|
|
75
|
+
if (level) {
|
|
76
|
+
this.log(` \u21B3 level=${level}`);
|
|
77
|
+
}
|
|
78
|
+
if ((status === "fail" || status === "warn" || status === "skip") && failureMessage) {
|
|
74
79
|
this.log(` \u21B3 ${failureMessage}`);
|
|
75
80
|
}
|
|
76
81
|
}
|
|
@@ -89,23 +94,26 @@ ${"\u2550".repeat(60)}`);
|
|
|
89
94
|
description: test.description,
|
|
90
95
|
status,
|
|
91
96
|
failureMessage,
|
|
92
|
-
|
|
97
|
+
level: test.level
|
|
93
98
|
};
|
|
94
99
|
if (status === "skip") {
|
|
95
100
|
result.skipReason = "Requires real browser (addEventListener events)";
|
|
96
101
|
}
|
|
97
102
|
this.dynamicResults.push(result);
|
|
98
|
-
const icons = { pass: "\u2713", fail: "\u2717",
|
|
99
|
-
const
|
|
100
|
-
this.log(` ${icons[status]} ${
|
|
103
|
+
const icons = { pass: "\u2713", fail: "\u2717", warn: "\u26A0", skip: "\u25CB" };
|
|
104
|
+
const levelPrefix = test.level ? `[${test.level.toUpperCase()}] ` : "";
|
|
105
|
+
this.log(` ${icons[status]} ${levelPrefix}${test.description}`);
|
|
101
106
|
if (status === "skip" && !this.isPlaywright) {
|
|
102
107
|
this.log(` \u21B3 Skipped in jsdom (runs in Playwright)`);
|
|
103
108
|
}
|
|
104
|
-
if (status === "fail" && failureMessage
|
|
109
|
+
if (status === "fail" && failureMessage) {
|
|
105
110
|
this.log(` \u21B3 ${failureMessage}`);
|
|
106
111
|
}
|
|
107
|
-
if (status === "
|
|
108
|
-
this.log(` \u21B3
|
|
112
|
+
if (status === "warn" && failureMessage) {
|
|
113
|
+
this.log(` \u21B3 ${failureMessage}`);
|
|
114
|
+
}
|
|
115
|
+
if (status === "skip" && failureMessage) {
|
|
116
|
+
this.log(` \u21B3 ${failureMessage}`);
|
|
109
117
|
}
|
|
110
118
|
}
|
|
111
119
|
/**
|
|
@@ -129,29 +137,29 @@ ${"\u2500".repeat(60)}`);
|
|
|
129
137
|
this.log("");
|
|
130
138
|
});
|
|
131
139
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
reportOptionalSuggestions() {
|
|
136
|
-
const suggestions = this.dynamicResults.filter((r) => r.status === "optional-fail");
|
|
137
|
-
if (suggestions.length === 0) return;
|
|
140
|
+
reportWarnings() {
|
|
141
|
+
const warnings = this.dynamicResults.filter((r) => r.status === "warn");
|
|
142
|
+
if (warnings.length === 0 && this.staticWarnings === 0) return;
|
|
138
143
|
this.log(`
|
|
139
144
|
${"\u2500".repeat(60)}`);
|
|
140
|
-
this.log(`\
|
|
145
|
+
this.log(`\u26A0\uFE0F Warnings (${this.staticWarnings + warnings.length}):
|
|
141
146
|
`);
|
|
142
|
-
this.log(`These
|
|
143
|
-
this.log(`for improved user experience and keyboard interaction:
|
|
147
|
+
this.log(`These checks are failing but treated as warnings under the active strictness mode.
|
|
144
148
|
`);
|
|
145
|
-
|
|
149
|
+
warnings.forEach((test, index) => {
|
|
146
150
|
this.log(`${index + 1}. ${test.description}`);
|
|
147
151
|
if (test.failureMessage) {
|
|
148
152
|
this.log(` \u21B3 ${test.failureMessage}`);
|
|
149
153
|
}
|
|
154
|
+
if (test.level) {
|
|
155
|
+
this.log(` \u21B3 level=${test.level}`);
|
|
156
|
+
}
|
|
150
157
|
});
|
|
151
|
-
this.
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
if (this.apgUrl) {
|
|
159
|
+
this.log(`
|
|
160
|
+
Reference: ${this.apgUrl}
|
|
154
161
|
`);
|
|
162
|
+
}
|
|
155
163
|
}
|
|
156
164
|
/**
|
|
157
165
|
* Report skipped tests with helpful context
|
|
@@ -181,41 +189,42 @@ ${"\u2500".repeat(60)}`);
|
|
|
181
189
|
const duration = Date.now() - this.startTime;
|
|
182
190
|
const dynamicPasses = this.dynamicResults.filter((r) => r.status === "pass").length;
|
|
183
191
|
const dynamicFailures = this.dynamicResults.filter((r) => r.status === "fail").length;
|
|
192
|
+
const dynamicWarnings = this.dynamicResults.filter((r) => r.status === "warn").length;
|
|
184
193
|
this.skipped = this.dynamicResults.filter((r) => r.status === "skip").length;
|
|
185
|
-
this.
|
|
194
|
+
this.warnings = this.staticWarnings + dynamicWarnings;
|
|
186
195
|
const totalPasses = this.staticPasses + dynamicPasses;
|
|
187
196
|
const totalFailures = this.staticFailures + dynamicFailures;
|
|
188
|
-
const totalRun = totalPasses + totalFailures;
|
|
197
|
+
const totalRun = totalPasses + totalFailures + this.warnings;
|
|
189
198
|
if (failures.length > 0) {
|
|
190
199
|
this.reportFailures(failures);
|
|
191
200
|
}
|
|
192
|
-
this.
|
|
201
|
+
this.reportWarnings();
|
|
193
202
|
this.reportSkipped();
|
|
194
203
|
this.log(`
|
|
195
204
|
${"\u2550".repeat(60)}`);
|
|
196
205
|
this.log(`\u{1F4CA} Summary
|
|
197
206
|
`);
|
|
198
|
-
if (totalFailures === 0 && this.skipped === 0 && this.
|
|
207
|
+
if (totalFailures === 0 && this.skipped === 0 && this.warnings === 0) {
|
|
199
208
|
this.log(`\u2705 All ${totalRun} tests passed!`);
|
|
200
209
|
this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
|
|
201
210
|
} else if (totalFailures === 0) {
|
|
202
|
-
this.log(`\u2705 ${totalPasses}/${totalRun}
|
|
211
|
+
this.log(`\u2705 ${totalPasses}/${totalRun} tests passed`);
|
|
203
212
|
if (this.skipped > 0) {
|
|
204
213
|
this.log(`\u25CB ${this.skipped} tests skipped`);
|
|
205
214
|
}
|
|
206
|
-
if (this.
|
|
207
|
-
this.log(`\
|
|
215
|
+
if (this.warnings > 0) {
|
|
216
|
+
this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
|
|
208
217
|
}
|
|
209
218
|
this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
|
|
210
219
|
} else {
|
|
211
220
|
this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
|
|
212
221
|
this.log(`\u2705 ${totalPasses} test${totalPasses > 1 ? "s" : ""} passed`);
|
|
222
|
+
if (this.warnings > 0) {
|
|
223
|
+
this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
|
|
224
|
+
}
|
|
213
225
|
if (this.skipped > 0) {
|
|
214
226
|
this.log(`\u25CB ${this.skipped} test${this.skipped > 1 ? "s" : ""} skipped`);
|
|
215
227
|
}
|
|
216
|
-
if (this.optionalSuggestions > 0) {
|
|
217
|
-
this.log(`\u{1F4A1} ${this.optionalSuggestions} optional enhancement${this.optionalSuggestions > 1 ? "s" : ""} suggested`);
|
|
218
|
-
}
|
|
219
228
|
}
|
|
220
229
|
this.log(`\u23F1\uFE0F Duration: ${duration}ms`);
|
|
221
230
|
this.log(`${"\u2550".repeat(60)}
|
|
@@ -250,6 +259,46 @@ ${"\u2550".repeat(60)}`);
|
|
|
250
259
|
}
|
|
251
260
|
};
|
|
252
261
|
|
|
262
|
+
// src/utils/test/src/strictness.ts
|
|
263
|
+
var FALLBACK_LEVEL = "required";
|
|
264
|
+
function normalizeLevel(level) {
|
|
265
|
+
if (level === "required" || level === "recommended" || level === "optional") {
|
|
266
|
+
return level;
|
|
267
|
+
}
|
|
268
|
+
return FALLBACK_LEVEL;
|
|
269
|
+
}
|
|
270
|
+
function normalizeStrictness(strictness) {
|
|
271
|
+
if (strictness === "minimal" || strictness === "balanced" || strictness === "strict" || strictness === "paranoid") {
|
|
272
|
+
return strictness;
|
|
273
|
+
}
|
|
274
|
+
return "balanced";
|
|
275
|
+
}
|
|
276
|
+
function resolveEnforcement(level, strictness) {
|
|
277
|
+
const matrix = {
|
|
278
|
+
minimal: {
|
|
279
|
+
required: "error",
|
|
280
|
+
recommended: "ignore",
|
|
281
|
+
optional: "ignore"
|
|
282
|
+
},
|
|
283
|
+
balanced: {
|
|
284
|
+
required: "error",
|
|
285
|
+
recommended: "warning",
|
|
286
|
+
optional: "ignore"
|
|
287
|
+
},
|
|
288
|
+
strict: {
|
|
289
|
+
required: "error",
|
|
290
|
+
recommended: "error",
|
|
291
|
+
optional: "warning"
|
|
292
|
+
},
|
|
293
|
+
paranoid: {
|
|
294
|
+
required: "error",
|
|
295
|
+
recommended: "error",
|
|
296
|
+
optional: "error"
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
return matrix[strictness][level];
|
|
300
|
+
}
|
|
301
|
+
|
|
253
302
|
// src/utils/test/src/playwrightTestHarness.ts
|
|
254
303
|
import { chromium } from "playwright";
|
|
255
304
|
var sharedBrowser = null;
|
|
@@ -297,6 +346,9 @@ async function closeSharedBrowser() {
|
|
|
297
346
|
export {
|
|
298
347
|
contract_default,
|
|
299
348
|
ContractReporter,
|
|
349
|
+
normalizeLevel,
|
|
350
|
+
normalizeStrictness,
|
|
351
|
+
resolveEnforcement,
|
|
300
352
|
createTestPage,
|
|
301
353
|
closeSharedBrowser
|
|
302
354
|
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/utils/cli/configLoader.ts
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
function validateConfig(config) {
|
|
5
|
+
const errors = [];
|
|
6
|
+
if (!config || typeof config !== "object") {
|
|
7
|
+
errors.push("Config must be an object");
|
|
8
|
+
return { valid: false, errors };
|
|
9
|
+
}
|
|
10
|
+
const cfg = config;
|
|
11
|
+
if (cfg.audit !== void 0) {
|
|
12
|
+
if (typeof cfg.audit !== "object" || cfg.audit === null) {
|
|
13
|
+
errors.push("audit must be an object");
|
|
14
|
+
} else {
|
|
15
|
+
if (cfg.audit.urls !== void 0) {
|
|
16
|
+
if (!Array.isArray(cfg.audit.urls)) {
|
|
17
|
+
errors.push("audit.urls must be an array");
|
|
18
|
+
} else if (cfg.audit.urls.some((url) => typeof url !== "string")) {
|
|
19
|
+
errors.push("audit.urls must contain only strings");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (cfg.audit.output !== void 0) {
|
|
23
|
+
if (typeof cfg.audit.output !== "object") {
|
|
24
|
+
errors.push("audit.output must be an object");
|
|
25
|
+
} else {
|
|
26
|
+
const output = cfg.audit.output;
|
|
27
|
+
if (output.format !== void 0) {
|
|
28
|
+
if (!["json", "csv", "html", "all"].includes(output.format)) {
|
|
29
|
+
errors.push("audit.output.format must be one of: json, csv, html, all");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (output.out !== void 0 && typeof output.out !== "string") {
|
|
33
|
+
errors.push("audit.output.out must be a string");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (cfg.test !== void 0) {
|
|
40
|
+
if (typeof cfg.test !== "object" || cfg.test === null) {
|
|
41
|
+
errors.push("test must be an object");
|
|
42
|
+
} else {
|
|
43
|
+
if (cfg.test.components !== void 0) {
|
|
44
|
+
if (!Array.isArray(cfg.test.components)) {
|
|
45
|
+
errors.push("test.components must be an array");
|
|
46
|
+
} else {
|
|
47
|
+
cfg.test.components.forEach((comp, idx) => {
|
|
48
|
+
if (typeof comp !== "object" || comp === null) {
|
|
49
|
+
errors.push(`test.components[${idx}] must be an object`);
|
|
50
|
+
} else {
|
|
51
|
+
if (typeof comp.name !== "string") {
|
|
52
|
+
errors.push(`test.components[${idx}].name must be a string`);
|
|
53
|
+
}
|
|
54
|
+
if (comp.path !== void 0 && typeof comp.path !== "string") {
|
|
55
|
+
errors.push(`test.components[${idx}].path must be a string when provided`);
|
|
56
|
+
}
|
|
57
|
+
if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
|
|
58
|
+
errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (cfg.test.strictness !== void 0) {
|
|
65
|
+
if (!["minimal", "balanced", "strict", "paranoid"].includes(cfg.test.strictness)) {
|
|
66
|
+
errors.push("test.strictness must be one of: minimal, balanced, strict, paranoid");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { valid: errors.length === 0, errors };
|
|
72
|
+
}
|
|
73
|
+
async function loadConfigFile(filePath) {
|
|
74
|
+
try {
|
|
75
|
+
const ext = path.extname(filePath);
|
|
76
|
+
if (ext === ".json") {
|
|
77
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
78
|
+
return JSON.parse(content);
|
|
79
|
+
} else if ([".js", ".mjs", ".cjs", ".ts"].includes(ext)) {
|
|
80
|
+
const imported = await import(filePath);
|
|
81
|
+
return imported.default || imported;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
89
|
+
const configNames = [
|
|
90
|
+
"ariaease.config.js",
|
|
91
|
+
"ariaease.config.mjs",
|
|
92
|
+
"ariaease.config.cjs",
|
|
93
|
+
"ariaease.config.json",
|
|
94
|
+
"ariaease.config.ts"
|
|
95
|
+
];
|
|
96
|
+
let loadedConfig = null;
|
|
97
|
+
let foundPath = null;
|
|
98
|
+
const errors = [];
|
|
99
|
+
for (const name of configNames) {
|
|
100
|
+
const configPath = path.resolve(cwd, name);
|
|
101
|
+
if (await fs.pathExists(configPath)) {
|
|
102
|
+
foundPath = configPath;
|
|
103
|
+
loadedConfig = await loadConfigFile(configPath);
|
|
104
|
+
if (loadedConfig === null) {
|
|
105
|
+
errors.push(`Found config at ${name} but failed to load it. Check for syntax errors.`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const validation = validateConfig(loadedConfig);
|
|
109
|
+
if (!validation.valid) {
|
|
110
|
+
errors.push(`Config validation failed in ${name}:`);
|
|
111
|
+
errors.push(...validation.errors.map((err) => ` - ${err}`));
|
|
112
|
+
loadedConfig = null;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
config: loadedConfig || {},
|
|
120
|
+
configPath: loadedConfig ? foundPath : null,
|
|
121
|
+
errors
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export {
|
|
126
|
+
loadConfig
|
|
127
|
+
};
|