aria-ease 6.5.0 → 6.6.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 (42) hide show
  1. package/README.md +14 -10
  2. package/bin/{chunk-AUJAN4RK.js → chunk-LKN5PRYD.js} +0 -5
  3. package/bin/cli.cjs +50 -33
  4. package/bin/cli.js +1 -1
  5. package/{dist/contractTestRunnerPlaywright-7F756CFB.js → bin/contractTestRunnerPlaywright-PC6JOYYV.js} +51 -29
  6. package/bin/{test-C3CMRHSI.js → test-LP723IXM.js} +2 -2
  7. package/dist/{chunk-AUJAN4RK.js → chunk-LKN5PRYD.js} +0 -5
  8. package/{bin/contractTestRunnerPlaywright-7F756CFB.js → dist/contractTestRunnerPlaywright-PC6JOYYV.js} +51 -29
  9. package/dist/index.cjs +165 -100
  10. package/dist/index.js +117 -69
  11. package/dist/src/{Types.d-yGC2bBaB.d.cts → Types.d-DYfYR3Vc.d.cts} +1 -1
  12. package/dist/src/{Types.d-yGC2bBaB.d.ts → Types.d-DYfYR3Vc.d.ts} +1 -1
  13. package/dist/src/accordion/index.d.cts +1 -1
  14. package/dist/src/accordion/index.d.ts +1 -1
  15. package/dist/src/block/index.cjs +1 -6
  16. package/dist/src/block/index.d.cts +1 -1
  17. package/dist/src/block/index.d.ts +1 -1
  18. package/dist/src/block/index.js +72 -1
  19. package/dist/src/checkbox/index.d.cts +1 -1
  20. package/dist/src/checkbox/index.d.ts +1 -1
  21. package/dist/src/combobox/index.d.cts +1 -1
  22. package/dist/src/combobox/index.d.ts +1 -1
  23. package/dist/src/menu/index.cjs +112 -142
  24. package/dist/src/menu/index.d.cts +1 -1
  25. package/dist/src/menu/index.d.ts +1 -1
  26. package/dist/src/menu/index.js +112 -18
  27. package/dist/src/radio/index.d.cts +1 -1
  28. package/dist/src/radio/index.d.ts +1 -1
  29. package/dist/src/tabs/index.d.cts +1 -1
  30. package/dist/src/tabs/index.d.ts +1 -1
  31. package/dist/src/toggle/index.d.cts +1 -1
  32. package/dist/src/toggle/index.d.ts +1 -1
  33. package/dist/src/utils/test/aria-contracts/accordion/accordion.contract.json +1 -1
  34. package/dist/src/utils/test/aria-contracts/combobox/combobox.listbox.contract.json +1 -1
  35. package/dist/src/utils/test/aria-contracts/menu/menu.contract.json +143 -30
  36. package/dist/src/utils/test/aria-contracts/tabs/tabs.contract.json +8 -8
  37. package/dist/src/utils/test/{chunk-AUJAN4RK.js → chunk-LKN5PRYD.js} +0 -5
  38. package/dist/src/utils/test/{contractTestRunnerPlaywright-HL73FADJ.js → contractTestRunnerPlaywright-RGKMGXND.js} +51 -29
  39. package/dist/src/utils/test/index.cjs +50 -33
  40. package/dist/src/utils/test/index.js +2 -2
  41. package/package.json +1 -1
  42. package/dist/src/chunk-ZJXZZDUR.js +0 -127
@@ -234,11 +234,6 @@ ${"\u2500".repeat(60)}`);
234
234
  ${"\u2550".repeat(60)}`);
235
235
  this.log(`\u{1F4CA} Summary
236
236
  `);
237
- const staticIcon = this.staticFailures === 0 ? "\u2705" : "\u274C";
238
- const staticStatus = this.staticFailures === 0 ? "PASS" : "FAIL";
239
- this.log(`${staticIcon} Static ARIA Tests: ${staticStatus}`);
240
- this.log(` ${this.staticPasses}/${this.staticPasses + this.staticFailures} required attributes present`);
241
- this.log("");
242
237
  if (totalFailures === 0 && this.skipped === 0 && this.optionalSuggestions === 0) {
243
238
  this.log(`\u2705 All ${totalRun} tests passed!`);
244
239
  this.log(` ${this.componentName.charAt(0).toUpperCase()}${this.componentName.slice(1)} component meets WAI-ARIA expectations for Roles, States, Properties, and Keyboard Interactions \u2713`);
@@ -495,29 +490,20 @@ This indicates a problem with the menu component's close functionality.`
495
490
  }
496
491
  }
497
492
  async shouldSkipTest(test, page) {
498
- for (const act of test.action) {
499
- if (act.type === "keypress" && (act.target === "submenuTrigger" || act.target === "submenu")) {
500
- const submenuSelector = this.selectors[act.target];
501
- if (submenuSelector) {
502
- const submenuCount = await page.locator(submenuSelector).count();
503
- if (submenuCount === 0) {
504
- return true;
505
- }
506
- }
507
- }
493
+ const requiresSubmenu = test.action.some(
494
+ (act) => act.target === "submenu" || act.target === "submenuTrigger" || act.target === "submenuItems"
495
+ ) || test.assertions.some(
496
+ (assertion) => assertion.target === "submenu" || assertion.target === "submenuTrigger" || assertion.target === "submenuItems"
497
+ );
498
+ if (!requiresSubmenu) {
499
+ return false;
508
500
  }
509
- for (const assertion of test.assertions) {
510
- if (assertion.target === "submenu" || assertion.target === "submenuTrigger") {
511
- const submenuSelector = this.selectors[assertion.target];
512
- if (submenuSelector) {
513
- const submenuCount = await page.locator(submenuSelector).count();
514
- if (submenuCount === 0) {
515
- return true;
516
- }
517
- }
518
- }
501
+ const submenuTriggerSelector = this.selectors.submenuTrigger;
502
+ if (!submenuTriggerSelector) {
503
+ return true;
519
504
  }
520
- return false;
505
+ const submenuTriggerCount = await page.locator(submenuTriggerSelector).count();
506
+ return submenuTriggerCount === 0;
521
507
  }
522
508
  getMainSelector() {
523
509
  return this.mainSelector;
@@ -652,6 +638,9 @@ var init_ActionExecutor = __esm({
652
638
  this.selectors = selectors;
653
639
  this.timeoutMs = timeoutMs;
654
640
  }
641
+ isOptionalMenuTarget(target) {
642
+ return ["submenu", "submenuTrigger", "submenuItems"].includes(target);
643
+ }
655
644
  /**
656
645
  * Check if error is due to browser/page being closed
657
646
  */
@@ -772,7 +761,7 @@ var init_ActionExecutor = __esm({
772
761
  } else if (keyValue.includes(" ")) {
773
762
  keyValue = keyValue.replace(/ /g, "");
774
763
  }
775
- if (target === "focusable" && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape"].includes(keyValue)) {
764
+ if (target === "focusable" && ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape", "Home", "End", "Tab", "Shift+Tab"].includes(keyValue)) {
776
765
  await this.page.keyboard.press(keyValue);
777
766
  return { success: true };
778
767
  }
@@ -783,9 +772,10 @@ var init_ActionExecutor = __esm({
783
772
  const locator = this.page.locator(selector).first();
784
773
  const elementCount = await locator.count();
785
774
  if (elementCount === 0) {
775
+ const optionalMenuTarget = this.isOptionalMenuTarget(target);
786
776
  return {
787
777
  success: false,
788
- error: `${target} element not found (optional submenu test)`,
778
+ error: optionalMenuTarget ? `${target} element not found (optional submenu test)` : `${target} element not found.`,
789
779
  shouldBreak: true
790
780
  // Signal to skip this test
791
781
  };
@@ -1154,23 +1144,35 @@ This usually means:
1154
1144
  }).catch(() => {
1155
1145
  });
1156
1146
  }
1157
- const failuresBeforeStatic = failures.length;
1147
+ const hasSubmenuCapability = componentName === "menu" && !!componentContract.selectors.submenuTrigger ? await page.locator(componentContract.selectors.submenuTrigger).count() > 0 : false;
1148
+ let staticFailed = 0;
1158
1149
  const staticAssertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1159
1150
  for (const test of componentContract.static[0]?.assertions || []) {
1160
1151
  if (test.target === "relative") continue;
1161
1152
  const staticDescription = `${test.target}${test.attribute ? ` (${test.attribute})` : ""}`;
1153
+ if (componentName === "menu" && test.target === "submenuTrigger" && !hasSubmenuCapability) {
1154
+ passes.push(`Skipping submenu static assertion for ${test.target}: no submenu capability detected in rendered component.`);
1155
+ reporter.reportStaticTest(staticDescription, true);
1156
+ continue;
1157
+ }
1162
1158
  const targetSelector = componentContract.selectors[test.target];
1163
1159
  if (!targetSelector) {
1164
1160
  const failure = `Selector for target ${test.target} not found.`;
1165
1161
  failures.push(failure);
1162
+ staticFailed += 1;
1166
1163
  reporter.reportStaticTest(staticDescription, false, failure);
1167
1164
  continue;
1168
1165
  }
1169
1166
  const target = page.locator(targetSelector).first();
1170
1167
  const exists = await target.count() > 0;
1171
1168
  if (!exists) {
1169
+ if (test.isOptional === true) {
1170
+ reporter.reportStaticTest(staticDescription, true);
1171
+ continue;
1172
+ }
1172
1173
  const failure = `Target ${test.target} not found.`;
1173
1174
  failures.push(failure);
1175
+ staticFailed += 1;
1174
1176
  reporter.reportStaticTest(staticDescription, false, failure);
1175
1177
  continue;
1176
1178
  }
@@ -1207,6 +1209,7 @@ This usually means:
1207
1209
  if (!hasAny && !allRedundant) {
1208
1210
  const failure = test.failureMessage + ` None of the attributes "${test.attribute}" are present.`;
1209
1211
  failures.push(failure);
1212
+ staticFailed += 1;
1210
1213
  reporter.reportStaticTest(staticDescription, false, failure);
1211
1214
  } else if (!allRedundant && hasAny) {
1212
1215
  passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
@@ -1232,6 +1235,7 @@ This usually means:
1232
1235
  reporter.reportStaticTest(staticDescription, true);
1233
1236
  } else if (!result.success && result.failMessage) {
1234
1237
  failures.push(result.failMessage);
1238
+ staticFailed += 1;
1235
1239
  reporter.reportStaticTest(staticDescription, false, result.failMessage);
1236
1240
  }
1237
1241
  }
@@ -1261,9 +1265,12 @@ This usually means:
1261
1265
  }
1262
1266
  const actionExecutor = new ActionExecutor(page, componentContract.selectors, actionTimeoutMs);
1263
1267
  const assertionRunner = new AssertionRunner(page, componentContract.selectors, assertionTimeoutMs);
1268
+ let shouldSkipCurrentTest = false;
1269
+ let shouldAbortCurrentTest = false;
1264
1270
  for (const act of action) {
1265
1271
  if (!page || page.isClosed()) {
1266
1272
  failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
1273
+ shouldAbortCurrentTest = true;
1267
1274
  break;
1268
1275
  }
1269
1276
  let result;
@@ -1281,18 +1288,29 @@ This usually means:
1281
1288
  continue;
1282
1289
  }
1283
1290
  if (!result.success) {
1284
- if (result.error) {
1285
- failures.push(result.error);
1286
- }
1287
1291
  if (result.shouldBreak) {
1288
1292
  if (result.error?.includes("optional submenu test")) {
1289
1293
  reporter.reportTest(dynamicTest, "skip", result.error);
1294
+ shouldSkipCurrentTest = true;
1295
+ } else if (result.error) {
1296
+ failures.push(result.error);
1297
+ shouldAbortCurrentTest = true;
1290
1298
  }
1291
1299
  break;
1292
1300
  }
1301
+ if (result.error) {
1302
+ failures.push(result.error);
1303
+ }
1293
1304
  continue;
1294
1305
  }
1295
1306
  }
1307
+ if (shouldSkipCurrentTest) {
1308
+ continue;
1309
+ }
1310
+ if (shouldAbortCurrentTest) {
1311
+ reporter.reportTest(dynamicTest, "fail", failures[failures.length - 1]);
1312
+ continue;
1313
+ }
1296
1314
  for (const assertion of assertions) {
1297
1315
  const result = await assertionRunner.validate(assertion, dynamicTest.description);
1298
1316
  if (result.success && result.passMessage) {
@@ -1312,7 +1330,6 @@ This usually means:
1312
1330
  }
1313
1331
  }
1314
1332
  const staticTotal = componentContract.static[0].assertions.length;
1315
- const staticFailed = failures.length - failuresBeforeStatic;
1316
1333
  const staticPassed = Math.max(0, staticTotal - staticFailed);
1317
1334
  reporter.reportStatic(staticPassed, staticFailed);
1318
1335
  reporter.summary(failures);
@@ -1,4 +1,4 @@
1
- import { closeSharedBrowser, ContractReporter, contract_default } from './chunk-AUJAN4RK.js';
1
+ import { closeSharedBrowser, ContractReporter, contract_default } from './chunk-LKN5PRYD.js';
2
2
  import { axe } from 'jest-axe';
3
3
  import fs from 'fs/promises';
4
4
 
@@ -108,7 +108,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
108
108
  const devServerUrl = await checkDevServer(url);
109
109
  if (devServerUrl) {
110
110
  console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
111
- const { runContractTestsPlaywright } = await import('./contractTestRunnerPlaywright-HL73FADJ.js');
111
+ const { runContractTestsPlaywright } = await import('./contractTestRunnerPlaywright-RGKMGXND.js');
112
112
  contract = await runContractTestsPlaywright(componentName, devServerUrl);
113
113
  } else {
114
114
  throw new Error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aria-ease",
3
- "version": "6.5.0",
3
+ "version": "6.6.0",
4
4
  "description": "Accessibility infrastructure for the entire frontend engineering lifecycle. Build accessible patterns, run automated audits, verify component interactions, and gate deployments — all in one system.",
5
5
  "main": "dist/index.cjs",
6
6
  "type": "module",
@@ -1,127 +0,0 @@
1
- // src/utils/handleKeyPress/handleKeyPress.ts
2
- function isTextInput(el) {
3
- if (el.tagName !== "INPUT") return false;
4
- const type = el.type;
5
- return ["text", "email", "password", "tel", "number"].includes(type);
6
- }
7
- function isTextArea(el) {
8
- return el.tagName === "TEXTAREA";
9
- }
10
- function isNativeButton(el) {
11
- return el.tagName === "BUTTON" || el.tagName === "INPUT" && ["button", "submit", "reset"].includes(el.type);
12
- }
13
- function isLink(el) {
14
- return el.tagName === "A";
15
- }
16
- function moveFocus(elementItems, currentIndex, direction) {
17
- const len = elementItems.length;
18
- const nextIndex = (currentIndex + direction + len) % len;
19
- elementItems.item(nextIndex).focus();
20
- }
21
- function isClickableButNotSemantic(el) {
22
- return el.getAttribute("data-custom-click") !== null && el.getAttribute("data-custom-click") !== void 0;
23
- }
24
- function handleMenuClose(menuElement, menuTriggerButton) {
25
- menuElement.style.display = "none";
26
- const menuTriggerButtonId = menuTriggerButton.getAttribute("id");
27
- if (!menuTriggerButtonId) {
28
- console.error("[aria-ease] Menu trigger button must have an id attribute to properly set aria attributes.");
29
- return;
30
- }
31
- menuTriggerButton.setAttribute("aria-expanded", "false");
32
- }
33
- function hasSubmenu(menuItem) {
34
- return menuItem.getAttribute("aria-haspopup") === "true" || menuItem.getAttribute("aria-haspopup") === "menu";
35
- }
36
- function getSubmenuId(menuItem) {
37
- return menuItem.getAttribute("aria-controls");
38
- }
39
- function handleKeyPress(event, elementItems, elementItemIndex, menuElementDiv, triggerButton, openSubmenu, closeSubmenu, onOpenChange) {
40
- const currentEl = elementItems.item(elementItemIndex);
41
- switch (event.key) {
42
- case "ArrowUp":
43
- case "ArrowLeft": {
44
- if (event.key === "ArrowLeft" && menuElementDiv && closeSubmenu) {
45
- const labelledBy = menuElementDiv.getAttribute("aria-labelledby");
46
- if (labelledBy) {
47
- const parentTrigger = document.getElementById(labelledBy);
48
- if (parentTrigger && parentTrigger.getAttribute("role") === "menuitem") {
49
- event.preventDefault();
50
- closeSubmenu();
51
- parentTrigger.focus();
52
- return;
53
- }
54
- }
55
- }
56
- if (!isTextInput(currentEl) && !isTextArea(currentEl)) {
57
- event.preventDefault();
58
- moveFocus(elementItems, elementItemIndex, -1);
59
- } else if (isTextInput(currentEl) || isTextArea(currentEl)) {
60
- const cursorStart = currentEl.selectionStart;
61
- if (cursorStart === 0) {
62
- event.preventDefault();
63
- moveFocus(elementItems, elementItemIndex, -1);
64
- }
65
- }
66
- break;
67
- }
68
- case "ArrowDown":
69
- case "ArrowRight": {
70
- if (event.key === "ArrowRight" && hasSubmenu(currentEl) && openSubmenu) {
71
- event.preventDefault();
72
- const submenuId = getSubmenuId(currentEl);
73
- if (submenuId) {
74
- openSubmenu(submenuId);
75
- return;
76
- }
77
- }
78
- if (!isTextInput(currentEl) && !isTextArea(currentEl)) {
79
- event.preventDefault();
80
- moveFocus(elementItems, elementItemIndex, 1);
81
- } else if (isTextInput(currentEl) || isTextArea(currentEl)) {
82
- const value = currentEl.value;
83
- const cursorEnd = currentEl.selectionStart;
84
- if (cursorEnd === value.length) {
85
- event.preventDefault();
86
- moveFocus(elementItems, elementItemIndex, 1);
87
- }
88
- }
89
- break;
90
- }
91
- case "Escape": {
92
- event.preventDefault();
93
- if (menuElementDiv && triggerButton) {
94
- if (getComputedStyle(menuElementDiv).display === "block") {
95
- handleMenuClose(menuElementDiv, triggerButton);
96
- if (onOpenChange) {
97
- onOpenChange(false);
98
- }
99
- }
100
- triggerButton.focus();
101
- }
102
- break;
103
- }
104
- case "Enter":
105
- case " ": {
106
- if (!isNativeButton(currentEl) && !isLink(currentEl) && isClickableButNotSemantic(currentEl)) {
107
- event.preventDefault();
108
- currentEl.click();
109
- } else if (isNativeButton(currentEl)) {
110
- event.preventDefault();
111
- currentEl.click();
112
- }
113
- break;
114
- }
115
- case "Tab": {
116
- if (menuElementDiv && triggerButton && (!event.shiftKey || event.shiftKey)) {
117
- handleMenuClose(menuElementDiv, triggerButton);
118
- if (onOpenChange) {
119
- onOpenChange(false);
120
- }
121
- }
122
- break;
123
- }
124
- }
125
- }
126
-
127
- export { handleKeyPress };