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.
- package/README.md +14 -10
- package/bin/{chunk-AUJAN4RK.js → chunk-LKN5PRYD.js} +0 -5
- package/bin/cli.cjs +50 -33
- package/bin/cli.js +1 -1
- package/{dist/contractTestRunnerPlaywright-7F756CFB.js → bin/contractTestRunnerPlaywright-PC6JOYYV.js} +51 -29
- package/bin/{test-C3CMRHSI.js → test-LP723IXM.js} +2 -2
- package/dist/{chunk-AUJAN4RK.js → chunk-LKN5PRYD.js} +0 -5
- package/{bin/contractTestRunnerPlaywright-7F756CFB.js → dist/contractTestRunnerPlaywright-PC6JOYYV.js} +51 -29
- package/dist/index.cjs +165 -100
- package/dist/index.js +117 -69
- package/dist/src/{Types.d-yGC2bBaB.d.cts → Types.d-DYfYR3Vc.d.cts} +1 -1
- package/dist/src/{Types.d-yGC2bBaB.d.ts → Types.d-DYfYR3Vc.d.ts} +1 -1
- package/dist/src/accordion/index.d.cts +1 -1
- package/dist/src/accordion/index.d.ts +1 -1
- package/dist/src/block/index.cjs +1 -6
- package/dist/src/block/index.d.cts +1 -1
- package/dist/src/block/index.d.ts +1 -1
- package/dist/src/block/index.js +72 -1
- package/dist/src/checkbox/index.d.cts +1 -1
- package/dist/src/checkbox/index.d.ts +1 -1
- package/dist/src/combobox/index.d.cts +1 -1
- package/dist/src/combobox/index.d.ts +1 -1
- package/dist/src/menu/index.cjs +112 -142
- package/dist/src/menu/index.d.cts +1 -1
- package/dist/src/menu/index.d.ts +1 -1
- package/dist/src/menu/index.js +112 -18
- package/dist/src/radio/index.d.cts +1 -1
- package/dist/src/radio/index.d.ts +1 -1
- package/dist/src/tabs/index.d.cts +1 -1
- package/dist/src/tabs/index.d.ts +1 -1
- package/dist/src/toggle/index.d.cts +1 -1
- package/dist/src/toggle/index.d.ts +1 -1
- package/dist/src/utils/test/aria-contracts/accordion/accordion.contract.json +1 -1
- package/dist/src/utils/test/aria-contracts/combobox/combobox.listbox.contract.json +1 -1
- package/dist/src/utils/test/aria-contracts/menu/menu.contract.json +143 -30
- package/dist/src/utils/test/aria-contracts/tabs/tabs.contract.json +8 -8
- package/dist/src/utils/test/{chunk-AUJAN4RK.js → chunk-LKN5PRYD.js} +0 -5
- package/dist/src/utils/test/{contractTestRunnerPlaywright-HL73FADJ.js → contractTestRunnerPlaywright-RGKMGXND.js} +51 -29
- package/dist/src/utils/test/index.cjs +50 -33
- package/dist/src/utils/test/index.js +2 -2
- package/package.json +1 -1
- 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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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.
|
|
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 };
|