aria-ease 6.8.0 → 6.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +68 -6
  2. package/bin/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
  3. package/bin/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
  4. package/bin/MenuComponentStrategy-JAMTCSNF.js +81 -0
  5. package/bin/TabsComponentStrategy-3SQURPMX.js +29 -0
  6. package/bin/buildContracts-GBOY7UXG.js +437 -0
  7. package/bin/{chunk-VPBHLMAS.js → chunk-LMSKLN5O.js} +21 -0
  8. package/bin/chunk-PK5L2SAF.js +17 -0
  9. package/bin/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
  10. package/bin/cli.cjs +991 -128
  11. package/bin/cli.js +33 -2
  12. package/bin/{configLoader-XRF6VM4J.js → configLoader-Q6A4JLKW.js} +1 -1
  13. package/{dist/contractTestRunnerPlaywright-UAOFNS7Z.js → bin/contractTestRunnerPlaywright-ZZNWDUYP.js} +270 -219
  14. package/bin/{test-WRIJHN6H.js → test-OND56UUL.js} +97 -10
  15. package/dist/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
  16. package/dist/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
  17. package/dist/MenuComponentStrategy-JAMTCSNF.js +81 -0
  18. package/dist/TabsComponentStrategy-3SQURPMX.js +29 -0
  19. package/dist/chunk-PK5L2SAF.js +17 -0
  20. package/dist/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
  21. package/dist/{configLoader-IT4PWCJB.js → configLoader-WTGJAP4Z.js} +21 -0
  22. package/{bin/contractTestRunnerPlaywright-UAOFNS7Z.js → dist/contractTestRunnerPlaywright-XBWJZMR3.js} +270 -219
  23. package/dist/index.cjs +794 -90
  24. package/dist/index.d.cts +136 -1
  25. package/dist/index.d.ts +136 -1
  26. package/dist/index.js +415 -10
  27. package/dist/src/utils/test/AccordionComponentStrategy-WRHZOEN6.js +38 -0
  28. package/dist/src/utils/test/ComboboxComponentStrategy-5AECQSRN.js +60 -0
  29. package/dist/src/utils/test/MenuComponentStrategy-VKZQYLBE.js +77 -0
  30. package/dist/src/utils/test/TabsComponentStrategy-BKG53SEV.js +26 -0
  31. package/dist/src/utils/test/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
  32. package/dist/src/utils/test/{configLoader-LD4RV2WQ.js → configLoader-YE2CYGDG.js} +21 -0
  33. package/dist/src/utils/test/{contractTestRunnerPlaywright-IRJOAEMT.js → contractTestRunnerPlaywright-LC5OAVXB.js} +262 -200
  34. package/dist/src/utils/test/dsl/index.cjs +320 -0
  35. package/dist/src/utils/test/dsl/index.d.cts +136 -0
  36. package/dist/src/utils/test/dsl/index.d.ts +136 -0
  37. package/dist/src/utils/test/dsl/index.js +318 -0
  38. package/dist/src/utils/test/index.cjs +472 -88
  39. package/dist/src/utils/test/index.js +97 -12
  40. package/package.json +9 -3
@@ -5,7 +5,7 @@ import {
5
5
  normalizeLevel,
6
6
  normalizeStrictness,
7
7
  resolveEnforcement
8
- } from "./chunk-2TOYEY5L.js";
8
+ } from "./chunk-XERMSYEH.js";
9
9
  import "./chunk-I2KLQ2HA.js";
10
10
 
11
11
  // src/utils/test/src/test.ts
@@ -24,7 +24,7 @@ async function runContractTests(componentName, component, strictness) {
24
24
  const resolvedPath = new URL(contractPath, import.meta.url).pathname;
25
25
  const contractData = await fs.readFile(resolvedPath, "utf-8");
26
26
  const componentContract = JSON.parse(contractData);
27
- const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
27
+ const totalTests = (componentContract.relationships?.length || 0) + (componentContract.static[0]?.assertions.length || 0) + componentContract.dynamic.length;
28
28
  reporter.start(componentName, totalTests);
29
29
  const failures = [];
30
30
  const passes = [];
@@ -48,6 +48,82 @@ async function runContractTests(componentName, component, strictness) {
48
48
  let staticPassed = 0;
49
49
  let staticFailed = 0;
50
50
  let staticWarnings = 0;
51
+ for (const rel of componentContract.relationships || []) {
52
+ const relationshipLevel = normalizeLevel(rel.level);
53
+ if (rel.type === "aria-reference") {
54
+ const fromSelector = componentContract.selectors[rel.from];
55
+ const toSelector = componentContract.selectors[rel.to];
56
+ const relDescription = `${rel.from}.${rel.attribute} references ${rel.to}`;
57
+ if (!fromSelector || !toSelector) {
58
+ const outcome = classifyFailure(`Relationship selector missing: from="${rel.from}" or to="${rel.to}" not found in selectors.`, rel.level);
59
+ if (outcome.status === "fail") staticFailed += 1;
60
+ if (outcome.status === "warn") staticWarnings += 1;
61
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
62
+ continue;
63
+ }
64
+ const fromTarget = component.querySelector(fromSelector);
65
+ const toTarget = component.querySelector(toSelector);
66
+ if (!fromTarget || !toTarget) {
67
+ const outcome = classifyFailure(`Relationship target not found: ${!fromTarget ? rel.from : rel.to}.`, rel.level);
68
+ if (outcome.status === "fail") staticFailed += 1;
69
+ if (outcome.status === "warn") staticWarnings += 1;
70
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
71
+ continue;
72
+ }
73
+ const toId = toTarget.getAttribute("id");
74
+ const attrValue = fromTarget.getAttribute(rel.attribute) || "";
75
+ if (!toId) {
76
+ const outcome = classifyFailure(`Relationship target "${rel.to}" must have an id for ${rel.attribute} validation.`, rel.level);
77
+ if (outcome.status === "fail") staticFailed += 1;
78
+ if (outcome.status === "warn") staticWarnings += 1;
79
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
80
+ continue;
81
+ }
82
+ const references = attrValue.split(/\s+/).filter(Boolean);
83
+ if (!references.includes(toId)) {
84
+ const outcome = classifyFailure(`Expected ${rel.from} ${rel.attribute} to reference id "${toId}", found "${attrValue}".`, rel.level);
85
+ if (outcome.status === "fail") staticFailed += 1;
86
+ if (outcome.status === "warn") staticWarnings += 1;
87
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
88
+ continue;
89
+ }
90
+ passes.push(`Relationship valid: ${rel.from}.${rel.attribute} -> ${rel.to} (id=${toId}).`);
91
+ staticPassed += 1;
92
+ reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
93
+ continue;
94
+ }
95
+ if (rel.type === "contains") {
96
+ const parentSelector = componentContract.selectors[rel.parent];
97
+ const childSelector = componentContract.selectors[rel.child];
98
+ const relDescription = `${rel.parent} contains ${rel.child}`;
99
+ if (!parentSelector || !childSelector) {
100
+ const outcome = classifyFailure(`Relationship selector missing: parent="${rel.parent}" or child="${rel.child}" not found in selectors.`, rel.level);
101
+ if (outcome.status === "fail") staticFailed += 1;
102
+ if (outcome.status === "warn") staticWarnings += 1;
103
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
104
+ continue;
105
+ }
106
+ const parentTarget = component.querySelector(parentSelector);
107
+ if (!parentTarget) {
108
+ const outcome = classifyFailure(`Relationship parent target not found: ${rel.parent}.`, rel.level);
109
+ if (outcome.status === "fail") staticFailed += 1;
110
+ if (outcome.status === "warn") staticWarnings += 1;
111
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
112
+ continue;
113
+ }
114
+ const nestedChild = parentTarget.querySelector(childSelector);
115
+ if (!nestedChild) {
116
+ const outcome = classifyFailure(`Expected ${rel.parent} to contain descendant matching selector for ${rel.child}.`, rel.level);
117
+ if (outcome.status === "fail") staticFailed += 1;
118
+ if (outcome.status === "warn") staticWarnings += 1;
119
+ reporter.reportStaticTest(relDescription, outcome.status, outcome.detail, outcome.level);
120
+ continue;
121
+ }
122
+ passes.push(`Relationship valid: ${rel.parent} contains ${rel.child}.`);
123
+ staticPassed += 1;
124
+ reporter.reportStaticTest(relDescription, "pass", void 0, relationshipLevel);
125
+ }
126
+ }
51
127
  for (const test of componentContract.static[0].assertions) {
52
128
  if (test.target !== "relative") {
53
129
  const staticLevel = normalizeLevel(test.level);
@@ -103,6 +179,7 @@ async function runContractTests(componentName, component, strictness) {
103
179
  }
104
180
 
105
181
  // src/utils/test/src/test.ts
182
+ import path from "path";
106
183
  async function testUiComponent(componentName, component, url, options = {}) {
107
184
  if (!componentName || typeof componentName !== "string") {
108
185
  throw new Error("\u274C testUiComponent requires a valid componentName (string)");
@@ -141,14 +218,24 @@ Error: ${error instanceof Error ? error.message : String(error)}`
141
218
  return null;
142
219
  }
143
220
  let strictness = normalizeStrictness(options.strictness);
144
- if (options.strictness === void 0 && typeof window === "undefined") {
221
+ let config = {};
222
+ let configBaseDir = typeof process !== "undefined" ? process.cwd() : "";
223
+ if (typeof process !== "undefined" && typeof process.cwd === "function") {
145
224
  try {
146
- const { loadConfig } = await import("./configLoader-XRF6VM4J.js");
147
- const { config } = await loadConfig(process.cwd());
148
- const componentStrictness = config.test?.components?.find((comp) => comp?.name === componentName)?.strictness;
149
- strictness = normalizeStrictness(componentStrictness ?? config.test?.strictness);
225
+ const { loadConfig } = await import("./configLoader-Q6A4JLKW.js");
226
+ const result2 = await loadConfig(process.cwd());
227
+ config = result2.config;
228
+ if (result2.configPath) {
229
+ configBaseDir = path.dirname(result2.configPath);
230
+ }
231
+ if (options.strictness === void 0) {
232
+ const componentStrictness = config.test?.components?.find((comp) => comp?.name === componentName)?.strictness;
233
+ strictness = normalizeStrictness(componentStrictness ?? config.test?.strictness);
234
+ }
150
235
  } catch {
151
- strictness = "balanced";
236
+ if (options.strictness === void 0) {
237
+ strictness = "balanced";
238
+ }
152
239
  }
153
240
  }
154
241
  let contract;
@@ -157,8 +244,8 @@ Error: ${error instanceof Error ? error.message : String(error)}`
157
244
  const devServerUrl = await checkDevServer(url);
158
245
  if (devServerUrl) {
159
246
  console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
160
- const { runContractTestsPlaywright } = await import("./contractTestRunnerPlaywright-UAOFNS7Z.js");
161
- contract = await runContractTestsPlaywright(componentName, devServerUrl, strictness);
247
+ const { runContractTestsPlaywright } = await import("./contractTestRunnerPlaywright-ZZNWDUYP.js");
248
+ contract = await runContractTestsPlaywright(componentName, devServerUrl, strictness, config, configBaseDir);
162
249
  } else {
163
250
  throw new Error(
164
251
  `\u274C Dev server not running at ${url}
@@ -0,0 +1,42 @@
1
+ import {
2
+ test_exports
3
+ } from "./chunk-PK5L2SAF.js";
4
+ import "./chunk-I2KLQ2HA.js";
5
+
6
+ // src/utils/test/src/component-strategies/AccordionComponentStrategy.ts
7
+ var AccordionComponentStrategy = class {
8
+ constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
9
+ this.mainSelector = mainSelector;
10
+ this.selectors = selectors;
11
+ this.actionTimeoutMs = actionTimeoutMs;
12
+ this.assertionTimeoutMs = assertionTimeoutMs;
13
+ }
14
+ async resetState(page) {
15
+ if (!this.selectors.panel || !this.selectors.trigger || this.selectors.popup) {
16
+ return;
17
+ }
18
+ const triggerSelector = this.selectors.trigger;
19
+ const panelSelector = this.selectors.panel;
20
+ if (!triggerSelector || !panelSelector) return;
21
+ const allTriggers = await page.locator(triggerSelector).all();
22
+ for (const trigger of allTriggers) {
23
+ const isExpanded = await trigger.getAttribute("aria-expanded") === "true";
24
+ const triggerPanel = await trigger.getAttribute("aria-controls");
25
+ if (isExpanded && triggerPanel) {
26
+ await trigger.click({ timeout: this.actionTimeoutMs });
27
+ const panel = page.locator(`#${triggerPanel}`);
28
+ await (0, test_exports.expect)(panel).toBeHidden({ timeout: this.assertionTimeoutMs }).catch(() => {
29
+ });
30
+ }
31
+ }
32
+ }
33
+ async shouldSkipTest() {
34
+ return false;
35
+ }
36
+ getMainSelector() {
37
+ return this.mainSelector;
38
+ }
39
+ };
40
+ export {
41
+ AccordionComponentStrategy
42
+ };
@@ -0,0 +1,64 @@
1
+ import {
2
+ test_exports
3
+ } from "./chunk-PK5L2SAF.js";
4
+ import "./chunk-I2KLQ2HA.js";
5
+
6
+ // src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts
7
+ var ComboboxComponentStrategy = class {
8
+ constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
9
+ this.mainSelector = mainSelector;
10
+ this.selectors = selectors;
11
+ this.actionTimeoutMs = actionTimeoutMs;
12
+ this.assertionTimeoutMs = assertionTimeoutMs;
13
+ }
14
+ async resetState(page) {
15
+ if (!this.selectors.popup) return;
16
+ const popupSelector = this.selectors.popup;
17
+ const popupElement = page.locator(popupSelector).first();
18
+ const isPopupVisible = await popupElement.isVisible().catch(() => false);
19
+ if (!isPopupVisible) return;
20
+ let listBoxClosed = false;
21
+ let closeSelector = this.selectors.input;
22
+ if (!closeSelector && this.selectors.focusable) {
23
+ closeSelector = this.selectors.focusable;
24
+ } else if (!closeSelector) {
25
+ closeSelector = this.selectors.button;
26
+ }
27
+ if (closeSelector) {
28
+ const closeElement = page.locator(closeSelector).first();
29
+ await closeElement.focus();
30
+ await page.keyboard.press("Escape");
31
+ listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
32
+ }
33
+ if (!listBoxClosed && this.selectors.button) {
34
+ const buttonElement = page.locator(this.selectors.button).first();
35
+ await buttonElement.click({ timeout: this.actionTimeoutMs });
36
+ listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
37
+ }
38
+ if (!listBoxClosed) {
39
+ await page.mouse.click(10, 10);
40
+ listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
41
+ }
42
+ if (!listBoxClosed) {
43
+ throw new Error(
44
+ `\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
45
+ 1. Escape key
46
+ 2. Clicking button
47
+ 3. Clicking outside
48
+ This indicates a problem with the combobox component's close functionality.`
49
+ );
50
+ }
51
+ if (this.selectors.input) {
52
+ await page.locator(this.selectors.input).first().clear();
53
+ }
54
+ }
55
+ async shouldSkipTest() {
56
+ return false;
57
+ }
58
+ getMainSelector() {
59
+ return this.mainSelector;
60
+ }
61
+ };
62
+ export {
63
+ ComboboxComponentStrategy
64
+ };
@@ -0,0 +1,81 @@
1
+ import {
2
+ test_exports
3
+ } from "./chunk-PK5L2SAF.js";
4
+ import "./chunk-I2KLQ2HA.js";
5
+
6
+ // src/utils/test/src/component-strategies/MenuComponentStrategy.ts
7
+ var MenuComponentStrategy = class {
8
+ constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
9
+ this.mainSelector = mainSelector;
10
+ this.selectors = selectors;
11
+ this.actionTimeoutMs = actionTimeoutMs;
12
+ this.assertionTimeoutMs = assertionTimeoutMs;
13
+ }
14
+ async resetState(page) {
15
+ if (!this.selectors.popup) return;
16
+ const popupSelector = this.selectors.popup;
17
+ const popupElement = page.locator(popupSelector).first();
18
+ const isPopupVisible = await popupElement.isVisible().catch(() => false);
19
+ if (!isPopupVisible) return;
20
+ let menuClosed = false;
21
+ let closeSelector = this.selectors.input;
22
+ if (!closeSelector && this.selectors.focusable) {
23
+ closeSelector = this.selectors.focusable;
24
+ } else if (!closeSelector) {
25
+ closeSelector = this.selectors.trigger;
26
+ }
27
+ if (closeSelector) {
28
+ const closeElement = page.locator(closeSelector).first();
29
+ await closeElement.focus();
30
+ await page.keyboard.press("Escape");
31
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
32
+ }
33
+ if (!menuClosed && this.selectors.trigger) {
34
+ const triggerElement = page.locator(this.selectors.trigger).first();
35
+ await triggerElement.click({ timeout: this.actionTimeoutMs });
36
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
37
+ }
38
+ if (!menuClosed) {
39
+ await page.mouse.click(10, 10);
40
+ menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
41
+ }
42
+ if (!menuClosed) {
43
+ throw new Error(
44
+ `\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
45
+ 1. Escape key
46
+ 2. Clicking trigger
47
+ 3. Clicking outside
48
+ This indicates a problem with the menu component's close functionality.`
49
+ );
50
+ }
51
+ if (this.selectors.input) {
52
+ await page.locator(this.selectors.input).first().clear();
53
+ }
54
+ if (this.selectors.trigger) {
55
+ const triggerElement = page.locator(this.selectors.trigger).first();
56
+ await triggerElement.focus();
57
+ }
58
+ }
59
+ async shouldSkipTest(test, page) {
60
+ const requiresSubmenu = test.action.some(
61
+ (act) => act.target === "submenu" || act.target === "submenuTrigger" || act.target === "submenuItems"
62
+ ) || test.assertions.some(
63
+ (assertion) => assertion.target === "submenu" || assertion.target === "submenuTrigger" || assertion.target === "submenuItems"
64
+ );
65
+ if (!requiresSubmenu) {
66
+ return false;
67
+ }
68
+ const submenuTriggerSelector = this.selectors.submenuTrigger;
69
+ if (!submenuTriggerSelector) {
70
+ return true;
71
+ }
72
+ const submenuTriggerCount = await page.locator(submenuTriggerSelector).count();
73
+ return submenuTriggerCount === 0;
74
+ }
75
+ getMainSelector() {
76
+ return this.mainSelector;
77
+ }
78
+ };
79
+ export {
80
+ MenuComponentStrategy
81
+ };
@@ -0,0 +1,29 @@
1
+ import "./chunk-I2KLQ2HA.js";
2
+
3
+ // src/utils/test/src/component-strategies/TabsComponentStrategy.ts
4
+ var TabsComponentStrategy = class {
5
+ constructor(mainSelector, selectors) {
6
+ this.mainSelector = mainSelector;
7
+ this.selectors = selectors;
8
+ }
9
+ async resetState() {
10
+ }
11
+ async shouldSkipTest(test, page) {
12
+ if (test.isVertical !== void 0 && this.selectors.tablist) {
13
+ const tablistSelector = this.selectors.tablist;
14
+ const tablist = page.locator(tablistSelector).first();
15
+ const orientation = await tablist.getAttribute("aria-orientation");
16
+ const isVertical = orientation === "vertical";
17
+ if (test.isVertical !== isVertical) {
18
+ return true;
19
+ }
20
+ }
21
+ return false;
22
+ }
23
+ getMainSelector() {
24
+ return this.mainSelector;
25
+ }
26
+ };
27
+ export {
28
+ TabsComponentStrategy
29
+ };
@@ -0,0 +1,17 @@
1
+ import {
2
+ __export,
3
+ __reExport
4
+ } from "./chunk-I2KLQ2HA.js";
5
+
6
+ // node_modules/@playwright/test/index.mjs
7
+ var test_exports = {};
8
+ __export(test_exports, {
9
+ default: () => default2
10
+ });
11
+ __reExport(test_exports, test_star);
12
+ import * as test_star from "playwright/test";
13
+ import { default as default2 } from "playwright/test";
14
+
15
+ export {
16
+ test_exports
17
+ };
@@ -30,11 +30,13 @@ var ContractReporter = class {
30
30
  skipped = 0;
31
31
  warnings = 0;
32
32
  isPlaywright = false;
33
+ isCustomContract = false;
33
34
  apgUrl = "https://www.w3.org/WAI/ARIA/apg/";
34
35
  hasPrintedStaticSection = false;
35
36
  hasPrintedDynamicSection = false;
36
- constructor(isPlaywright = false) {
37
+ constructor(isPlaywright = false, isCustomContract = false) {
37
38
  this.isPlaywright = isPlaywright;
39
+ this.isCustomContract = isCustomContract;
38
40
  }
39
41
  log(message) {
40
42
  process.stderr.write(message + "\n");
@@ -195,6 +197,13 @@ ${"\u2500".repeat(60)}`);
195
197
  const totalPasses = this.staticPasses + dynamicPasses;
196
198
  const totalFailures = this.staticFailures + dynamicFailures;
197
199
  const totalRun = totalPasses + totalFailures + this.warnings;
200
+ const getComponentMessage = () => {
201
+ const componentDisplayName = `${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)}`;
202
+ if (this.isCustomContract) {
203
+ return `${componentDisplayName} component validates against your custom accessibility policy \u2713`;
204
+ }
205
+ return `${componentDisplayName} component meets Aria-Ease baseline WAI-ARIA expectations \u2713`;
206
+ };
198
207
  if (failures.length > 0) {
199
208
  this.reportFailures(failures);
200
209
  }
@@ -206,7 +215,7 @@ ${"\u2550".repeat(60)}`);
206
215
  `);
207
216
  if (totalFailures === 0 && this.skipped === 0 && this.warnings === 0) {
208
217
  this.log(`\u2705 All ${totalRun} tests passed!`);
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`);
218
+ this.log(` ${getComponentMessage()}`);
210
219
  } else if (totalFailures === 0) {
211
220
  this.log(`\u2705 ${totalPasses}/${totalRun} tests passed`);
212
221
  if (this.skipped > 0) {
@@ -215,7 +224,7 @@ ${"\u2550".repeat(60)}`);
215
224
  if (this.warnings > 0) {
216
225
  this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
217
226
  }
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`);
227
+ this.log(` ${getComponentMessage()}`);
219
228
  } else {
220
229
  this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
221
230
  this.log(`\u2705 ${totalPasses} test${totalPasses > 1 ? "s" : ""} passed`);
@@ -56,6 +56,9 @@ function validateConfig(config) {
56
56
  if (comp.path !== void 0 && typeof comp.path !== "string") {
57
57
  errors.push(`test.components[${idx}].path must be a string when provided`);
58
58
  }
59
+ if (comp.strategyPath !== void 0 && typeof comp.strategyPath !== "string") {
60
+ errors.push(`test.components[${idx}].strategyPath must be a string when provided`);
61
+ }
59
62
  if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
60
63
  errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
61
64
  }
@@ -70,6 +73,24 @@ function validateConfig(config) {
70
73
  }
71
74
  }
72
75
  }
76
+ if (cfg.contracts !== void 0) {
77
+ if (!Array.isArray(cfg.contracts)) {
78
+ errors.push("contracts must be an array");
79
+ } else {
80
+ cfg.contracts.forEach((contract, idx) => {
81
+ if (typeof contract !== "object" || contract === null) {
82
+ errors.push(`contracts[${idx}] must be an object`);
83
+ } else {
84
+ if (typeof contract.src !== "string") {
85
+ errors.push(`contracts[${idx}].src is required and must be a string`);
86
+ }
87
+ if (contract.out !== void 0 && typeof contract.out !== "string") {
88
+ errors.push(`contracts[${idx}].out must be a string`);
89
+ }
90
+ }
91
+ });
92
+ }
93
+ }
73
94
  return { valid: errors.length === 0, errors };
74
95
  }
75
96
  async function loadConfigFile(filePath) {