aria-ease 6.8.0 → 6.9.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 +68 -6
- package/bin/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
- package/bin/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
- package/bin/MenuComponentStrategy-JAMTCSNF.js +81 -0
- package/bin/TabsComponentStrategy-3SQURPMX.js +29 -0
- package/bin/buildContracts-GBOY7UXG.js +437 -0
- package/bin/{chunk-VPBHLMAS.js → chunk-LMSKLN5O.js} +21 -0
- package/bin/chunk-PK5L2SAF.js +17 -0
- package/bin/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
- package/bin/cli.cjs +991 -128
- package/bin/cli.js +33 -2
- package/bin/{configLoader-XRF6VM4J.js → configLoader-Q6A4JLKW.js} +1 -1
- package/{dist/contractTestRunnerPlaywright-UAOFNS7Z.js → bin/contractTestRunnerPlaywright-ZZNWDUYP.js} +270 -219
- package/bin/{test-WRIJHN6H.js → test-OND56UUL.js} +97 -10
- package/dist/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
- package/dist/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
- package/dist/MenuComponentStrategy-JAMTCSNF.js +81 -0
- package/dist/TabsComponentStrategy-3SQURPMX.js +29 -0
- package/dist/chunk-PK5L2SAF.js +17 -0
- package/dist/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
- package/dist/{configLoader-IT4PWCJB.js → configLoader-WTGJAP4Z.js} +21 -0
- package/{bin/contractTestRunnerPlaywright-UAOFNS7Z.js → dist/contractTestRunnerPlaywright-XBWJZMR3.js} +270 -219
- package/dist/index.cjs +800 -96
- package/dist/index.d.cts +136 -1
- package/dist/index.d.ts +136 -1
- package/dist/index.js +421 -16
- package/dist/src/utils/test/AccordionComponentStrategy-WRHZOEN6.js +38 -0
- package/dist/src/utils/test/ComboboxComponentStrategy-5AECQSRN.js +60 -0
- package/dist/src/utils/test/MenuComponentStrategy-VKZQYLBE.js +77 -0
- package/dist/src/utils/test/TabsComponentStrategy-BKG53SEV.js +26 -0
- package/dist/src/utils/test/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
- package/dist/src/utils/test/{configLoader-LD4RV2WQ.js → configLoader-YE2CYGDG.js} +21 -0
- package/dist/src/utils/test/{contractTestRunnerPlaywright-IRJOAEMT.js → contractTestRunnerPlaywright-LC5OAVXB.js} +262 -200
- package/dist/src/utils/test/index.cjs +472 -88
- package/dist/src/utils/test/index.js +97 -12
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -31,13 +31,13 @@ Aria-Ease isn't a utility library. **It's an accessibility infrastructure** that
|
|
|
31
31
|
|
|
32
32
|
**Traditional approach:** Build features → Manual testing → Find accessibility issues → Fix them → Manual testing again → Ship (maybe)
|
|
33
33
|
|
|
34
|
-
**Aria-Ease approach:** Build with accessible utilities → Automated audits catch issues → Contract tests verify
|
|
34
|
+
**Aria-Ease approach:** Build with accessible baseline utilities → Automated audits catch issues → Contract tests verify consistent baseline component behaviors → CI/CD gates deployment → Ship with confidence
|
|
35
35
|
|
|
36
36
|
### What Makes This Different?
|
|
37
37
|
|
|
38
38
|
#### 1. **Component Utilities** (Available Now)
|
|
39
39
|
|
|
40
|
-
Reusable accessible interaction patterns
|
|
40
|
+
Reusable accessible interaction patterns based on Aria-Ease's baseline interpretation of WAI-ARIA APG guidance. Not the only valid implementation, but a proven, consistent place to start. Tree-shakable, framework-agnostic, production-ready.
|
|
41
41
|
|
|
42
42
|
```javascript
|
|
43
43
|
// Instead of 50+ lines managing ARIA attributes and keyboard events...
|
|
@@ -60,7 +60,9 @@ npx aria-ease audit --url https://yoursite.com
|
|
|
60
60
|
|
|
61
61
|
#### 3. **Contract Testing** (Available Now)
|
|
62
62
|
|
|
63
|
-
This is the game-changer. We encoded
|
|
63
|
+
This is the game-changer. We encoded a deterministic, testable baseline interpretation of WAI-ARIA APG guidance into JSON "contracts" and built a custom Playwright runner with isolated test-harness architecture. Run it locally or in CI/CD.
|
|
64
|
+
|
|
65
|
+
Today, Aria-Ease maintains this baseline contract set. Down the line, contracts are being designed to be extendable and overridable so teams and experts can enforce their own standards without losing consistency.
|
|
64
66
|
|
|
65
67
|
**The result?** Component interaction testing that feels closer to unit testing than manual QA.
|
|
66
68
|
|
|
@@ -70,7 +72,7 @@ npx aria-ease test
|
|
|
70
72
|
# ✓ 26 assertions in ~1 second in CI
|
|
71
73
|
```
|
|
72
74
|
|
|
73
|
-
**Why this matters:** Before, verifying a combobox meant testing every interaction manually. Now, Aria-Ease automates the
|
|
75
|
+
**Why this matters:** Before, verifying a combobox meant testing every interaction manually. Now, Aria-Ease automates the repeatable baseline aspects of testing a combobox: keyboard interaction, ARIA state updates, visibility, and semantic roles.
|
|
74
76
|
|
|
75
77
|
#### 4. **CI/CD Integration** (Available Now)
|
|
76
78
|
|
|
@@ -111,7 +113,7 @@ Visualize accessibility health across your entire application. Track progress, i
|
|
|
111
113
|
- 🎯 **Tree-shakable** - Import only what you need (1.4KB - 3.7KB per component)
|
|
112
114
|
- ♿ **WCAG Compliant** - Follows WAI-ARIA best practices
|
|
113
115
|
- ⌨️ **Keyboard Interaction** - Full keyboard support out of the box
|
|
114
|
-
- 🧪 **Contract Testing** - Built-in accessibility testing framework
|
|
116
|
+
- 🧪 **Contract Testing** - Built-in baseline accessibility testing framework
|
|
115
117
|
- 🎭 **Framework Agnostic** - Works with React, Vue, vanilla JS, etc.
|
|
116
118
|
- 🔍 **CLI Audit Tool** - Automated accessibility testing for your sites
|
|
117
119
|
- 📦 **TypeScript Support** - Full type definitions included
|
|
@@ -176,6 +178,61 @@ npx aria-ease audit
|
|
|
176
178
|
|
|
177
179
|
The CLI will automatically find and load your config file, with validation to catch errors early.
|
|
178
180
|
|
|
181
|
+
### Contract DSL Build Workflow
|
|
182
|
+
|
|
183
|
+
You can author custom component contracts as readable DSL files and compile them with a built-in CLI command.
|
|
184
|
+
|
|
185
|
+
1. Create one or more `*.contract.mjs` files that export a contract built with `contract("component.name", ...)`.
|
|
186
|
+
2. Configure contract sources in `ariaease.config.js`.
|
|
187
|
+
3. Run `npx aria-ease build contracts`.
|
|
188
|
+
4. Run `npx aria-ease test`.
|
|
189
|
+
|
|
190
|
+
Example config with multiple contract sources:
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
export default {
|
|
194
|
+
test: {
|
|
195
|
+
strictness: "balanced",
|
|
196
|
+
components: [
|
|
197
|
+
{
|
|
198
|
+
name: "combobox.listbox",
|
|
199
|
+
path: "./tests/external-contracts/combobox.listbox.contract.json",
|
|
200
|
+
strategyPath: "./tests/external-strategies/CustomComboboxStrategy.js",
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
contracts: [
|
|
205
|
+
{
|
|
206
|
+
src: "./tests/external-contracts/**/*.contract.mjs",
|
|
207
|
+
// optional: out: "./tests/external-contracts/generated"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
src: "./tests/client-a/**/*.contract.mjs",
|
|
211
|
+
out: "./tests/client-a/generated",
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Example `package.json` scripts:
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{
|
|
221
|
+
"scripts": {
|
|
222
|
+
"build:contracts": "npx aria-ease build contracts",
|
|
223
|
+
"test:a11y": "npx aria-ease build contracts && npx aria-ease test"
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
During build, Aria-Ease validates:
|
|
229
|
+
|
|
230
|
+
- contract schema shape
|
|
231
|
+
- selector references in static/dynamic targets
|
|
232
|
+
- relationship references (`aria-reference`, `contains`)
|
|
233
|
+
|
|
234
|
+
At runtime, relationship invariants are executed and reported alongside static assertions and dynamic tests.
|
|
235
|
+
|
|
179
236
|
**Perfect for CI/CD pipelines** to catch accessibility issues before production!
|
|
180
237
|
|
|
181
238
|
#### 🏅 Show Your Accessibility Commitment
|
|
@@ -951,6 +1008,11 @@ export default {
|
|
|
951
1008
|
out: "./accessibility-reports",
|
|
952
1009
|
},
|
|
953
1010
|
},
|
|
1011
|
+
contracts: [
|
|
1012
|
+
{
|
|
1013
|
+
src: "./tests/external-contracts/**/*.contract.mjs",
|
|
1014
|
+
},
|
|
1015
|
+
],
|
|
954
1016
|
};
|
|
955
1017
|
```
|
|
956
1018
|
|
|
@@ -960,7 +1022,7 @@ Add to `package.json`:
|
|
|
960
1022
|
{
|
|
961
1023
|
"scripts": {
|
|
962
1024
|
"audit": "npx aria-ease audit -f html",
|
|
963
|
-
"test:a11y": "npx aria-ease test",
|
|
1025
|
+
"test:a11y": "npx aria-ease build contracts && npx aria-ease test",
|
|
964
1026
|
"ci": "npm run audit && npm run test:a11y"
|
|
965
1027
|
}
|
|
966
1028
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
test_exports
|
|
3
|
+
} from "./chunk-PK5L2SAF.js";
|
|
4
|
+
import "./chunk-I2KLQ2HA.js";
|
|
5
|
+
|
|
6
|
+
// src/utils/test/src/component-strategies/AccordionComponentStrategy.ts
|
|
7
|
+
var AccordionComponentStrategy = class {
|
|
8
|
+
constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
|
|
9
|
+
this.mainSelector = mainSelector;
|
|
10
|
+
this.selectors = selectors;
|
|
11
|
+
this.actionTimeoutMs = actionTimeoutMs;
|
|
12
|
+
this.assertionTimeoutMs = assertionTimeoutMs;
|
|
13
|
+
}
|
|
14
|
+
async resetState(page) {
|
|
15
|
+
if (!this.selectors.panel || !this.selectors.trigger || this.selectors.popup) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const triggerSelector = this.selectors.trigger;
|
|
19
|
+
const panelSelector = this.selectors.panel;
|
|
20
|
+
if (!triggerSelector || !panelSelector) return;
|
|
21
|
+
const allTriggers = await page.locator(triggerSelector).all();
|
|
22
|
+
for (const trigger of allTriggers) {
|
|
23
|
+
const isExpanded = await trigger.getAttribute("aria-expanded") === "true";
|
|
24
|
+
const triggerPanel = await trigger.getAttribute("aria-controls");
|
|
25
|
+
if (isExpanded && triggerPanel) {
|
|
26
|
+
await trigger.click({ timeout: this.actionTimeoutMs });
|
|
27
|
+
const panel = page.locator(`#${triggerPanel}`);
|
|
28
|
+
await (0, test_exports.expect)(panel).toBeHidden({ timeout: this.assertionTimeoutMs }).catch(() => {
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async shouldSkipTest() {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
getMainSelector() {
|
|
37
|
+
return this.mainSelector;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
export {
|
|
41
|
+
AccordionComponentStrategy
|
|
42
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
test_exports
|
|
3
|
+
} from "./chunk-PK5L2SAF.js";
|
|
4
|
+
import "./chunk-I2KLQ2HA.js";
|
|
5
|
+
|
|
6
|
+
// src/utils/test/src/component-strategies/ComboboxComponentStrategy.ts
|
|
7
|
+
var ComboboxComponentStrategy = class {
|
|
8
|
+
constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
|
|
9
|
+
this.mainSelector = mainSelector;
|
|
10
|
+
this.selectors = selectors;
|
|
11
|
+
this.actionTimeoutMs = actionTimeoutMs;
|
|
12
|
+
this.assertionTimeoutMs = assertionTimeoutMs;
|
|
13
|
+
}
|
|
14
|
+
async resetState(page) {
|
|
15
|
+
if (!this.selectors.popup) return;
|
|
16
|
+
const popupSelector = this.selectors.popup;
|
|
17
|
+
const popupElement = page.locator(popupSelector).first();
|
|
18
|
+
const isPopupVisible = await popupElement.isVisible().catch(() => false);
|
|
19
|
+
if (!isPopupVisible) return;
|
|
20
|
+
let listBoxClosed = false;
|
|
21
|
+
let closeSelector = this.selectors.input;
|
|
22
|
+
if (!closeSelector && this.selectors.focusable) {
|
|
23
|
+
closeSelector = this.selectors.focusable;
|
|
24
|
+
} else if (!closeSelector) {
|
|
25
|
+
closeSelector = this.selectors.button;
|
|
26
|
+
}
|
|
27
|
+
if (closeSelector) {
|
|
28
|
+
const closeElement = page.locator(closeSelector).first();
|
|
29
|
+
await closeElement.focus();
|
|
30
|
+
await page.keyboard.press("Escape");
|
|
31
|
+
listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
32
|
+
}
|
|
33
|
+
if (!listBoxClosed && this.selectors.button) {
|
|
34
|
+
const buttonElement = page.locator(this.selectors.button).first();
|
|
35
|
+
await buttonElement.click({ timeout: this.actionTimeoutMs });
|
|
36
|
+
listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
37
|
+
}
|
|
38
|
+
if (!listBoxClosed) {
|
|
39
|
+
await page.mouse.click(10, 10);
|
|
40
|
+
listBoxClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
41
|
+
}
|
|
42
|
+
if (!listBoxClosed) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`\u274C FATAL: Cannot close combobox popup between tests. Popup remains visible after trying:
|
|
45
|
+
1. Escape key
|
|
46
|
+
2. Clicking button
|
|
47
|
+
3. Clicking outside
|
|
48
|
+
This indicates a problem with the combobox component's close functionality.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (this.selectors.input) {
|
|
52
|
+
await page.locator(this.selectors.input).first().clear();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async shouldSkipTest() {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
getMainSelector() {
|
|
59
|
+
return this.mainSelector;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
export {
|
|
63
|
+
ComboboxComponentStrategy
|
|
64
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
test_exports
|
|
3
|
+
} from "./chunk-PK5L2SAF.js";
|
|
4
|
+
import "./chunk-I2KLQ2HA.js";
|
|
5
|
+
|
|
6
|
+
// src/utils/test/src/component-strategies/MenuComponentStrategy.ts
|
|
7
|
+
var MenuComponentStrategy = class {
|
|
8
|
+
constructor(mainSelector, selectors, actionTimeoutMs = 400, assertionTimeoutMs = 400) {
|
|
9
|
+
this.mainSelector = mainSelector;
|
|
10
|
+
this.selectors = selectors;
|
|
11
|
+
this.actionTimeoutMs = actionTimeoutMs;
|
|
12
|
+
this.assertionTimeoutMs = assertionTimeoutMs;
|
|
13
|
+
}
|
|
14
|
+
async resetState(page) {
|
|
15
|
+
if (!this.selectors.popup) return;
|
|
16
|
+
const popupSelector = this.selectors.popup;
|
|
17
|
+
const popupElement = page.locator(popupSelector).first();
|
|
18
|
+
const isPopupVisible = await popupElement.isVisible().catch(() => false);
|
|
19
|
+
if (!isPopupVisible) return;
|
|
20
|
+
let menuClosed = false;
|
|
21
|
+
let closeSelector = this.selectors.input;
|
|
22
|
+
if (!closeSelector && this.selectors.focusable) {
|
|
23
|
+
closeSelector = this.selectors.focusable;
|
|
24
|
+
} else if (!closeSelector) {
|
|
25
|
+
closeSelector = this.selectors.trigger;
|
|
26
|
+
}
|
|
27
|
+
if (closeSelector) {
|
|
28
|
+
const closeElement = page.locator(closeSelector).first();
|
|
29
|
+
await closeElement.focus();
|
|
30
|
+
await page.keyboard.press("Escape");
|
|
31
|
+
menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
32
|
+
}
|
|
33
|
+
if (!menuClosed && this.selectors.trigger) {
|
|
34
|
+
const triggerElement = page.locator(this.selectors.trigger).first();
|
|
35
|
+
await triggerElement.click({ timeout: this.actionTimeoutMs });
|
|
36
|
+
menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
37
|
+
}
|
|
38
|
+
if (!menuClosed) {
|
|
39
|
+
await page.mouse.click(10, 10);
|
|
40
|
+
menuClosed = await (0, test_exports.expect)(popupElement).toBeHidden({ timeout: this.assertionTimeoutMs }).then(() => true).catch(() => false);
|
|
41
|
+
}
|
|
42
|
+
if (!menuClosed) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`\u274C FATAL: Cannot close menu between tests. Menu remains visible after trying:
|
|
45
|
+
1. Escape key
|
|
46
|
+
2. Clicking trigger
|
|
47
|
+
3. Clicking outside
|
|
48
|
+
This indicates a problem with the menu component's close functionality.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (this.selectors.input) {
|
|
52
|
+
await page.locator(this.selectors.input).first().clear();
|
|
53
|
+
}
|
|
54
|
+
if (this.selectors.trigger) {
|
|
55
|
+
const triggerElement = page.locator(this.selectors.trigger).first();
|
|
56
|
+
await triggerElement.focus();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async shouldSkipTest(test, page) {
|
|
60
|
+
const requiresSubmenu = test.action.some(
|
|
61
|
+
(act) => act.target === "submenu" || act.target === "submenuTrigger" || act.target === "submenuItems"
|
|
62
|
+
) || test.assertions.some(
|
|
63
|
+
(assertion) => assertion.target === "submenu" || assertion.target === "submenuTrigger" || assertion.target === "submenuItems"
|
|
64
|
+
);
|
|
65
|
+
if (!requiresSubmenu) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const submenuTriggerSelector = this.selectors.submenuTrigger;
|
|
69
|
+
if (!submenuTriggerSelector) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
const submenuTriggerCount = await page.locator(submenuTriggerSelector).count();
|
|
73
|
+
return submenuTriggerCount === 0;
|
|
74
|
+
}
|
|
75
|
+
getMainSelector() {
|
|
76
|
+
return this.mainSelector;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
export {
|
|
80
|
+
MenuComponentStrategy
|
|
81
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import "./chunk-I2KLQ2HA.js";
|
|
2
|
+
|
|
3
|
+
// src/utils/test/src/component-strategies/TabsComponentStrategy.ts
|
|
4
|
+
var TabsComponentStrategy = class {
|
|
5
|
+
constructor(mainSelector, selectors) {
|
|
6
|
+
this.mainSelector = mainSelector;
|
|
7
|
+
this.selectors = selectors;
|
|
8
|
+
}
|
|
9
|
+
async resetState() {
|
|
10
|
+
}
|
|
11
|
+
async shouldSkipTest(test, page) {
|
|
12
|
+
if (test.isVertical !== void 0 && this.selectors.tablist) {
|
|
13
|
+
const tablistSelector = this.selectors.tablist;
|
|
14
|
+
const tablist = page.locator(tablistSelector).first();
|
|
15
|
+
const orientation = await tablist.getAttribute("aria-orientation");
|
|
16
|
+
const isVertical = orientation === "vertical";
|
|
17
|
+
if (test.isVertical !== isVertical) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
getMainSelector() {
|
|
24
|
+
return this.mainSelector;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
export {
|
|
28
|
+
TabsComponentStrategy
|
|
29
|
+
};
|