aria-ease 6.5.1 → 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 +88 -24
  2. package/bin/{chunk-AUJAN4RK.js → chunk-2TOYEY5L.js} +87 -40
  3. package/bin/chunk-VPBHLMAS.js +127 -0
  4. package/bin/cli.cjs +403 -237
  5. package/bin/cli.js +8 -123
  6. package/bin/configLoader-XRF6VM4J.js +7 -0
  7. package/{dist/contractTestRunnerPlaywright-7F756CFB.js → bin/contractTestRunnerPlaywright-UAOFNS7Z.js} +121 -60
  8. package/bin/{test-C3CMRHSI.js → test-WRIJHN6H.js} +65 -24
  9. package/dist/{chunk-AUJAN4RK.js → chunk-2TOYEY5L.js} +87 -40
  10. package/dist/configLoader-IT4PWCJB.js +128 -0
  11. package/{bin/contractTestRunnerPlaywright-7F756CFB.js → dist/contractTestRunnerPlaywright-UAOFNS7Z.js} +121 -60
  12. package/dist/index.cjs +471 -137
  13. package/dist/index.d.cts +6 -1
  14. package/dist/index.d.ts +6 -1
  15. package/dist/index.js +127 -35
  16. package/dist/src/menu/index.cjs +62 -11
  17. package/dist/src/menu/index.js +62 -11
  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 +172 -34
  21. package/dist/src/utils/test/aria-contracts/tabs/tabs.contract.json +10 -10
  22. package/dist/src/utils/test/{chunk-AUJAN4RK.js → chunk-2TOYEY5L.js} +85 -41
  23. package/dist/src/utils/test/configLoader-LD4RV2WQ.js +126 -0
  24. package/dist/src/utils/test/{contractTestRunnerPlaywright-HL73FADJ.js → contractTestRunnerPlaywright-IRJOAEMT.js} +117 -59
  25. package/dist/src/utils/test/index.cjs +403 -125
  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) {
147
152
  this.log(` \u21B3 ${failureMessage}`);
148
153
  }
149
- if (status === "optional-fail") {
150
- this.log(` \u21B3 Not implemented (recommended for enhanced UX)`);
154
+ if (status === "warn" && failureMessage) {
155
+ this.log(` \u21B3 ${failureMessage}`);
156
+ }
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,46 +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
- const staticIcon = this.staticFailures === 0 ? "\u2705" : "\u274C";
241
- const staticStatus = this.staticFailures === 0 ? "PASS" : "FAIL";
242
- this.log(`${staticIcon} Static ARIA Tests: ${staticStatus}`);
243
- this.log(` ${this.staticPasses}/${this.staticPasses + this.staticFailures} required attributes present`);
244
- this.log("");
245
- if (totalFailures === 0 && this.skipped === 0 && this.optionalSuggestions === 0) {
249
+ if (totalFailures === 0 && this.skipped === 0 && this.warnings === 0) {
246
250
  this.log(`\u2705 All ${totalRun} tests passed!`);
247
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`);
248
252
  } else if (totalFailures === 0) {
249
- this.log(`\u2705 ${totalPasses}/${totalRun} required tests passed`);
253
+ this.log(`\u2705 ${totalPasses}/${totalRun} tests passed`);
250
254
  if (this.skipped > 0) {
251
255
  this.log(`\u25CB ${this.skipped} tests skipped`);
252
256
  }
253
- if (this.optionalSuggestions > 0) {
254
- 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" : ""}`);
255
259
  }
256
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`);
257
261
  } else {
258
262
  this.log(`\u274C ${totalFailures} test${totalFailures > 1 ? "s" : ""} failed`);
259
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
+ }
260
267
  if (this.skipped > 0) {
261
268
  this.log(`\u25CB ${this.skipped} test${this.skipped > 1 ? "s" : ""} skipped`);
262
269
  }
263
- if (this.optionalSuggestions > 0) {
264
- this.log(`\u{1F4A1} ${this.optionalSuggestions} optional enhancement${this.optionalSuggestions > 1 ? "s" : ""} suggested`);
265
- }
266
270
  }
267
271
  this.log(`\u23F1\uFE0F Duration: ${duration}ms`);
268
272
  this.log(`${"\u2550".repeat(60)}
@@ -299,6 +303,52 @@ ${"\u2550".repeat(60)}`);
299
303
  }
300
304
  });
301
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
+
302
352
  // src/utils/test/src/playwrightTestHarness.ts
303
353
  async function getOrCreateBrowser() {
304
354
  if (!sharedBrowser) {
@@ -349,6 +399,140 @@ var init_playwrightTestHarness = __esm({
349
399
  }
350
400
  });
351
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
+
352
536
  // node_modules/@playwright/test/index.mjs
353
537
  var test_exports = {};
354
538
  __export(test_exports, {
@@ -528,29 +712,20 @@ This indicates a problem with the menu component's close functionality.`
528
712
  }
529
713
  }
530
714
  async shouldSkipTest(test, page) {
531
- for (const act of test.action) {
532
- if (act.type === "keypress" && (act.target === "submenuTrigger" || act.target === "submenu")) {
533
- const submenuSelector = this.selectors[act.target];
534
- if (submenuSelector) {
535
- const submenuCount = await page.locator(submenuSelector).count();
536
- if (submenuCount === 0) {
537
- return true;
538
- }
539
- }
540
- }
715
+ const requiresSubmenu = test.action.some(
716
+ (act) => act.target === "submenu" || act.target === "submenuTrigger" || act.target === "submenuItems"
717
+ ) || test.assertions.some(
718
+ (assertion) => assertion.target === "submenu" || assertion.target === "submenuTrigger" || assertion.target === "submenuItems"
719
+ );
720
+ if (!requiresSubmenu) {
721
+ return false;
541
722
  }
542
- for (const assertion of test.assertions) {
543
- if (assertion.target === "submenu" || assertion.target === "submenuTrigger") {
544
- const submenuSelector = this.selectors[assertion.target];
545
- if (submenuSelector) {
546
- const submenuCount = await page.locator(submenuSelector).count();
547
- if (submenuCount === 0) {
548
- return true;
549
- }
550
- }
551
- }
723
+ const submenuTriggerSelector = this.selectors.submenuTrigger;
724
+ if (!submenuTriggerSelector) {
725
+ return true;
552
726
  }
553
- return false;
727
+ const submenuTriggerCount = await page.locator(submenuTriggerSelector).count();
728
+ return submenuTriggerCount === 0;
554
729
  }
555
730
  getMainSelector() {
556
731
  return this.mainSelector;
@@ -813,7 +988,7 @@ var init_ActionExecutor = __esm({
813
988
  } else if (keyValue.includes(" ")) {
814
989
  keyValue = keyValue.replace(/ /g, "");
815
990
  }
816
- if (target === "focusable" && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape"].includes(keyValue)) {
991
+ if (target === "focusable" && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape", "Home", "End", "Tab", "Shift+Tab"].includes(keyValue)) {
817
992
  await this.page.keyboard.press(keyValue);
818
993
  return { success: true };
819
994
  }
@@ -826,7 +1001,7 @@ var init_ActionExecutor = __esm({
826
1001
  if (elementCount === 0) {
827
1002
  return {
828
1003
  success: false,
829
- error: `${target} element not found (optional submenu test)`,
1004
+ error: `${target} element not found.`,
830
1005
  shouldBreak: true
831
1006
  // Signal to skip this test
832
1007
  };
@@ -1140,10 +1315,11 @@ var contractTestRunnerPlaywright_exports = {};
1140
1315
  __export(contractTestRunnerPlaywright_exports, {
1141
1316
  runContractTestsPlaywright: () => runContractTestsPlaywright
1142
1317
  });
1143
- async function runContractTestsPlaywright(componentName, url) {
1318
+ async function runContractTestsPlaywright(componentName, url, strictness) {
1144
1319
  const reporter = new ContractReporter(true);
1145
1320
  const actionTimeoutMs = 400;
1146
1321
  const assertionTimeoutMs = 400;
1322
+ const strictnessMode = normalizeStrictness(strictness);
1147
1323
  const contractTyped = contract_default;
1148
1324
  const contractPath = contractTyped[componentName]?.path;
1149
1325
  const resolvedPath = new URL(contractPath, import_meta3.url).pathname;
@@ -1152,9 +1328,25 @@ async function runContractTestsPlaywright(componentName, url) {
1152
1328
  const totalTests = componentContract.static[0].assertions.length + componentContract.dynamic.length;
1153
1329
  const apgUrl = componentContract.meta?.source?.apg;
1154
1330
  const failures = [];
1331
+ const warnings = [];
1155
1332
  const passes = [];
1156
1333
  const skipped = [];
1157
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
+ };
1158
1350
  try {
1159
1351
  page = await createTestPage();
1160
1352
  if (url) {
@@ -1199,24 +1391,38 @@ This usually means:
1199
1391
  }).catch(() => {
1200
1392
  });
1201
1393
  }
1202
- const failuresBeforeStatic = failures.length;
1394
+ const hasSubmenuCapability = componentName === "menu" && !!componentContract.selectors.submenuTrigger ? await page.locator(componentContract.selectors.submenuTrigger).count() > 0 : false;
1395
+ let staticPassed = 0;
1396
+ let staticFailed = 0;
1397
+ let staticWarnings = 0;
1203
1398
  const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1204
1399
  for (const test of componentContract.static[0]?.assertions || []) {
1205
1400
  if (test.target === "relative") continue;
1206
1401
  const staticDescription = `${test.target}${test.attribute ? ` (${test.attribute})` : ""}`;
1402
+ const staticLevel = normalizeLevel(test.level);
1403
+ if (componentName === "menu" && test.target === "submenuTrigger" && !hasSubmenuCapability) {
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);
1407
+ continue;
1408
+ }
1207
1409
  const targetSelector = componentContract.selectors[test.target];
1208
1410
  if (!targetSelector) {
1209
1411
  const failure = `Selector for target ${test.target} not found.`;
1210
- failures.push(failure);
1211
- 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);
1212
1416
  continue;
1213
1417
  }
1214
1418
  const target = page.locator(targetSelector).first();
1215
1419
  const exists = await target.count() > 0;
1216
1420
  if (!exists) {
1217
1421
  const failure = `Target ${test.target} not found.`;
1218
- failures.push(failure);
1219
- 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);
1220
1426
  continue;
1221
1427
  }
1222
1428
  const isRedundantCheck = (selector, attrName, expectedVal) => {
@@ -1251,18 +1457,23 @@ This usually means:
1251
1457
  }
1252
1458
  if (!hasAny && !allRedundant) {
1253
1459
  const failure = test.failureMessage + ` None of the attributes "${test.attribute}" are present.`;
1254
- failures.push(failure);
1255
- 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);
1256
1464
  } else if (!allRedundant && hasAny) {
1257
1465
  passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
1258
- reporter.reportStaticTest(staticDescription, true);
1466
+ staticPassed += 1;
1467
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1259
1468
  } else {
1260
- reporter.reportStaticTest(staticDescription, true);
1469
+ staticPassed += 1;
1470
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1261
1471
  }
1262
1472
  } else {
1263
1473
  if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
1264
1474
  passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
1265
- reporter.reportStaticTest(staticDescription, true);
1475
+ staticPassed += 1;
1476
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1266
1477
  } else {
1267
1478
  const result = await staticAssertionRunner.validateAttribute(
1268
1479
  target,
@@ -1274,10 +1485,13 @@ This usually means:
1274
1485
  );
1275
1486
  if (result.success && result.passMessage) {
1276
1487
  passes.push(result.passMessage);
1277
- reporter.reportStaticTest(staticDescription, true);
1488
+ staticPassed += 1;
1489
+ reporter.reportStaticTest(staticDescription, "pass", void 0, staticLevel);
1278
1490
  } else if (!result.success && result.failMessage) {
1279
- failures.push(result.failMessage);
1280
- 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);
1281
1495
  }
1282
1496
  }
1283
1497
  }
@@ -1292,6 +1506,9 @@ This usually means:
1292
1506
  }
1293
1507
  const { action, assertions } = dynamicTest;
1294
1508
  const failuresBeforeTest = failures.length;
1509
+ const warningsBeforeTest = warnings.length;
1510
+ const skippedBeforeTest = skipped.length;
1511
+ const dynamicLevel = normalizeLevel(dynamicTest.level);
1295
1512
  try {
1296
1513
  await strategy.resetState(page);
1297
1514
  } catch (error) {
@@ -1301,14 +1518,19 @@ This usually means:
1301
1518
  }
1302
1519
  const shouldSkipTest = await strategy.shouldSkipTest(dynamicTest, page);
1303
1520
  if (shouldSkipTest) {
1304
- 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);
1305
1524
  continue;
1306
1525
  }
1307
1526
  const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
1308
1527
  const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1528
+ let shouldAbortCurrentTest = false;
1529
+ let actionOutcome = null;
1309
1530
  for (const act of action) {
1310
1531
  if (!page || page.isClosed()) {
1311
1532
  failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
1533
+ shouldAbortCurrentTest = true;
1312
1534
  break;
1313
1535
  }
1314
1536
  let result;
@@ -1327,39 +1549,59 @@ This usually means:
1327
1549
  }
1328
1550
  if (!result.success) {
1329
1551
  if (result.error) {
1330
- failures.push(result.error);
1331
- }
1332
- if (result.shouldBreak) {
1333
- if (result.error?.includes("optional submenu test")) {
1334
- reporter.reportTest(dynamicTest, "skip", result.error);
1335
- }
1336
- break;
1552
+ const outcome = classifyFailure(result.error, dynamicTest.level);
1553
+ actionOutcome = { status: outcome.status, detail: outcome.detail };
1337
1554
  }
1338
- continue;
1555
+ shouldAbortCurrentTest = true;
1556
+ break;
1339
1557
  }
1340
1558
  }
1559
+ if (shouldAbortCurrentTest) {
1560
+ reporter.reportTest(
1561
+ { description: dynamicTest.description, level: dynamicLevel },
1562
+ actionOutcome?.status || "fail",
1563
+ actionOutcome?.detail || failures[failures.length - 1]
1564
+ );
1565
+ continue;
1566
+ }
1341
1567
  for (const assertion of assertions) {
1342
1568
  const result = await assertionRunner.validate(assertion, dynamicTest.description);
1343
1569
  if (result.success && result.passMessage) {
1344
1570
  passes.push(result.passMessage);
1345
1571
  } else if (!result.success && result.failMessage) {
1346
- 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
+ }
1347
1577
  }
1348
1578
  }
1349
1579
  const failuresAfterTest = failures.length;
1350
- const testPassed = failuresAfterTest === failuresBeforeTest;
1351
- const failureMessage = testPassed ? void 0 : failures[failures.length - 1];
1352
- if (dynamicTest.isOptional === true && !testPassed) {
1353
- failures.pop();
1354
- 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
+ );
1355
1600
  } else {
1356
- reporter.reportTest(dynamicTest, testPassed ? "pass" : "fail", failureMessage);
1601
+ reporter.reportTest({ description: dynamicTest.description, level: dynamicLevel }, "pass");
1357
1602
  }
1358
1603
  }
1359
- const staticTotal = componentContract.static[0].assertions.length;
1360
- const staticFailed = failures.length - failuresBeforeStatic;
1361
- const staticPassed = Math.max(0, staticTotal - staticFailed);
1362
- reporter.reportStatic(staticPassed, staticFailed);
1604
+ reporter.reportStatic(staticPassed, staticFailed, staticWarnings);
1363
1605
  reporter.summary(failures);
1364
1606
  } catch (error) {
1365
1607
  if (error instanceof Error) {
@@ -1384,7 +1626,7 @@ Make sure your dev server is running at ${url}`);
1384
1626
  } finally {
1385
1627
  if (page) await page.close();
1386
1628
  }
1387
- return { passes, failures, skipped };
1629
+ return { passes, failures, skipped, warnings };
1388
1630
  }
1389
1631
  var import_fs2, import_meta3;
1390
1632
  var init_contractTestRunnerPlaywright = __esm({
@@ -1397,6 +1639,7 @@ var init_contractTestRunnerPlaywright = __esm({
1397
1639
  init_ContractReporter();
1398
1640
  init_ActionExecutor();
1399
1641
  init_AssertionRunner();
1642
+ init_strictness();
1400
1643
  import_meta3 = {};
1401
1644
  }
1402
1645
  });
@@ -1422,13 +1665,13 @@ function displayBadgeInfo(badgeType) {
1422
1665
  console.log(import_chalk.default.dim("\n This helps others discover accessibility tools and shows you care!\n"));
1423
1666
  }
1424
1667
  async function promptAddBadge(badgeType, cwd = process.cwd()) {
1425
- const readmePath = import_path.default.join(cwd, "README.md");
1426
- 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);
1427
1670
  if (!readmeExists) {
1428
1671
  console.log(import_chalk.default.yellow(" \u2139\uFE0F No README.md found in current directory"));
1429
1672
  return;
1430
1673
  }
1431
- const readmeContent = await import_fs_extra.default.readFile(readmePath, "utf-8");
1674
+ const readmeContent = await import_fs_extra2.default.readFile(readmePath, "utf-8");
1432
1675
  const markdown = getBadgeMarkdown(badgeType);
1433
1676
  if (readmeContent.includes(markdown) || readmeContent.includes(BADGE_CONFIGS[badgeType].fileName)) {
1434
1677
  console.log(import_chalk.default.gray(" \u2713 Badge already in README.md"));
@@ -1472,7 +1715,7 @@ async function addBadgeToReadme(readmePath, content, badge) {
1472
1715
  insertIndex = 1;
1473
1716
  }
1474
1717
  lines.splice(insertIndex, 0, badge);
1475
- 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");
1476
1719
  }
1477
1720
  function displayAllBadges() {
1478
1721
  console.log(import_chalk.default.cyan("\n\u{1F4CD} Available badges:"));
@@ -1484,12 +1727,12 @@ function displayAllBadges() {
1484
1727
  console.log(import_chalk.default.green(" " + getBadgeMarkdown("verified")));
1485
1728
  console.log("");
1486
1729
  }
1487
- 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;
1488
1731
  var init_badgeHelper = __esm({
1489
1732
  "src/utils/cli/badgeHelper.ts"() {
1490
1733
  "use strict";
1491
- import_fs_extra = __toESM(require("fs-extra"), 1);
1492
- import_path = __toESM(require("path"), 1);
1734
+ import_fs_extra2 = __toESM(require("fs-extra"), 1);
1735
+ import_path2 = __toESM(require("path"), 1);
1493
1736
  import_chalk = __toESM(require("chalk"), 1);
1494
1737
  import_readline = __toESM(require("readline"), 1);
1495
1738
  BADGE_CONFIGS = {
@@ -1990,11 +2233,14 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
1990
2233
  for (let i = 0; i < allItems.length; i++) {
1991
2234
  const item = allItems.item(i);
1992
2235
  const isNested = isItemInNestedSubmenu(item);
2236
+ const isDisabled = item.getAttribute("aria-disabled") === "true";
1993
2237
  if (!isNested) {
1994
2238
  if (!item.hasAttribute("tabindex")) {
1995
2239
  item.setAttribute("tabindex", "-1");
1996
2240
  }
1997
- filteredItems.push(item);
2241
+ if (!isDisabled) {
2242
+ filteredItems.push(item);
2243
+ }
1998
2244
  }
1999
2245
  }
2000
2246
  }
@@ -2019,9 +2265,14 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
2019
2265
  const items = getItems();
2020
2266
  items.forEach((item) => {
2021
2267
  item.setAttribute("role", "menuitem");
2022
- if (item.hasAttribute("data-submenu-id")) {
2268
+ const submenuId = item.getAttribute("data-submenu-id") ?? item.getAttribute("aria-controls");
2269
+ const hasSubmenuTriggerAttributes = item.hasAttribute("aria-haspopup") && submenuId;
2270
+ if (submenuId && (item.hasAttribute("data-submenu-id") || hasSubmenuTriggerAttributes)) {
2023
2271
  item.setAttribute("aria-haspopup", "menu");
2024
- item.setAttribute("aria-controls", item.getAttribute("data-submenu-id"));
2272
+ item.setAttribute("aria-controls", submenuId);
2273
+ if (!item.hasAttribute("aria-expanded")) {
2274
+ item.setAttribute("aria-expanded", "false");
2275
+ }
2025
2276
  }
2026
2277
  });
2027
2278
  }
@@ -2030,24 +2281,43 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
2030
2281
  const nextIndex = (currentIndex + direction + len) % len;
2031
2282
  elementItems.item(nextIndex).focus();
2032
2283
  }
2284
+ function focusItemAtIndex(items, index) {
2285
+ if (items.length === 0) return;
2286
+ items[index]?.focus();
2287
+ }
2033
2288
  function hasSubmenu(menuItem) {
2034
2289
  return menuItem.hasAttribute("aria-controls") && menuItem.hasAttribute("aria-haspopup") && menuItem.getAttribute("role") === "menuitem";
2035
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
+ }
2036
2305
  intializeMenuItems();
2037
2306
  function handleItemsKeydown(event, menuItem, menuItemIndex) {
2038
2307
  switch (event.key) {
2039
- case "ArrowUp":
2040
2308
  case "ArrowLeft": {
2041
2309
  if (event.key === "ArrowLeft" && triggerButton.getAttribute("role") === "menuitem") {
2042
2310
  event.preventDefault();
2043
2311
  closeMenu();
2044
2312
  return;
2045
2313
  }
2314
+ break;
2315
+ }
2316
+ case "ArrowUp": {
2046
2317
  event.preventDefault();
2047
2318
  moveFocus2(toNodeListLike(getFilteredItems()), menuItemIndex, -1);
2048
2319
  break;
2049
2320
  }
2050
- case "ArrowDown":
2051
2321
  case "ArrowRight": {
2052
2322
  if (event.key === "ArrowRight" && hasSubmenu(menuItem)) {
2053
2323
  event.preventDefault();
@@ -2057,10 +2327,24 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
2057
2327
  return;
2058
2328
  }
2059
2329
  }
2330
+ break;
2331
+ }
2332
+ case "ArrowDown": {
2060
2333
  event.preventDefault();
2061
2334
  moveFocus2(toNodeListLike(getFilteredItems()), menuItemIndex, 1);
2062
2335
  break;
2063
2336
  }
2337
+ case "Home": {
2338
+ event.preventDefault();
2339
+ focusItemAtIndex(getFilteredItems(), 0);
2340
+ break;
2341
+ }
2342
+ case "End": {
2343
+ event.preventDefault();
2344
+ const items = getFilteredItems();
2345
+ focusItemAtIndex(items, items.length - 1);
2346
+ break;
2347
+ }
2064
2348
  case "Escape": {
2065
2349
  event.preventDefault();
2066
2350
  closeMenu();
@@ -2073,15 +2357,25 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
2073
2357
  case "Enter":
2074
2358
  case " ": {
2075
2359
  event.preventDefault();
2360
+ if (hasSubmenu(menuItem)) {
2361
+ const submenuId = menuItem.getAttribute("aria-controls");
2362
+ if (submenuId) {
2363
+ openSubmenu(submenuId);
2364
+ return;
2365
+ }
2366
+ }
2076
2367
  menuItem.click();
2368
+ closeMenu();
2369
+ if (onOpenChange) {
2370
+ onOpenChange(false);
2371
+ }
2077
2372
  break;
2078
2373
  }
2079
2374
  case "Tab": {
2080
- if (!event.shiftKey || event.shiftKey) {
2081
- closeMenu();
2082
- if (onOpenChange) {
2083
- onOpenChange(false);
2084
- }
2375
+ closeMenu();
2376
+ closeAncestorMenusFromTrigger(triggerButton);
2377
+ if (onOpenChange) {
2378
+ onOpenChange(false);
2085
2379
  }
2086
2380
  break;
2087
2381
  }
@@ -2182,6 +2476,7 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
2182
2476
  }
2183
2477
  }
2184
2478
  function closeMenu() {
2479
+ submenuInstances.forEach((instance) => instance.closeMenu());
2185
2480
  setAria(false);
2186
2481
  menuDiv.style.display = "none";
2187
2482
  removeListeners();
@@ -2213,7 +2508,6 @@ function makeMenuAccessible({ menuId, menuItemsClass, triggerId, callback }) {
2213
2508
  }
2214
2509
  triggerButton.addEventListener("click", handleTriggerClick);
2215
2510
  document.addEventListener("click", handleClickOutside);
2216
- triggerButton.setAttribute("data-menu-initialized", "true");
2217
2511
  function cleanup() {
2218
2512
  removeListeners();
2219
2513
  triggerButton.removeEventListener("click", handleTriggerClick);
@@ -2973,9 +3267,11 @@ var import_jest_axe = require("jest-axe");
2973
3267
  init_contract();
2974
3268
  var import_promises = __toESM(require("fs/promises"), 1);
2975
3269
  init_ContractReporter();
3270
+ init_strictness();
2976
3271
  var import_meta = {};
2977
- async function runContractTests(componentName, component) {
3272
+ async function runContractTests(componentName, component, strictness) {
2978
3273
  const reporter = new ContractReporter(false);
3274
+ const strictnessMode = normalizeStrictness(strictness);
2979
3275
  const contractTyped = contract_default;
2980
3276
  const contractPath = contractTyped[componentName]?.path;
2981
3277
  if (!contractPath) {
@@ -2989,19 +3285,42 @@ async function runContractTests(componentName, component) {
2989
3285
  const failures = [];
2990
3286
  const passes = [];
2991
3287
  const skipped = [];
2992
- 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;
2993
3307
  for (const test of componentContract.static[0].assertions) {
2994
3308
  if (test.target !== "relative") {
3309
+ const staticLevel = normalizeLevel(test.level);
2995
3310
  const selector = componentContract.selectors[test.target];
2996
3311
  if (!selector) {
2997
- failures.push(`Selector for target ${test.target} not found.`);
2998
- 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);
2999
3316
  continue;
3000
3317
  }
3001
3318
  const target = component.querySelector(selector);
3002
3319
  if (!target) {
3003
- failures.push(`Target ${test.target} not found.`);
3004
- 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);
3005
3324
  continue;
3006
3325
  }
3007
3326
  const attributeValue = target.getAttribute(test.attribute);
@@ -3009,36 +3328,40 @@ async function runContractTests(componentName, component) {
3009
3328
  const attributes = test.attribute.split(" | ");
3010
3329
  const hasAnyAttribute = attributes.some((attr) => target.hasAttribute(attr));
3011
3330
  if (!hasAnyAttribute) {
3012
- failures.push(test.failureMessage + ` None of the attributes "${test.attribute}" are present.`);
3013
- 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);
3014
3335
  } else {
3015
3336
  passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
3016
- reporter.reportStaticTest(`${test.target} has ${test.attribute}`, true);
3337
+ staticPassed += 1;
3338
+ reporter.reportStaticTest(`${test.target} has ${test.attribute}`, "pass", void 0, staticLevel);
3017
3339
  }
3018
3340
  } else if (!attributeValue || !test.expectedValue.split(" | ").includes(attributeValue)) {
3019
- failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
3020
- 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);
3021
3345
  } else {
3022
3346
  passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
3023
- 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);
3024
3349
  }
3025
3350
  }
3026
3351
  }
3027
3352
  for (const dynamicTest of componentContract.dynamic) {
3028
3353
  skipped.push(dynamicTest.description);
3029
- reporter.reportTest(dynamicTest, "skip");
3354
+ reporter.reportTest({ description: dynamicTest.description, level: dynamicTest.level }, "skip");
3030
3355
  }
3031
- const staticTotal = componentContract.static[0].assertions.length;
3032
- const staticFailed = failures.length - failuresBeforeStatic;
3033
- const staticPassed = Math.max(0, staticTotal - staticFailed);
3034
- reporter.reportStatic(staticPassed, staticFailed);
3356
+ reporter.reportStatic(staticPassed, staticFailed, staticWarnings);
3035
3357
  reporter.summary(failures);
3036
- return { passes, failures, skipped };
3358
+ return { passes, failures, skipped, warnings };
3037
3359
  }
3038
3360
 
3039
3361
  // src/utils/test/src/test.ts
3040
3362
  init_playwrightTestHarness();
3041
- async function testUiComponent(componentName, component, url) {
3363
+ init_strictness();
3364
+ async function testUiComponent(componentName, component, url, options = {}) {
3042
3365
  if (!componentName || typeof componentName !== "string") {
3043
3366
  throw new Error("\u274C testUiComponent requires a valid componentName (string)");
3044
3367
  }
@@ -3075,6 +3398,17 @@ Error: ${error instanceof Error ? error.message : String(error)}`
3075
3398
  }
3076
3399
  return null;
3077
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
+ }
3078
3412
  let contract;
3079
3413
  try {
3080
3414
  if (url) {
@@ -3082,7 +3416,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
3082
3416
  if (devServerUrl) {
3083
3417
  console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
3084
3418
  const { runContractTestsPlaywright: runContractTestsPlaywright2 } = await Promise.resolve().then(() => (init_contractTestRunnerPlaywright(), contractTestRunnerPlaywright_exports));
3085
- contract = await runContractTestsPlaywright2(componentName, devServerUrl);
3419
+ contract = await runContractTestsPlaywright2(componentName, devServerUrl, strictness);
3086
3420
  } else {
3087
3421
  throw new Error(
3088
3422
  `\u274C Dev server not running at ${url}
@@ -3091,7 +3425,7 @@ Please start your dev server and try again.`
3091
3425
  }
3092
3426
  } else if (component) {
3093
3427
  console.log(`\u{1F3AD} Running component contract tests in JSDOM mode`);
3094
- contract = await runContractTests(componentName, component);
3428
+ contract = await runContractTests(componentName, component, strictness);
3095
3429
  } else {
3096
3430
  throw new Error("\u274C Either component or URL must be provided");
3097
3431
  }