aria-ease 6.7.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.
Files changed (39) hide show
  1. package/README.md +77 -10
  2. package/bin/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
  3. package/bin/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
  4. package/bin/MenuComponentStrategy-JAMTCSNF.js +81 -0
  5. package/bin/TabsComponentStrategy-3SQURPMX.js +29 -0
  6. package/bin/buildContracts-GBOY7UXG.js +437 -0
  7. package/bin/{chunk-VPBHLMAS.js → chunk-LMSKLN5O.js} +21 -0
  8. package/bin/chunk-PK5L2SAF.js +17 -0
  9. package/bin/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
  10. package/bin/cli.cjs +991 -128
  11. package/bin/cli.js +33 -2
  12. package/bin/{configLoader-XRF6VM4J.js → configLoader-Q6A4JLKW.js} +1 -1
  13. package/{dist/contractTestRunnerPlaywright-UAOFNS7Z.js → bin/contractTestRunnerPlaywright-ZZNWDUYP.js} +270 -219
  14. package/bin/{test-WRIJHN6H.js → test-OND56UUL.js} +97 -10
  15. package/dist/AccordionComponentStrategy-4ZEIQ2V6.js +42 -0
  16. package/dist/ComboboxComponentStrategy-OGRVZXAF.js +64 -0
  17. package/dist/MenuComponentStrategy-JAMTCSNF.js +81 -0
  18. package/dist/TabsComponentStrategy-3SQURPMX.js +29 -0
  19. package/dist/chunk-PK5L2SAF.js +17 -0
  20. package/dist/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
  21. package/dist/{configLoader-IT4PWCJB.js → configLoader-WTGJAP4Z.js} +21 -0
  22. package/{bin/contractTestRunnerPlaywright-UAOFNS7Z.js → dist/contractTestRunnerPlaywright-XBWJZMR3.js} +270 -219
  23. package/dist/index.cjs +800 -96
  24. package/dist/index.d.cts +136 -1
  25. package/dist/index.d.ts +136 -1
  26. package/dist/index.js +421 -16
  27. package/dist/src/utils/test/AccordionComponentStrategy-WRHZOEN6.js +38 -0
  28. package/dist/src/utils/test/ComboboxComponentStrategy-5AECQSRN.js +60 -0
  29. package/dist/src/utils/test/MenuComponentStrategy-VKZQYLBE.js +77 -0
  30. package/dist/src/utils/test/TabsComponentStrategy-BKG53SEV.js +26 -0
  31. package/dist/src/utils/test/aria-contracts/accordion/accordion.contract.json +5 -11
  32. package/dist/src/utils/test/aria-contracts/combobox/combobox.listbox.contract.json +1 -1
  33. package/dist/src/utils/test/aria-contracts/menu/menu.contract.json +1 -1
  34. package/dist/src/utils/test/{chunk-2TOYEY5L.js → chunk-XERMSYEH.js} +12 -3
  35. package/dist/src/utils/test/{configLoader-LD4RV2WQ.js → configLoader-YE2CYGDG.js} +21 -0
  36. package/dist/src/utils/test/{contractTestRunnerPlaywright-IRJOAEMT.js → contractTestRunnerPlaywright-LC5OAVXB.js} +262 -200
  37. package/dist/src/utils/test/index.cjs +472 -88
  38. package/dist/src/utils/test/index.js +97 -12
  39. 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 deterministic component behaviors → CI/CD gates deployment → Ship with confidence
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 that implement WAI-ARIA APG specifications. Tree-shakable, framework-agnostic, production-ready.
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 the WAI-ARIA APG into deterministic JSON "contracts" and built a custom Playwright runner with isolated test-harness architecture. Run it locally or in CI/CD.
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 manual keyboard testing across browsers. Now, it's automated, fast, and repeatable. You can boast about executing 26 combobox interaction assertions in ~2 seconds.
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
@@ -663,7 +720,7 @@ describe("Shopify User Menu Accessibility Test", () => {
663
720
 
664
721
  ### Strictness Modes
665
722
 
666
- Aria-Ease supports strictness policies to define how contract levels are enforced.
723
+ Aria-Ease supports strictness policies to define how APG specs are enforced.
667
724
 
668
725
  - `minimal`
669
726
  - `required` -> error
@@ -717,9 +774,14 @@ export default {
717
774
  Or override per test call:
718
775
 
719
776
  ```javascript
720
- await testUiComponent("menu", null, "http://localhost:5173/test-harness?component=menu", {
721
- strictness: "strict",
722
- });
777
+ await testUiComponent(
778
+ "menu",
779
+ null,
780
+ "http://localhost:5173/test-harness?component=menu",
781
+ {
782
+ strictness: "strict",
783
+ },
784
+ );
723
785
  ```
724
786
 
725
787
  ---
@@ -946,6 +1008,11 @@ export default {
946
1008
  out: "./accessibility-reports",
947
1009
  },
948
1010
  },
1011
+ contracts: [
1012
+ {
1013
+ src: "./tests/external-contracts/**/*.contract.mjs",
1014
+ },
1015
+ ],
949
1016
  };
950
1017
  ```
951
1018
 
@@ -955,7 +1022,7 @@ Add to `package.json`:
955
1022
  {
956
1023
  "scripts": {
957
1024
  "audit": "npx aria-ease audit -f html",
958
- "test:a11y": "npx aria-ease test",
1025
+ "test:a11y": "npx aria-ease build contracts && npx aria-ease test",
959
1026
  "ci": "npm run audit && npm run test:a11y"
960
1027
  }
961
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
+ };