aria-ease 6.6.0 → 6.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +75 -15
  2. package/bin/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +87 -35
  3. package/bin/chunk-VPBHLMAS.js +127 -0
  4. package/bin/cli.cjs +380 -231
  5. package/bin/cli.js +8 -123
  6. package/bin/configLoader-XRF6VM4J.js +7 -0
  7. package/{dist/contractTestRunnerPlaywright-PC6JOYYV.js → bin/contractTestRunnerPlaywright-UAOFNS7Z.js} +98 -59
  8. package/bin/{test-LP723IXM.js → test-WRIJHN6H.js} +65 -24
  9. package/dist/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +87 -35
  10. package/dist/configLoader-IT4PWCJB.js +128 -0
  11. package/{bin/contractTestRunnerPlaywright-PC6JOYYV.js → dist/contractTestRunnerPlaywright-UAOFNS7Z.js} +98 -59
  12. package/dist/index.cjs +404 -125
  13. package/dist/index.d.cts +6 -1
  14. package/dist/index.d.ts +6 -1
  15. package/dist/index.js +83 -29
  16. package/dist/src/menu/index.cjs +18 -5
  17. package/dist/src/menu/index.js +18 -5
  18. package/dist/src/utils/test/aria-contracts/accordion/accordion.contract.json +8 -8
  19. package/dist/src/utils/test/aria-contracts/combobox/combobox.listbox.contract.json +4 -4
  20. package/dist/src/utils/test/aria-contracts/menu/menu.contract.json +44 -19
  21. package/dist/src/utils/test/aria-contracts/tabs/tabs.contract.json +3 -3
  22. package/dist/src/utils/test/{chunk-LKN5PRYD.js → chunk-2TOYEY5L.js} +85 -36
  23. package/dist/src/utils/test/configLoader-LD4RV2WQ.js +126 -0
  24. package/dist/src/utils/test/{contractTestRunnerPlaywright-RGKMGXND.js → contractTestRunnerPlaywright-IRJOAEMT.js} +94 -58
  25. package/dist/src/utils/test/index.cjs +380 -119
  26. package/dist/src/utils/test/index.d.cts +7 -1
  27. package/dist/src/utils/test/index.d.ts +7 -1
  28. package/dist/src/utils/test/index.js +61 -23
  29. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -66,10 +66,11 @@ var init_ContractReporter = __esm({
66
66
  componentName = "";
67
67
  staticPasses = 0;
68
68
  staticFailures = 0;
69
+ staticWarnings = 0;
69
70
  dynamicResults = [];
70
71
  totalTests = 0;
71
72
  skipped = 0;
72
- optionalSuggestions = 0;
73
+ warnings = 0;
73
74
  isPlaywright = false;
74
75
  apgUrl = "https://www.w3.org/WAI/ARIA/apg/";
75
76
  hasPrintedStaticSection = false;
@@ -96,23 +97,27 @@ ${"\u2550".repeat(60)}`);
96
97
  this.log(`${"\u2550".repeat(60)}
97
98
  `);
98
99
  }
99
- reportStatic(passes, failures) {
100
+ reportStatic(passes, failures, warnings = 0) {
100
101
  this.staticPasses = passes;
101
102
  this.staticFailures = failures;
103
+ this.staticWarnings = warnings;
102
104
  }
103
105
  /**
104
106
  * Report individual static test pass
105
107
  */
106
- reportStaticTest(description, passed, failureMessage) {
108
+ reportStaticTest(description, status, failureMessage, level) {
107
109
  if (!this.hasPrintedStaticSection) {
108
110
  this.log(`${"\u2500".repeat(60)}`);
109
111
  this.log(`\u{1F9EA} Static Assertions`);
110
112
  this.log(`${"\u2500".repeat(60)}`);
111
113
  this.hasPrintedStaticSection = true;
112
114
  }
113
- const icon = passed ? "\u2713" : "\u2717";
115
+ const icon = status === "pass" ? "\u2713" : status === "warn" ? "\u26A0" : status === "skip" ? "\u25CB" : "\u2717";
114
116
  this.log(` ${icon} ${description}`);
115
- if (!passed && failureMessage) {
117
+ if (level) {
118
+ this.log(` \u21B3 level=${level}`);
119
+ }
120
+ if ((status === "fail" || status === "warn" || status === "skip") && failureMessage) {
116
121
  this.log(` \u21B3 ${failureMessage}`);
117
122
  }
118
123
  }
@@ -131,23 +136,26 @@ ${"\u2550".repeat(60)}`);
131
136
  description: test.description,
132
137
  status,
133
138
  failureMessage,
134
- isOptional: test.isOptional
139
+ level: test.level
135
140
  };
136
141
  if (status === "skip") {
137
142
  result.skipReason = "Requires real browser (addEventListener events)";
138
143
  }
139
144
  this.dynamicResults.push(result);
140
- const icons = { pass: "\u2713", fail: "\u2717", skip: "\u25CB", "optional-fail": "\u25CB" };
141
- const prefix = test.isOptional ? "[OPTIONAL] " : "";
142
- this.log(` ${icons[status]} ${prefix}${test.description}`);
145
+ const icons = { pass: "\u2713", fail: "\u2717", warn: "\u26A0", skip: "\u25CB" };
146
+ const levelPrefix = test.level ? `[${test.level.toUpperCase()}] ` : "";
147
+ this.log(` ${icons[status]} ${levelPrefix}${test.description}`);
143
148
  if (status === "skip" && !this.isPlaywright) {
144
149
  this.log(` \u21B3 Skipped in jsdom (runs in Playwright)`);
145
150
  }
146
- if (status === "fail" && failureMessage && !test.isOptional) {
151
+ if (status === "fail" && failureMessage) {
152
+ this.log(` \u21B3 ${failureMessage}`);
153
+ }
154
+ if (status === "warn" && failureMessage) {
147
155
  this.log(` \u21B3 ${failureMessage}`);
148
156
  }
149
- if (status === "optional-fail") {
150
- this.log(` \u21B3 Not implemented (recommended for enhanced UX)`);
157
+ if (status === "skip" && failureMessage) {
158
+ this.log(` \u21B3 ${failureMessage}`);
151
159
  }
152
160
  }
153
161
  /**
@@ -171,29 +179,29 @@ ${"\u2500".repeat(60)}`);
171
179
  this.log("");
172
180
  });
173
181
  }
174
- /**
175
- * Report optional features that aren't implemented
176
- */
177
- reportOptionalSuggestions() {
178
- const suggestions = this.dynamicResults.filter((r) => r.status === "optional-fail");
179
- if (suggestions.length === 0) return;
182
+ reportWarnings() {
183
+ const warnings = this.dynamicResults.filter((r) => r.status === "warn");
184
+ if (warnings.length === 0 && this.staticWarnings === 0) return;
180
185
  this.log(`
181
186
  ${"\u2500".repeat(60)}`);
182
- this.log(`\u{1F4A1} Optional Enhancements (${suggestions.length}):
187
+ this.log(`\u26A0\uFE0F Warnings (${this.staticWarnings + warnings.length}):
183
188
  `);
184
- this.log(`These features are optional per APG guidelines but recommended`);
185
- this.log(`for improved user experience and keyboard interaction:
189
+ this.log(`These checks are failing but treated as warnings under the active strictness mode.
186
190
  `);
187
- suggestions.forEach((test, index) => {
191
+ warnings.forEach((test, index) => {
188
192
  this.log(`${index + 1}. ${test.description}`);
189
193
  if (test.failureMessage) {
190
194
  this.log(` \u21B3 ${test.failureMessage}`);
191
195
  }
196
+ if (test.level) {
197
+ this.log(` \u21B3 level=${test.level}`);
198
+ }
192
199
  });
193
- this.log(`
194
- \u2728 Consider implementing these for better accessibility`);
195
- this.log(` Reference: ${this.apgUrl}
200
+ if (this.apgUrl) {
201
+ this.log(`
202
+ Reference: ${this.apgUrl}
196
203
  `);
204
+ }
197
205
  }
198
206
  /**
199
207
  * Report skipped tests with helpful context
@@ -223,41 +231,42 @@ ${"\u2500".repeat(60)}`);
223
231
  const duration = Date.now() - this.startTime;
224
232
  const dynamicPasses = this.dynamicResults.filter((r) => r.status === "pass").length;
225
233
  const dynamicFailures = this.dynamicResults.filter((r) => r.status === "fail").length;
234
+ const dynamicWarnings = this.dynamicResults.filter((r) => r.status === "warn").length;
226
235
  this.skipped = this.dynamicResults.filter((r) => r.status === "skip").length;
227
- this.optionalSuggestions = this.dynamicResults.filter((r) => r.status === "optional-fail").length;
236
+ this.warnings = this.staticWarnings + dynamicWarnings;
228
237
  const totalPasses = this.staticPasses + dynamicPasses;
229
238
  const totalFailures = this.staticFailures + dynamicFailures;
230
- const totalRun = totalPasses + totalFailures;
239
+ const totalRun = totalPasses + totalFailures + this.warnings;
231
240
  if (failures.length > 0) {
232
241
  this.reportFailures(failures);
233
242
  }
234
- this.reportOptionalSuggestions();
243
+ this.reportWarnings();
235
244
  this.reportSkipped();
236
245
  this.log(`
237
246
  ${"\u2550".repeat(60)}`);
238
247
  this.log(`\u{1F4CA} Summary
239
248
  `);
240
- if (totalFailures === 0 && this.skipped === 0 && this.optionalSuggestions === 0) {
249
+ if (totalFailures === 0 && this.skipped === 0 && this.warnings === 0) {
241
250
  this.log(`\u2705 All ${totalRun} tests passed!`);
242
251
  this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
243
252
  } else if (totalFailures === 0) {
244
- this.log(`\u2705 ${totalPasses}/${totalRun} required tests passed`);
253
+ this.log(`\u2705 ${totalPasses}/${totalRun} tests passed`);
245
254
  if (this.skipped > 0) {
246
255
  this.log(`\u25CB ${this.skipped} tests skipped`);
247
256
  }
248
- if (this.optionalSuggestions > 0) {
249
- this.log(`\u{1F4A1} ${this.optionalSuggestions} optional enhancement${this.optionalSuggestions > 1 ? "s" : ""} suggested`);
257
+ if (this.warnings > 0) {
258
+ this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
250
259
  }
251
260
  this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
252
261
  } else {
253
262
  this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
254
263
  this.log(`\u2705 ${totalPasses} test${totalPasses > 1 ? "s" : ""} passed`);
264
+ if (this.warnings > 0) {
265
+ this.log(`\u26A0\uFE0F ${this.warnings} warning${this.warnings > 1 ? "s" : ""}`);
266
+ }
255
267
  if (this.skipped > 0) {
256
268
  this.log(`\u25CB ${this.skipped} test${this.skipped > 1 ? "s" : ""} skipped`);
257
269
  }
258
- if (this.optionalSuggestions > 0) {
259
- this.log(`\u{1F4A1} ${this.optionalSuggestions} optional enhancement${this.optionalSuggestions > 1 ? "s" : ""} suggested`);
260
- }
261
270
  }
262
271
  this.log(`\u23F1\uFE0F Duration: ${duration}ms`);
263
272
  this.log(`${"\u2550".repeat(60)}
@@ -294,6 +303,52 @@ ${"\u2550".repeat(60)}`);
294
303
  }
295
304
  });
296
305
 
306
+ // src/utils/test/src/strictness.ts
307
+ function normalizeLevel(level) {
308
+ if (level === "required" || level === "recommended" || level === "optional") {
309
+ return level;
310
+ }
311
+ return FALLBACK_LEVEL;
312
+ }
313
+ function normalizeStrictness(strictness) {
314
+ if (strictness === "minimal" || strictness === "balanced" || strictness === "strict" || strictness === "paranoid") {
315
+ return strictness;
316
+ }
317
+ return "balanced";
318
+ }
319
+ function resolveEnforcement(level, strictness) {
320
+ const matrix = {
321
+ minimal: {
322
+ required: "error",
323
+ recommended: "ignore",
324
+ optional: "ignore"
325
+ },
326
+ balanced: {
327
+ required: "error",
328
+ recommended: "warning",
329
+ optional: "ignore"
330
+ },
331
+ strict: {
332
+ required: "error",
333
+ recommended: "error",
334
+ optional: "warning"
335
+ },
336
+ paranoid: {
337
+ required: "error",
338
+ recommended: "error",
339
+ optional: "error"
340
+ }
341
+ };
342
+ return matrix[strictness][level];
343
+ }
344
+ var FALLBACK_LEVEL;
345
+ var init_strictness = __esm({
346
+ "src/utils/test/src/strictness.ts"() {
347
+ "use strict";
348
+ FALLBACK_LEVEL = "required";
349
+ }
350
+ });
351
+
297
352
  // src/utils/test/src/playwrightTestHarness.ts
298
353
  async function getOrCreateBrowser() {
299
354
  if (!sharedBrowser) {
@@ -344,6 +399,140 @@ var init_playwrightTestHarness = __esm({
344
399
  }
345
400
  });
346
401
 
402
+ // src/utils/cli/configLoader.ts
403
+ var configLoader_exports = {};
404
+ __export(configLoader_exports, {
405
+ loadConfig: () => loadConfig
406
+ });
407
+ function validateConfig(config) {
408
+ const errors = [];
409
+ if (!config || typeof config !== "object") {
410
+ errors.push("Config must be an object");
411
+ return { valid: false, errors };
412
+ }
413
+ const cfg = config;
414
+ if (cfg.audit !== void 0) {
415
+ if (typeof cfg.audit !== "object" || cfg.audit === null) {
416
+ errors.push("audit must be an object");
417
+ } else {
418
+ if (cfg.audit.urls !== void 0) {
419
+ if (!Array.isArray(cfg.audit.urls)) {
420
+ errors.push("audit.urls must be an array");
421
+ } else if (cfg.audit.urls.some((url) => typeof url !== "string")) {
422
+ errors.push("audit.urls must contain only strings");
423
+ }
424
+ }
425
+ if (cfg.audit.output !== void 0) {
426
+ if (typeof cfg.audit.output !== "object") {
427
+ errors.push("audit.output must be an object");
428
+ } else {
429
+ const output = cfg.audit.output;
430
+ if (output.format !== void 0) {
431
+ if (!["json", "csv", "html", "all"].includes(output.format)) {
432
+ errors.push("audit.output.format must be one of: json, csv, html, all");
433
+ }
434
+ }
435
+ if (output.out !== void 0 && typeof output.out !== "string") {
436
+ errors.push("audit.output.out must be a string");
437
+ }
438
+ }
439
+ }
440
+ }
441
+ }
442
+ if (cfg.test !== void 0) {
443
+ if (typeof cfg.test !== "object" || cfg.test === null) {
444
+ errors.push("test must be an object");
445
+ } else {
446
+ if (cfg.test.components !== void 0) {
447
+ if (!Array.isArray(cfg.test.components)) {
448
+ errors.push("test.components must be an array");
449
+ } else {
450
+ cfg.test.components.forEach((comp, idx) => {
451
+ if (typeof comp !== "object" || comp === null) {
452
+ errors.push(`test.components[${idx}] must be an object`);
453
+ } else {
454
+ if (typeof comp.name !== "string") {
455
+ errors.push(`test.components[${idx}].name must be a string`);
456
+ }
457
+ if (comp.path !== void 0 && typeof comp.path !== "string") {
458
+ errors.push(`test.components[${idx}].path must be a string when provided`);
459
+ }
460
+ if (comp.strictness !== void 0 && !["minimal", "balanced", "strict", "paranoid"].includes(comp.strictness)) {
461
+ errors.push(`test.components[${idx}].strictness must be one of: minimal, balanced, strict, paranoid`);
462
+ }
463
+ }
464
+ });
465
+ }
466
+ }
467
+ if (cfg.test.strictness !== void 0) {
468
+ if (!["minimal", "balanced", "strict", "paranoid"].includes(cfg.test.strictness)) {
469
+ errors.push("test.strictness must be one of: minimal, balanced, strict, paranoid");
470
+ }
471
+ }
472
+ }
473
+ }
474
+ return { valid: errors.length === 0, errors };
475
+ }
476
+ async function loadConfigFile(filePath) {
477
+ try {
478
+ const ext = import_path.default.extname(filePath);
479
+ if (ext === ".json") {
480
+ const content = await import_fs_extra.default.readFile(filePath, "utf-8");
481
+ return JSON.parse(content);
482
+ } else if ([".js", ".mjs", ".cjs", ".ts"].includes(ext)) {
483
+ const imported = await import(filePath);
484
+ return imported.default || imported;
485
+ }
486
+ return null;
487
+ } catch {
488
+ return null;
489
+ }
490
+ }
491
+ async function loadConfig(cwd = process.cwd()) {
492
+ const configNames = [
493
+ "ariaease.config.js",
494
+ "ariaease.config.mjs",
495
+ "ariaease.config.cjs",
496
+ "ariaease.config.json",
497
+ "ariaease.config.ts"
498
+ ];
499
+ let loadedConfig = null;
500
+ let foundPath = null;
501
+ const errors = [];
502
+ for (const name of configNames) {
503
+ const configPath = import_path.default.resolve(cwd, name);
504
+ if (await import_fs_extra.default.pathExists(configPath)) {
505
+ foundPath = configPath;
506
+ loadedConfig = await loadConfigFile(configPath);
507
+ if (loadedConfig === null) {
508
+ errors.push(`Found config at ${name} but failed to load it. Check for syntax errors.`);
509
+ continue;
510
+ }
511
+ const validation = validateConfig(loadedConfig);
512
+ if (!validation.valid) {
513
+ errors.push(`Config validation failed in ${name}:`);
514
+ errors.push(...validation.errors.map((err) => ` - ${err}`));
515
+ loadedConfig = null;
516
+ continue;
517
+ }
518
+ break;
519
+ }
520
+ }
521
+ return {
522
+ config: loadedConfig || {},
523
+ configPath: loadedConfig ? foundPath : null,
524
+ errors
525
+ };
526
+ }
527
+ var import_path, import_fs_extra;
528
+ var init_configLoader = __esm({
529
+ "src/utils/cli/configLoader.ts"() {
530
+ "use strict";
531
+ import_path = __toESM(require("path"), 1);
532
+ import_fs_extra = __toESM(require("fs-extra"), 1);
533
+ }
534
+ });
535
+
347
536
  // node_modules/@playwright/test/index.mjs
348
537
  var test_exports = {};
349
538
  __export(test_exports, {
@@ -679,9 +868,6 @@ var init_ActionExecutor = __esm({
679
868
  this.selectors = selectors;
680
869
  this.timeoutMs = timeoutMs;
681
870
  }
682
- isOptionalMenuTarget(target) {
683
- return ["submenu", "submenuTrigger", "submenuItems"].includes(target);
684
- }
685
871
  /**
686
872
  * Check if error is due to browser/page being closed
687
873
  */
@@ -813,10 +999,9 @@ var init_ActionExecutor = __esm({
813
999
  const locator = this.page.locator(selector).first();
814
1000
  const elementCount = await locator.count();
815
1001
  if (elementCount === 0) {
816
- const optionalMenuTarget = this.isOptionalMenuTarget(target);
817
1002
  return {
818
1003
  success: false,
819
- error: optionalMenuTarget ? `${target} element not found (optional submenu test)` : `${target} element not found.`,
1004
+ error: `${target} element not found.`,
820
1005
  shouldBreak: true
821
1006
  // Signal to skip this test
822
1007
  };
@@ -1130,10 +1315,11 @@ var contractTestRunnerPlaywright_exports = {};
1130
1315
  __export(contractTestRunnerPlaywright_exports, {
1131
1316
  runContractTestsPlaywright: () => runContractTestsPlaywright
1132
1317
  });
1133
- async function runContractTestsPlaywright(componentName, url) {
1318
+ async function runContractTestsPlaywright(componentName, url, strictness) {
1134
1319
  const reporter = new ContractReporter(true);
1135
1320
  const actionTimeoutMs = 400;
1136
1321
  const assertionTimeoutMs = 400;
1322
+ const strictnessMode = normalizeStrictness(strictness);
1137
1323
  const contractTyped = contract_default;
1138
1324
  const contractPath = contractTyped[componentName]?.path;
1139
1325
  const resolvedPath = new URL(contractPath, import_meta3.url).pathname;
@@ -1142,9 +1328,25 @@ async function runContractTestsPlaywright(componentName, url) {
1142
1328
  const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
1143
1329
  const apgUrl = componentContract.meta?.source?.apg;
1144
1330
  const failures = [];
1331
+ const warnings = [];
1145
1332
  const passes = [];
1146
1333
  const skipped = [];
1147
1334
  let page = null;
1335
+ const classifyFailure = (message, levelRaw) => {
1336
+ const level = normalizeLevel(levelRaw);
1337
+ const enforcement = resolveEnforcement(level, strictnessMode);
1338
+ if (enforcement === "error") {
1339
+ failures.push(message);
1340
+ return { status: "fail", level, detail: message };
1341
+ }
1342
+ if (enforcement === "warning") {
1343
+ warnings.push(message);
1344
+ return { status: "warn", level, detail: message };
1345
+ }
1346
+ const ignoredMessage = `${message} (ignored by strictness=${strictnessMode}, level=${level})`;
1347
+ skipped.push(ignoredMessage);
1348
+ return { status: "skip", level, detail: ignoredMessage };
1349
+ };
1148
1350
  try {
1149
1351
  page = await createTestPage();
1150
1352
  if (url) {
@@ -1190,35 +1392,37 @@ This usually means:
1190
1392
  });
1191
1393
  }
1192
1394
  const hasSubmenuCapability = componentName === "menu" && !!componentContract.selectors.submenuTrigger ? await page.locator(componentContract.selectors.submenuTrigger).count() > 0 : false;
1395
+ let staticPassed = 0;
1193
1396
  let staticFailed = 0;
1397
+ let staticWarnings = 0;
1194
1398
  const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1195
1399
  for (const test of componentContract.static[0]?.assertions || []) {
1196
1400
  if (test.target === "relative") continue;
1197
1401
  const staticDescription = `${test.target}${test.attribute ? ` (${test.attribute})` : ""}`;
1402
+ const staticLevel = normalizeLevel(test.level);
1198
1403
  if (componentName === "menu" && test.target === "submenuTrigger" && !hasSubmenuCapability) {
1199
- passes.push(`Skipping submenu static assertion for ${test.target}: no submenu capability detected in rendered component.`);
1200
- reporter.reportStaticTest(staticDescription, true);
1404
+ const skipMessage = `Skipping submenu static assertion for ${test.target}: no submenu capability detected in rendered component.`;
1405
+ skipped.push(skipMessage);
1406
+ reporter.reportStaticTest(staticDescription, "skip", skipMessage, staticLevel);
1201
1407
  continue;
1202
1408
  }
1203
1409
  const targetSelector = componentContract.selectors[test.target];
1204
1410
  if (!targetSelector) {
1205
1411
  const failure = `Selector for target ${test.target} not found.`;
1206
- failures.push(failure);
1207
- staticFailed += 1;
1208
- reporter.reportStaticTest(staticDescription, false, failure);
1412
+ const outcome = classifyFailure(failure, test.level);
1413
+ if (outcome.status === "fail") staticFailed += 1;
1414
+ if (outcome.status === "warn") staticWarnings += 1;
1415
+ reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
1209
1416
  continue;
1210
1417
  }
1211
1418
  const target = page.locator(targetSelector).first();
1212
1419
  const exists = await target.count() > 0;
1213
1420
  if (!exists) {
1214
- if (test.isOptional === true) {
1215
- reporter.reportStaticTest(staticDescription, true);
1216
- continue;
1217
- }
1218
1421
  const failure = `Target ${test.target} not found.`;
1219
- failures.push(failure);
1220
- staticFailed += 1;
1221
- reporter.reportStaticTest(staticDescription, false, failure);
1422
+ const outcome = classifyFailure(failure, test.level);
1423
+ if (outcome.status === "fail") staticFailed += 1;
1424
+ if (outcome.status === "warn") staticWarnings += 1;
1425
+ reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
1222
1426
  continue;
1223
1427
  }
1224
1428
  const isRedundantCheck = (selector, attrName, expectedVal) => {
@@ -1253,19 +1457,23 @@ This usually means:
1253
1457
  }
1254
1458
  if (!hasAny && !allRedundant) {
1255
1459
  const failure = test.failureMessage + ` None of the attributes "${test.attribute}" are present.`;
1256
- failures.push(failure);
1257
- staticFailed += 1;
1258
- reporter.reportStaticTest(staticDescription, false, failure);
1460
+ const outcome = classifyFailure(failure, test.level);
1461
+ if (outcome.status === "fail") staticFailed += 1;
1462
+ if (outcome.status === "warn") staticWarnings += 1;
1463
+ reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
1259
1464
  } else if (!allRedundant && hasAny) {
1260
1465
  passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
1261
- reporter.reportStaticTest(staticDescription, true);
1466
+ staticPassed += 1;
1467
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1262
1468
  } else {
1263
- reporter.reportStaticTest(staticDescription, true);
1469
+ staticPassed += 1;
1470
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1264
1471
  }
1265
1472
  } else {
1266
1473
  if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
1267
1474
  passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
1268
- reporter.reportStaticTest(staticDescription, true);
1475
+ staticPassed += 1;
1476
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1269
1477
  } else {
1270
1478
  const result = await staticAssertionRunner.validateAttribute(
1271
1479
  target,
@@ -1277,11 +1485,13 @@ This usually means:
1277
1485
  );
1278
1486
  if (result.success && result.passMessage) {
1279
1487
  passes.push(result.passMessage);
1280
- reporter.reportStaticTest(staticDescription, true);
1488
+ staticPassed += 1;
1489
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1281
1490
  } else if (!result.success && result.failMessage) {
1282
- failures.push(result.failMessage);
1283
- staticFailed += 1;
1284
- reporter.reportStaticTest(staticDescription, false, result.failMessage);
1491
+ const outcome = classifyFailure(result.failMessage, test.level);
1492
+ if (outcome.status === "fail") staticFailed += 1;
1493
+ if (outcome.status === "warn") staticWarnings += 1;
1494
+ reporter.reportStaticTest(staticDescription, outcome.status, outcome.detail, outcome.level);
1285
1495
  }
1286
1496
  }
1287
1497
  }
@@ -1296,6 +1506,9 @@ This usually means:
1296
1506
  }
1297
1507
  const { action, assertions } = dynamicTest;
1298
1508
  const failuresBeforeTest = failures.length;
1509
+ const warningsBeforeTest = warnings.length;
1510
+ const skippedBeforeTest = skipped.length;
1511
+ const dynamicLevel = normalizeLevel(dynamicTest.level);
1299
1512
  try {
1300
1513
  await strategy.resetState(page);
1301
1514
  } catch (error) {
@@ -1305,13 +1518,15 @@ This usually means:
1305
1518
  }
1306
1519
  const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
1307
1520
  if (shouldSkipTest) {
1308
- reporter.reportTest(dynamicTest, "skip", `Skipping test - component-specific conditions not met`);
1521
+ const skipMessage = `Skipping test - component-specific conditions not met`;
1522
+ skipped.push(skipMessage);
1523
+ reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "skip", skipMessage);
1309
1524
  continue;
1310
1525
  }
1311
1526
  const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
1312
1527
  const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1313
- let shouldSkipCurrentTest = false;
1314
1528
  let shouldAbortCurrentTest = false;
1529
+ let actionOutcome = null;
1315
1530
  for (const act of action) {
1316
1531
  if (!page || page.isClosed()) {
1317
1532
  failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
@@ -1333,27 +1548,20 @@ This usually means:
1333
1548
  continue;
1334
1549
  }
1335
1550
  if (!result.success) {
1336
- if (result.shouldBreak) {
1337
- if (result.error?.includes("optional submenu test")) {
1338
- reporter.reportTest(dynamicTest, "skip", result.error);
1339
- shouldSkipCurrentTest = true;
1340
- } else if (result.error) {
1341
- failures.push(result.error);
1342
- shouldAbortCurrentTest = true;
1343
- }
1344
- break;
1345
- }
1346
1551
  if (result.error) {
1347
- failures.push(result.error);
1552
+ const outcome = classifyFailure(result.error, dynamicTest.level);
1553
+ actionOutcome = { status: outcome.status, detail: outcome.detail };
1348
1554
  }
1349
- continue;
1555
+ shouldAbortCurrentTest = true;
1556
+ break;
1350
1557
  }
1351
1558
  }
1352
- if (shouldSkipCurrentTest) {
1353
- continue;
1354
- }
1355
1559
  if (shouldAbortCurrentTest) {
1356
- reporter.reportTest(dynamicTest, "fail", failures[failures.length - 1]);
1560
+ reporter.reportTest(
1561
+ { description: dynamicTest.description, level: dynamicLevel },
1562
+ actionOutcome?.status || "fail",
1563
+ actionOutcome?.detail || failures[failures.length - 1]
1564
+ );
1357
1565
  continue;
1358
1566
  }
1359
1567
  for (const assertion of assertions) {
@@ -1361,22 +1569,39 @@ This usually means:
1361
1569
  if (result.success && result.passMessage) {
1362
1570
  passes.push(result.passMessage);
1363
1571
  } else if (!result.success && result.failMessage) {
1364
- failures.push(result.failMessage);
1572
+ const assertionLevel = normalizeLevel(assertion.level || dynamicTest.level);
1573
+ const outcome = classifyFailure(result.failMessage, assertionLevel);
1574
+ if (outcome.status === "skip") {
1575
+ continue;
1576
+ }
1365
1577
  }
1366
1578
  }
1367
1579
  const failuresAfterTest = failures.length;
1368
- const testPassed = failuresAfterTest === failuresBeforeTest;
1369
- const failureMessage = testPassed ? void 0 : failures[failures.length - 1];
1370
- if (dynamicTest.isOptional === true && !testPassed) {
1371
- failures.pop();
1372
- reporter.reportTest(dynamicTest, "optional-fail", failureMessage);
1580
+ const warningsAfterTest = warnings.length;
1581
+ const skippedAfterTest = skipped.length;
1582
+ if (failuresAfterTest > failuresBeforeTest) {
1583
+ reporter.reportTest(
1584
+ { description: dynamicTest.description, level: dynamicLevel },
1585
+ "fail",
1586
+ failures[failures.length - 1]
1587
+ );
1588
+ } else if (warningsAfterTest > warningsBeforeTest) {
1589
+ reporter.reportTest(
1590
+ { description: dynamicTest.description, level: dynamicLevel },
1591
+ "warn",
1592
+ warnings[warnings.length - 1]
1593
+ );
1594
+ } else if (skippedAfterTest > skippedBeforeTest) {
1595
+ reporter.reportTest(
1596
+ { description: dynamicTest.description, level: dynamicLevel },
1597
+ "skip",
1598
+ skipped[skipped.length - 1]
1599
+ );
1373
1600
  } else {
1374
- reporter.reportTest(dynamicTest, testPassed ? "pass" : "fail", failureMessage);
1601
+ reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "pass");
1375
1602
  }
1376
1603
  }
1377
- const staticTotal = componentContract.static[0].assertions.length;
1378
- const staticPassed = Math.max(0, staticTotal - staticFailed);
1379
- reporter.reportStatic(staticPassed, staticFailed);
1604
+ reporter.reportStatic(staticPassed, staticFailed, staticWarnings);
1380
1605
  reporter.summary(failures);
1381
1606
  } catch (error) {
1382
1607
  if (error instanceof Error) {
@@ -1401,7 +1626,7 @@ Make sure your dev server is running at ${url}`);
1401
1626
  } finally {
1402
1627
  if (page) await page.close();
1403
1628
  }
1404
- return { passes, failures, skipped };
1629
+ return { passes, failures, skipped, warnings };
1405
1630
  }
1406
1631
  var import_fs2, import_meta3;
1407
1632
  var init_contractTestRunnerPlaywright = __esm({
@@ -1414,6 +1639,7 @@ var init_contractTestRunnerPlaywright = __esm({
1414
1639
  init_ContractReporter();
1415
1640
  init_ActionExecutor();
1416
1641
  init_AssertionRunner();
1642
+ init_strictness();
1417
1643
  import_meta3 = {};
1418
1644
  }
1419
1645
  });
@@ -1439,13 +1665,13 @@ function displayBadgeInfo(badgeType) {
1439
1665
  console.log(import_chalk.default.dim("\n This helps others discover accessibility tools and shows you care!\n"));
1440
1666
  }
1441
1667
  async function promptAddBadge(badgeType, cwd = process.cwd()) {
1442
- const readmePath = import_path.default.join(cwd, "README.md");
1443
- const readmeExists = await import_fs_extra.default.pathExists(readmePath);
1668
+ const readmePath = import_path2.default.join(cwd, "README.md");
1669
+ const readmeExists = await import_fs_extra2.default.pathExists(readmePath);
1444
1670
  if (!readmeExists) {
1445
1671
  console.log(import_chalk.default.yellow(" \u2139\uFE0F No README.md found in current directory"));
1446
1672
  return;
1447
1673
  }
1448
- const readmeContent = await import_fs_extra.default.readFile(readmePath, "utf-8");
1674
+ const readmeContent = await import_fs_extra2.default.readFile(readmePath, "utf-8");
1449
1675
  const markdown = getBadgeMarkdown(badgeType);
1450
1676
  if (readmeContent.includes(markdown) || readmeContent.includes(BADGE_CONFIGS[badgeType].fileName)) {
1451
1677
  console.log(import_chalk.default.gray(" \u2713 Badge already in README.md"));
@@ -1489,7 +1715,7 @@ async function addBadgeToReadme(readmePath, content, badge) {
1489
1715
  insertIndex = 1;
1490
1716
  }
1491
1717
  lines.splice(insertIndex, 0, badge);
1492
- await import_fs_extra.default.writeFile(readmePath, lines.join("\n"), "utf-8");
1718
+ await import_fs_extra2.default.writeFile(readmePath, lines.join("\n"), "utf-8");
1493
1719
  }
1494
1720
  function displayAllBadges() {
1495
1721
  console.log(import_chalk.default.cyan("\n\u{1F4CD} Available badges:"));
@@ -1501,12 +1727,12 @@ function displayAllBadges() {
1501
1727
  console.log(import_chalk.default.green(" " + getBadgeMarkdown("verified")));
1502
1728
  console.log("");
1503
1729
  }
1504
- var import_fs_extra, import_path, import_chalk, import_readline, BADGE_CONFIGS;
1730
+ var import_fs_extra2, import_path2, import_chalk, import_readline, BADGE_CONFIGS;
1505
1731
  var init_badgeHelper = __esm({
1506
1732
  "src/utils/cli/badgeHelper.ts"() {
1507
1733
  "use strict";
1508
- import_fs_extra = __toESM(require("fs-extra"), 1);
1509
- import_path = __toESM(require("path"), 1);
1734
+ import_fs_extra2 = __toESM(require("fs-extra"), 1);
1735
+ import_path2 = __toESM(require("path"), 1);
1510
1736
  import_chalk = __toESM(require("chalk"), 1);
1511
1737
  import_readline = __toESM(require("readline"), 1);
1512
1738
  BADGE_CONFIGS = {
@@ -2062,6 +2288,20 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
2062
2288
  function hasSubmenu(menuItem) {
2063
2289
  return menuItem.hasAttribute("aria-controls") && menuItem.hasAttribute("aria-haspopup") && menuItem.getAttribute("role") === "menuitem";
2064
2290
  }
2291
+ function closeAncestorMenusFromTrigger(triggerEl) {
2292
+ let currentTrigger = triggerEl;
2293
+ while (currentTrigger && currentTrigger.getAttribute("role") === "menuitem") {
2294
+ const parentMenu = currentTrigger.closest('[role="menu"]');
2295
+ if (!parentMenu) break;
2296
+ parentMenu.style.display = "none";
2297
+ currentTrigger.setAttribute("aria-expanded", "false");
2298
+ const parentTriggerId = parentMenu.getAttribute("aria-labelledby");
2299
+ if (!parentTriggerId) break;
2300
+ const nextTrigger = document.getElementById(parentTriggerId);
2301
+ if (!nextTrigger) break;
2302
+ currentTrigger = nextTrigger;
2303
+ }
2304
+ }
2065
2305
  intializeMenuItems();
2066
2306
  function handleItemsKeydown(event, menuItem, menuItemIndex) {
2067
2307
  switch (event.key) {
@@ -2132,11 +2372,10 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
2132
2372
  break;
2133
2373
  }
2134
2374
  case "Tab": {
2135
- if (!event.shiftKey || event.shiftKey) {
2136
- closeMenu();
2137
- if (onOpenChange) {
2138
- onOpenChange(false);
2139
- }
2375
+ closeMenu();
2376
+ closeAncestorMenusFromTrigger(triggerButton);
2377
+ if (onOpenChange) {
2378
+ onOpenChange(false);
2140
2379
  }
2141
2380
  break;
2142
2381
  }
@@ -3028,9 +3267,11 @@ var import_jest_axe = require("jest-axe");
3028
3267
  init_contract();
3029
3268
  var import_promises = __toESM(require("fs/promises"), 1);
3030
3269
  init_ContractReporter();
3270
+ init_strictness();
3031
3271
  var import_meta = {};
3032
- async function runContractTests(componentName, component) {
3272
+ async function runContractTests(componentName, component, strictness) {
3033
3273
  const reporter = new ContractReporter(false);
3274
+ const strictnessMode = normalizeStrictness(strictness);
3034
3275
  const contractTyped = contract_default;
3035
3276
  const contractPath = contractTyped[componentName]?.path;
3036
3277
  if (!contractPath) {
@@ -3044,19 +3285,42 @@ async function runContractTests(componentName, component) {
3044
3285
  const failures = [];
3045
3286
  const passes = [];
3046
3287
  const skipped = [];
3047
- const failuresBeforeStatic = failures.length;
3288
+ const warnings = [];
3289
+ const classifyFailure = (message, levelRaw) => {
3290
+ const level = normalizeLevel(levelRaw);
3291
+ const enforcement = resolveEnforcement(level, strictnessMode);
3292
+ if (enforcement === "error") {
3293
+ failures.push(message);
3294
+ return { status: "fail", level, detail: message };
3295
+ }
3296
+ if (enforcement === "warning") {
3297
+ warnings.push(message);
3298
+ return { status: "warn", level, detail: message };
3299
+ }
3300
+ const ignoredMessage = `${message} (ignored by strictness=${strictnessMode}, level=${level})`;
3301
+ skipped.push(ignoredMessage);
3302
+ return { status: "skip", level, detail: ignoredMessage };
3303
+ };
3304
+ let staticPassed = 0;
3305
+ let staticFailed = 0;
3306
+ let staticWarnings = 0;
3048
3307
  for (const test of componentContract.static[0].assertions) {
3049
3308
  if (test.target !== "relative") {
3309
+ const staticLevel = normalizeLevel(test.level);
3050
3310
  const selector = componentContract.selectors[test.target];
3051
3311
  if (!selector) {
3052
- failures.push(`Selector for target ${test.target} not found.`);
3053
- reporter.reportStaticTest(`${test.target} has required ARIA attributes`, false, `Selector for target ${test.target} not found.`);
3312
+ const outcome = classifyFailure(`Selector for target ${test.target} not found.`, test.level);
3313
+ if (outcome.status === "fail") staticFailed += 1;
3314
+ if (outcome.status === "warn") staticWarnings += 1;
3315
+ reporter.reportStaticTest(`${test.target} has required ARIA attributes`, outcome.status, outcome.detail, outcome.level);
3054
3316
  continue;
3055
3317
  }
3056
3318
  const target = component.querySelector(selector);
3057
3319
  if (!target) {
3058
- failures.push(`Target ${test.target} not found.`);
3059
- reporter.reportStaticTest(`${test.target} has required ARIA attributes`, false, `Target ${test.target} not found.`);
3320
+ const outcome = classifyFailure(`Target ${test.target} not found.`, test.level);
3321
+ if (outcome.status === "fail") staticFailed += 1;
3322
+ if (outcome.status === "warn") staticWarnings += 1;
3323
+ reporter.reportStaticTest(`${test.target} has required ARIA attributes`, outcome.status, outcome.detail, outcome.level);
3060
3324
  continue;
3061
3325
  }
3062
3326
  const attributeValue = target.getAttribute(test.attribute);
@@ -3064,36 +3328,40 @@ async function runContractTests(componentName, component) {
3064
3328
  const attributes = test.attribute.split(" | ");
3065
3329
  const hasAnyAttribute = attributes.some((attr) => target.hasAttribute(attr));
3066
3330
  if (!hasAnyAttribute) {
3067
- failures.push(test.failureMessage + ` None of the attributes "${test.attribute}" are present.`);
3068
- reporter.reportStaticTest(`${test.target} has ${test.attribute}`, false, test.failureMessage);
3331
+ const outcome = classifyFailure(test.failureMessage + ` None of the attributes "${test.attribute}" are present.`, test.level);
3332
+ if (outcome.status === "fail") staticFailed += 1;
3333
+ if (outcome.status === "warn") staticWarnings += 1;
3334
+ reporter.reportStaticTest(`${test.target} has ${test.attribute}`, outcome.status, outcome.detail, outcome.level);
3069
3335
  } else {
3070
3336
  passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
3071
- reporter.reportStaticTest(`${test.target} has ${test.attribute}`, true);
3337
+ staticPassed += 1;
3338
+ reporter.reportStaticTest(`${test.target} has ${test.attribute}`, "pass", void 0, staticLevel);
3072
3339
  }
3073
3340
  } else if (!attributeValue || !test.expectedValue.split(" | ").includes(attributeValue)) {
3074
- failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
3075
- reporter.reportStaticTest(`${test.target} has ${test.attribute}="${test.expectedValue}"`, false, test.failureMessage);
3341
+ const outcome = classifyFailure(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`, test.level);
3342
+ if (outcome.status === "fail") staticFailed += 1;
3343
+ if (outcome.status === "warn") staticWarnings += 1;
3344
+ reporter.reportStaticTest(`${test.target} has ${test.attribute}="${test.expectedValue}"`, outcome.status, outcome.detail, outcome.level);
3076
3345
  } else {
3077
3346
  passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
3078
- reporter.reportStaticTest(`${test.target} has ${test.attribute}="${attributeValue}"`, true);
3347
+ staticPassed += 1;
3348
+ reporter.reportStaticTest(`${test.target} has ${test.attribute}="${attributeValue}"`, "pass", void 0, staticLevel);
3079
3349
  }
3080
3350
  }
3081
3351
  }
3082
3352
  for (const dynamicTest of componentContract.dynamic) {
3083
3353
  skipped.push(dynamicTest.description);
3084
- reporter.reportTest(dynamicTest, "skip");
3354
+ reporter.reportTest({ description: dynamicTest.description, level: dynamicTest.level }, "skip");
3085
3355
  }
3086
- const staticTotal = componentContract.static[0].assertions.length;
3087
- const staticFailed = failures.length - failuresBeforeStatic;
3088
- const staticPassed = Math.max(0, staticTotal - staticFailed);
3089
- reporter.reportStatic(staticPassed, staticFailed);
3356
+ reporter.reportStatic(staticPassed, staticFailed, staticWarnings);
3090
3357
  reporter.summary(failures);
3091
- return { passes, failures, skipped };
3358
+ return { passes, failures, skipped, warnings };
3092
3359
  }
3093
3360
 
3094
3361
  // src/utils/test/src/test.ts
3095
3362
  init_playwrightTestHarness();
3096
- async function testUiComponent(componentName, component, url) {
3363
+ init_strictness();
3364
+ async function testUiComponent(componentName, component, url, options = {}) {
3097
3365
  if (!componentName || typeof componentName !== "string") {
3098
3366
  throw new Error("\u274C testUiComponent requires a valid componentName (string)");
3099
3367
  }
@@ -3130,6 +3398,17 @@ Error: ${error instanceof Error ? error.message : String(error)}`
3130
3398
  }
3131
3399
  return null;
3132
3400
  }
3401
+ let strictness = normalizeStrictness(options.strictness);
3402
+ if (options.strictness === void 0 && typeof window === "undefined") {
3403
+ try {
3404
+ const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_configLoader(), configLoader_exports));
3405
+ const { config } = await loadConfig2(process.cwd());
3406
+ const componentStrictness = config.test?.components?.find((comp) => comp?.name === componentName)?.strictness;
3407
+ strictness = normalizeStrictness(componentStrictness ?? config.test?.strictness);
3408
+ } catch {
3409
+ strictness = "balanced";
3410
+ }
3411
+ }
3133
3412
  let contract;
3134
3413
  try {
3135
3414
  if (url) {
@@ -3137,7 +3416,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
3137
3416
  if (devServerUrl) {
3138
3417
  console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
3139
3418
  const { runContractTestsPlaywright: runContractTestsPlaywright2 } = await Promise.resolve().then(() => (init_contractTestRunnerPlaywright(), contractTestRunnerPlaywright_exports));
3140
- contract = await runContractTestsPlaywright2(componentName, devServerUrl);
3419
+ contract = await runContractTestsPlaywright2(componentName, devServerUrl, strictness);
3141
3420
  } else {
3142
3421
  throw new Error(
3143
3422
  `\u274C Dev server not running at ${url}
@@ -3146,7 +3425,7 @@ Please start your dev server and try again.`
3146
3425
  }
3147
3426
  } else if (component) {
3148
3427
  console.log(`\u{1F3AD} Running component contract tests in JSDOM mode`);
3149
- contract = await runContractTests(componentName, component);
3428
+ contract = await runContractTests(componentName, component, strictness);
3150
3429
  } else {
3151
3430
  throw new Error("\u274C Either component or URL must be provided");
3152
3431
  }