aria-ease 6.2.3 → 6.3.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 (41) hide show
  1. package/README.md +91 -12
  2. package/bin/{chunk-7RMRFSJL.js → chunk-XLG3MIPQ.js} +5 -1
  3. package/bin/cli.cjs +52 -11
  4. package/bin/cli.js +1 -1
  5. package/bin/{contractTestRunnerPlaywright-ACAWN34W.js → contractTestRunnerPlaywright-JXQUUKFO.js} +48 -11
  6. package/bin/{test-A3ESFXOR.js → test-XSDP2NX3.js} +2 -2
  7. package/dist/{chunk-PDZQOXUN.js → chunk-RDEAG4KE.js} +5 -1
  8. package/dist/{contractTestRunnerPlaywright-O7FF7GV4.js → contractTestRunnerPlaywright-EUXD6ZZK.js} +48 -11
  9. package/dist/index.cjs +316 -11
  10. package/dist/index.d.cts +34 -4
  11. package/dist/index.d.ts +34 -4
  12. package/dist/index.js +265 -2
  13. package/dist/src/{Types.d-CRjhbrcw.d.cts → Types.d-DYfYR3Vc.d.cts} +18 -1
  14. package/dist/src/{Types.d-CRjhbrcw.d.ts → Types.d-DYfYR3Vc.d.ts} +18 -1
  15. package/dist/src/accordion/index.d.cts +2 -2
  16. package/dist/src/accordion/index.d.ts +2 -2
  17. package/dist/src/block/index.d.cts +1 -1
  18. package/dist/src/block/index.d.ts +1 -1
  19. package/dist/src/checkbox/index.d.cts +2 -2
  20. package/dist/src/checkbox/index.d.ts +2 -2
  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.d.cts +1 -1
  24. package/dist/src/menu/index.d.ts +1 -1
  25. package/dist/src/radio/index.d.cts +2 -2
  26. package/dist/src/radio/index.d.ts +2 -2
  27. package/dist/src/tabs/index.cjs +265 -0
  28. package/dist/src/tabs/index.d.cts +16 -0
  29. package/dist/src/tabs/index.d.ts +16 -0
  30. package/dist/src/tabs/index.js +263 -0
  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/{chunk-7RMRFSJL.js → chunk-XLG3MIPQ.js} +5 -1
  34. package/dist/src/utils/test/{contractTestRunnerPlaywright-7BPRTIN4.js → contractTestRunnerPlaywright-N77NEY25.js} +48 -11
  35. package/dist/src/utils/test/contracts/AccordionContract.json +18 -17
  36. package/dist/src/utils/test/contracts/ComboboxContract.json +32 -48
  37. package/dist/src/utils/test/contracts/MenuContract.json +19 -25
  38. package/dist/src/utils/test/contracts/TabsContract.json +348 -0
  39. package/dist/src/utils/test/index.cjs +52 -11
  40. package/dist/src/utils/test/index.js +2 -2
  41. package/package.json +8 -3
package/README.md CHANGED
@@ -11,7 +11,7 @@ Out of the box accessibility utility package to develop production ready applica
11
11
 
12
12
  - 🎯 **Tree-shakable** - Import only what you need (1.4KB - 3.7KB per component)
13
13
  - ♿ **WCAG Compliant** - Follows WAI-ARIA best practices
14
- - ⌨️ **Keyboard Navigation** - Full keyboard support out of the box
14
+ - ⌨️ **Keyboard Interaction** - Full keyboard support out of the box
15
15
  - 🧪 **Contract Testing** - Built-in accessibility testing framework
16
16
  - 🎭 **Framework Agnostic** - Works with React, Vue, vanilla JS, etc.
17
17
  - 🔍 **CLI Audit Tool** - Automated accessibility testing for your sites
@@ -82,7 +82,7 @@ The CLI will automatically find and load your config file, with validation to ca
82
82
 
83
83
  ### 🍔 Menu (Dropdowns)
84
84
 
85
- Creates accessible menus with focus trapping and keyboard navigation. Works for dropdowns that toggles display with interactive items.
85
+ Creates accessible menus with aria attribute management, focus trapping, and keyboard interaction. Works for dropdowns that toggles display with interactive items.
86
86
 
87
87
  **Features:**
88
88
 
@@ -138,13 +138,97 @@ menu.refresh();
138
138
 
139
139
  ## 🎮 Live Demo
140
140
 
141
- - [Menu Component](https://codesandbox.io/p/sandbox/szsclq) - Dropdown with keyboard navigation
141
+ - [Menu Component](https://codepen.io/aria-ease/pen/PwGqdzp) - Menu dropdown with keyboard interaction
142
+
143
+ ---
144
+
145
+ ### 🍔 Combobox (Listbox)
146
+
147
+ Creates accessible listbox combobox with aria attribute management, focus trapping, and keyboard interaction.
148
+
149
+ **Features:**
150
+
151
+ - Arrow key navigation
152
+ - Escape key closes listbox
153
+ - ARIA attribute management (aria-expanded, aria-activedescendant, aria-controls, roles)
154
+ - Focus management with aria-activedescendant pattern
155
+
156
+ ```javascript
157
+ import * as Combobox from "aria-ease/combobox";
158
+
159
+ // React Example
160
+ useEffect(() => {
161
+ comboboxRef.current = Combobox.makeComboboxAccessible({
162
+ comboboxInputId: "search-input",
163
+ listBoxId: "suggestions-list",
164
+ listBoxItemsClass: "suggestion-item",
165
+ });
166
+
167
+ return () => {
168
+ if (comboboxRef.current) {
169
+ comboboxRef.current.cleanup();
170
+ }
171
+ };
172
+ }, []);
173
+
174
+ // Programmatically control
175
+ comboboxRef.current.openListbox(); // Open the listbox
176
+ comboboxRef.current.refresh(); // Refresh the cache after dynamically adding/removing a listbox option item
177
+
178
+ // Vanilla JS Example
179
+ const combobox = Combobox.makeComboboxAccessible({
180
+ comboboxInputId: "fruit",
181
+ comboboxButtonId: "show-list-button",
182
+ listBoxId: "fruits-listbox",
183
+ listBoxItemsClass: "list-option",
184
+ callback: {
185
+ onSelect: (option) => {
186
+ input.value = option.textContent;
187
+ // Show all options after selection
188
+ options.forEach((opt) => (opt.hidden = false));
189
+ },
190
+ onOpenChange: (isOpen) => {
191
+ console.log("Listbox is", isOpen ? "open" : "closed");
192
+ },
193
+ },
194
+ });
195
+
196
+ // Programmatically control
197
+ combobox.openListbox();
198
+ combobox.closeListbox();
199
+
200
+ // If you dynamically add/remove listbox option items, refresh the cache
201
+ combobox.refresh();
202
+ ```
203
+
204
+ **Required HTML structure:**
205
+
206
+ ```html
207
+ <div id="combo-wrapper">
208
+ <label for="fruit">Select a fruit</label>
209
+ <div class="input-wrapper">
210
+ <input type="text" id="fruit" placeholder="Search fruits..." />
211
+ <button id="show-list-button" tabindex="-1">▼</button>
212
+ </div>
213
+
214
+ <ul id="fruits-listbox">
215
+ <li id="apple" class="list-option">Apple</li>
216
+ <li id="mango" class="list-option">Mango</li>
217
+ <li id="orange" class="list-option">Orange</li>
218
+ <li id="banana" class="list-option">Banana</li>
219
+ </ul>
220
+ </div>
221
+ ```
222
+
223
+ ## 🎮 Live Demo
224
+
225
+ - [Combobox Component](https://codepen.io/aria-ease/pen/ByLNqOE) - Listbox combobox with keyboard interaction
142
226
 
143
227
  ---
144
228
 
145
229
  ### 🪗 Accordion
146
230
 
147
- Creates accessible accordions with keyboard navigation and automatic state management.
231
+ Creates accessible accordions with keyboard interaction and automatic state management.
148
232
 
149
233
  **Features:**
150
234
 
@@ -212,14 +296,12 @@ makeAccordionAccessible({
212
296
 
213
297
  ### ✅ Checkbox
214
298
 
215
- Creates accessible checkbox groups with keyboard navigation and state management.
299
+ Creates accessible checkbox groups with keyboard interaction and state management.
216
300
 
217
301
  **Features:**
218
302
 
219
- - Arrow key navigation
220
303
  - Space to toggle
221
304
  - Independent state tracking
222
- - Home/End key support
223
305
  - Query checked states
224
306
 
225
307
  ```javascript
@@ -270,14 +352,13 @@ makeCheckboxAccessible({
270
352
 
271
353
  ### 🔘 Radio Button
272
354
 
273
- Creates accessible radio groups with keyboard navigation and automatic selection management.
355
+ Creates accessible radio groups with keyboard interaction and automatic selection management.
274
356
 
275
357
  **Features:**
276
358
 
277
359
  - Arrow key navigation (all directions)
278
360
  - Automatic unchecking of other radios
279
361
  - Space to select
280
- - Home/End key support
281
362
  - Single selection enforcement
282
363
 
283
364
  ```javascript
@@ -334,8 +415,6 @@ Creates accessible toggle buttons with keyboard interactions and state managemen
334
415
 
335
416
  - Enter/Space to toggle
336
417
  - Single toggle or toggle groups
337
- - Arrow key navigation (groups)
338
- - Home/End support (groups)
339
418
  - Query pressed states
340
419
 
341
420
  ```javascript
@@ -492,7 +571,7 @@ useEffect(() => {
492
571
 
493
572
  ## 🎨 Focus Styling
494
573
 
495
- Aria-Ease handles ARIA attributes and keyboard navigation, but **you must provide visible focus styles**:
574
+ Aria-Ease handles ARIA attributes and keyboard interaction, but **you must provide visible focus styles**:
496
575
 
497
576
  ```css
498
577
  :focus {
@@ -11,6 +11,10 @@ var contract_default = {
11
11
  accordion: {
12
12
  path: "./contracts/AccordionContract.json",
13
13
  component: "accordion"
14
+ },
15
+ tabs: {
16
+ path: "./contracts/TabsContract.json",
17
+ component: "tabs"
14
18
  }
15
19
  };
16
20
 
@@ -125,7 +129,7 @@ ${"\u2500".repeat(60)}`);
125
129
  this.log(`\u{1F4A1} Optional Enhancements (${suggestions.length}):
126
130
  `);
127
131
  this.log(`These features are optional per APG guidelines but recommended`);
128
- this.log(`for improved user experience and keyboard navigation:
132
+ this.log(`for improved user experience and keyboard interaction:
129
133
  `);
130
134
  suggestions.forEach((test, index) => {
131
135
  this.log(`${index + 1}. ${test.description}`);
package/bin/cli.cjs CHANGED
@@ -299,6 +299,10 @@ var init_contract = __esm({
299
299
  accordion: {
300
300
  path: "./contracts/AccordionContract.json",
301
301
  component: "accordion"
302
+ },
303
+ tabs: {
304
+ path: "./contracts/TabsContract.json",
305
+ component: "tabs"
302
306
  }
303
307
  };
304
308
  }
@@ -419,7 +423,7 @@ ${"\u2500".repeat(60)}`);
419
423
  this.log(`\u{1F4A1} Optional Enhancements (${suggestions.length}):
420
424
  `);
421
425
  this.log(`These features are optional per APG guidelines but recommended`);
422
- this.log(`for improved user experience and keyboard navigation:
426
+ this.log(`for improved user experience and keyboard interaction:
423
427
  `);
424
428
  suggestions.forEach((test, index) => {
425
429
  this.log(`${index + 1}. ${test.description}`);
@@ -708,9 +712,9 @@ async function runContractTestsPlaywright(componentName, url) {
708
712
  }
709
713
  await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
710
714
  }
711
- const mainSelector = componentContract.selectors.trigger || componentContract.selectors.input || componentContract.selectors.container;
715
+ const mainSelector = componentContract.selectors.trigger || componentContract.selectors.input || componentContract.selectors.container || componentContract.selectors.tablist || componentContract.selectors.tab;
712
716
  if (!mainSelector) {
713
- throw new Error(`CRITICAL: No main selector (trigger, input, or container) found in contract for ${componentName}`);
717
+ throw new Error(`CRITICAL: No main selector (trigger, input, container, tablist, or tab) found in contract for ${componentName}`);
714
718
  }
715
719
  try {
716
720
  await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: 3e4 });
@@ -772,28 +776,52 @@ async function runContractTestsPlaywright(componentName, url) {
772
776
  failures.push(`Target ${test.target} not found.`);
773
777
  continue;
774
778
  }
779
+ const isRedundantCheck = (selector, attrName, expectedVal) => {
780
+ const attrPattern = new RegExp(`\\[${attrName}(?:=["']?([^\\]"']+)["']?)?\\]`);
781
+ const match = selector.match(attrPattern);
782
+ if (!match) return false;
783
+ if (!expectedVal) return true;
784
+ const selectorValue = match[1];
785
+ if (selectorValue) {
786
+ const expectedValues = expectedVal.split(" | ");
787
+ return expectedValues.includes(selectorValue);
788
+ }
789
+ return false;
790
+ };
775
791
  if (!test.expectedValue) {
776
792
  const attributes = test.attribute.split(" | ");
777
793
  let hasAny = false;
794
+ let allRedundant = true;
778
795
  for (const attr of attributes) {
779
- const value = await target.getAttribute(attr.trim());
796
+ const attrTrimmed = attr.trim();
797
+ if (isRedundantCheck(targetSelector, attrTrimmed)) {
798
+ passes.push(`${attrTrimmed} on ${test.target} verified by selector (already present in: ${targetSelector}).`);
799
+ hasAny = true;
800
+ continue;
801
+ }
802
+ allRedundant = false;
803
+ const value = await target.getAttribute(attrTrimmed);
780
804
  if (value !== null) {
781
805
  hasAny = true;
782
806
  break;
783
807
  }
784
808
  }
785
- if (!hasAny) {
809
+ if (!hasAny && !allRedundant) {
786
810
  failures.push(test.failureMessage + ` None of the attributes "${test.attribute}" are present.`);
787
- } else {
811
+ } else if (!allRedundant && hasAny) {
788
812
  passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
789
813
  }
790
814
  } else {
791
- const attributeValue = await target.getAttribute(test.attribute);
792
- const expectedValues = test.expectedValue.split(" | ");
793
- if (!attributeValue || !expectedValues.includes(attributeValue)) {
794
- failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
815
+ if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
816
+ passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
795
817
  } else {
796
- passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
818
+ const attributeValue = await target.getAttribute(test.attribute);
819
+ const expectedValues = test.expectedValue.split(" | ");
820
+ if (!attributeValue || !expectedValues.includes(attributeValue)) {
821
+ failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
822
+ } else {
823
+ passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
824
+ }
797
825
  }
798
826
  }
799
827
  }
@@ -902,6 +930,19 @@ This indicates a problem with the menu component's close functionality.`
902
930
  if (shouldSkipTest) {
903
931
  continue;
904
932
  }
933
+ if (componentContract.selectors.panel && componentContract.selectors.tab && componentContract.selectors.tablist) {
934
+ if (dynamicTest.isVertical !== void 0 && componentContract.selectors.tablist) {
935
+ const tablistSelector = componentContract.selectors.tablist;
936
+ const tablist = page.locator(tablistSelector).first();
937
+ const orientation = await tablist.getAttribute("aria-orientation");
938
+ const isVertical = orientation === "vertical";
939
+ if (dynamicTest.isVertical !== isVertical) {
940
+ const skipReason = dynamicTest.isVertical ? `Skipping vertical tabs test - component has horizontal orientation` : `Skipping horizontal tabs test - component has vertical orientation`;
941
+ reporter.reportTest(dynamicTest, "skip", skipReason);
942
+ continue;
943
+ }
944
+ }
945
+ }
905
946
  for (const act of action) {
906
947
  if (!page || page.isClosed()) {
907
948
  failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
package/bin/cli.js CHANGED
@@ -207,7 +207,7 @@ program.command("audit").description("Run axe-core powered accessibility audit o
207
207
  console.log(chalk.green("\n\u{1F389} All audits completed."));
208
208
  });
209
209
  program.command("test").description("Run core a11y accessibility standard tests on UI components").action(async () => {
210
- const { runTest } = await import("./test-A3ESFXOR.js");
210
+ const { runTest } = await import("./test-XSDP2NX3.js");
211
211
  runTest();
212
212
  });
213
213
  program.command("help").description("Display help information").action(() => {
@@ -2,7 +2,7 @@ import {
2
2
  ContractReporter,
3
3
  contract_default,
4
4
  createTestPage
5
- } from "./chunk-7RMRFSJL.js";
5
+ } from "./chunk-XLG3MIPQ.js";
6
6
  import {
7
7
  __export,
8
8
  __reExport
@@ -56,9 +56,9 @@ async function runContractTestsPlaywright(componentName, url) {
56
56
  }
57
57
  await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
58
58
  }
59
- const mainSelector = componentContract.selectors.trigger || componentContract.selectors.input || componentContract.selectors.container;
59
+ const mainSelector = componentContract.selectors.trigger || componentContract.selectors.input || componentContract.selectors.container || componentContract.selectors.tablist || componentContract.selectors.tab;
60
60
  if (!mainSelector) {
61
- throw new Error(`CRITICAL: No main selector (trigger, input, or container) found in contract for ${componentName}`);
61
+ throw new Error(`CRITICAL: No main selector (trigger, input, container, tablist, or tab) found in contract for ${componentName}`);
62
62
  }
63
63
  try {
64
64
  await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: 3e4 });
@@ -120,28 +120,52 @@ async function runContractTestsPlaywright(componentName, url) {
120
120
  failures.push(`Target ${test.target} not found.`);
121
121
  continue;
122
122
  }
123
+ const isRedundantCheck = (selector, attrName, expectedVal) => {
124
+ const attrPattern = new RegExp(`\\[${attrName}(?:=["']?([^\\]"']+)["']?)?\\]`);
125
+ const match = selector.match(attrPattern);
126
+ if (!match) return false;
127
+ if (!expectedVal) return true;
128
+ const selectorValue = match[1];
129
+ if (selectorValue) {
130
+ const expectedValues = expectedVal.split(" | ");
131
+ return expectedValues.includes(selectorValue);
132
+ }
133
+ return false;
134
+ };
123
135
  if (!test.expectedValue) {
124
136
  const attributes = test.attribute.split(" | ");
125
137
  let hasAny = false;
138
+ let allRedundant = true;
126
139
  for (const attr of attributes) {
127
- const value = await target.getAttribute(attr.trim());
140
+ const attrTrimmed = attr.trim();
141
+ if (isRedundantCheck(targetSelector, attrTrimmed)) {
142
+ passes.push(`${attrTrimmed} on ${test.target} verified by selector (already present in: ${targetSelector}).`);
143
+ hasAny = true;
144
+ continue;
145
+ }
146
+ allRedundant = false;
147
+ const value = await target.getAttribute(attrTrimmed);
128
148
  if (value !== null) {
129
149
  hasAny = true;
130
150
  break;
131
151
  }
132
152
  }
133
- if (!hasAny) {
153
+ if (!hasAny && !allRedundant) {
134
154
  failures.push(test.failureMessage + ` None of the attributes "${test.attribute}" are present.`);
135
- } else {
155
+ } else if (!allRedundant && hasAny) {
136
156
  passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
137
157
  }
138
158
  } else {
139
- const attributeValue = await target.getAttribute(test.attribute);
140
- const expectedValues = test.expectedValue.split(" | ");
141
- if (!attributeValue || !expectedValues.includes(attributeValue)) {
142
- failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
159
+ if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
160
+ passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
143
161
  } else {
144
- passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
162
+ const attributeValue = await target.getAttribute(test.attribute);
163
+ const expectedValues = test.expectedValue.split(" | ");
164
+ if (!attributeValue || !expectedValues.includes(attributeValue)) {
165
+ failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
166
+ } else {
167
+ passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
168
+ }
145
169
  }
146
170
  }
147
171
  }
@@ -250,6 +274,19 @@ This indicates a problem with the menu component's close functionality.`
250
274
  if (shouldSkipTest) {
251
275
  continue;
252
276
  }
277
+ if (componentContract.selectors.panel && componentContract.selectors.tab && componentContract.selectors.tablist) {
278
+ if (dynamicTest.isVertical !== void 0 && componentContract.selectors.tablist) {
279
+ const tablistSelector = componentContract.selectors.tablist;
280
+ const tablist = page.locator(tablistSelector).first();
281
+ const orientation = await tablist.getAttribute("aria-orientation");
282
+ const isVertical = orientation === "vertical";
283
+ if (dynamicTest.isVertical !== isVertical) {
284
+ const skipReason = dynamicTest.isVertical ? `Skipping vertical tabs test - component has horizontal orientation` : `Skipping horizontal tabs test - component has vertical orientation`;
285
+ reporter.reportTest(dynamicTest, "skip", skipReason);
286
+ continue;
287
+ }
288
+ }
289
+ }
253
290
  for (const act of action) {
254
291
  if (!page || page.isClosed()) {
255
292
  failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
@@ -2,7 +2,7 @@ import {
2
2
  ContractReporter,
3
3
  closeSharedBrowser,
4
4
  contract_default
5
- } from "./chunk-7RMRFSJL.js";
5
+ } from "./chunk-XLG3MIPQ.js";
6
6
  import "./chunk-I2KLQ2HA.js";
7
7
 
8
8
  // src/utils/test/src/test.ts
@@ -114,7 +114,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
114
114
  const devServerUrl = await checkDevServer(url);
115
115
  if (devServerUrl) {
116
116
  console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
117
- const { runContractTestsPlaywright } = await import("./contractTestRunnerPlaywright-ACAWN34W.js");
117
+ const { runContractTestsPlaywright } = await import("./contractTestRunnerPlaywright-JXQUUKFO.js");
118
118
  contract = await runContractTestsPlaywright(componentName, devServerUrl);
119
119
  } else {
120
120
  throw new Error(
@@ -29,6 +29,10 @@ var contract_default = {
29
29
  accordion: {
30
30
  path: "./contracts/AccordionContract.json",
31
31
  component: "accordion"
32
+ },
33
+ tabs: {
34
+ path: "./contracts/TabsContract.json",
35
+ component: "tabs"
32
36
  }
33
37
  };
34
38
 
@@ -143,7 +147,7 @@ ${"\u2500".repeat(60)}`);
143
147
  this.log(`\u{1F4A1} Optional Enhancements (${suggestions.length}):
144
148
  `);
145
149
  this.log(`These features are optional per APG guidelines but recommended`);
146
- this.log(`for improved user experience and keyboard navigation:
150
+ this.log(`for improved user experience and keyboard interaction:
147
151
  `);
148
152
  suggestions.forEach((test, index) => {
149
153
  this.log(`${index + 1}. ${test.description}`);
@@ -4,7 +4,7 @@ import {
4
4
  __reExport,
5
5
  contract_default,
6
6
  createTestPage
7
- } from "./chunk-PDZQOXUN.js";
7
+ } from "./chunk-RDEAG4KE.js";
8
8
 
9
9
  // node_modules/@playwright/test/index.mjs
10
10
  var test_exports = {};
@@ -54,9 +54,9 @@ async function runContractTestsPlaywright(componentName, url) {
54
54
  }
55
55
  await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
56
56
  }
57
- const mainSelector = componentContract.selectors.trigger || componentContract.selectors.input || componentContract.selectors.container;
57
+ const mainSelector = componentContract.selectors.trigger || componentContract.selectors.input || componentContract.selectors.container || componentContract.selectors.tablist || componentContract.selectors.tab;
58
58
  if (!mainSelector) {
59
- throw new Error(`CRITICAL: No main selector (trigger, input, or container) found in contract for ${componentName}`);
59
+ throw new Error(`CRITICAL: No main selector (trigger, input, container, tablist, or tab) found in contract for ${componentName}`);
60
60
  }
61
61
  try {
62
62
  await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: 3e4 });
@@ -118,28 +118,52 @@ async function runContractTestsPlaywright(componentName, url) {
118
118
  failures.push(`Target ${test.target} not found.`);
119
119
  continue;
120
120
  }
121
+ const isRedundantCheck = (selector, attrName, expectedVal) => {
122
+ const attrPattern = new RegExp(`\\[${attrName}(?:=["']?([^\\]"']+)["']?)?\\]`);
123
+ const match = selector.match(attrPattern);
124
+ if (!match) return false;
125
+ if (!expectedVal) return true;
126
+ const selectorValue = match[1];
127
+ if (selectorValue) {
128
+ const expectedValues = expectedVal.split(" | ");
129
+ return expectedValues.includes(selectorValue);
130
+ }
131
+ return false;
132
+ };
121
133
  if (!test.expectedValue) {
122
134
  const attributes = test.attribute.split(" | ");
123
135
  let hasAny = false;
136
+ let allRedundant = true;
124
137
  for (const attr of attributes) {
125
- const value = await target.getAttribute(attr.trim());
138
+ const attrTrimmed = attr.trim();
139
+ if (isRedundantCheck(targetSelector, attrTrimmed)) {
140
+ passes.push(`${attrTrimmed} on ${test.target} verified by selector (already present in: ${targetSelector}).`);
141
+ hasAny = true;
142
+ continue;
143
+ }
144
+ allRedundant = false;
145
+ const value = await target.getAttribute(attrTrimmed);
126
146
  if (value !== null) {
127
147
  hasAny = true;
128
148
  break;
129
149
  }
130
150
  }
131
- if (!hasAny) {
151
+ if (!hasAny && !allRedundant) {
132
152
  failures.push(test.failureMessage + ` None of the attributes "${test.attribute}" are present.`);
133
- } else {
153
+ } else if (!allRedundant && hasAny) {
134
154
  passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
135
155
  }
136
156
  } else {
137
- const attributeValue = await target.getAttribute(test.attribute);
138
- const expectedValues = test.expectedValue.split(" | ");
139
- if (!attributeValue || !expectedValues.includes(attributeValue)) {
140
- failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
157
+ if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
158
+ passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
141
159
  } else {
142
- passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
160
+ const attributeValue = await target.getAttribute(test.attribute);
161
+ const expectedValues = test.expectedValue.split(" | ");
162
+ if (!attributeValue || !expectedValues.includes(attributeValue)) {
163
+ failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
164
+ } else {
165
+ passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
166
+ }
143
167
  }
144
168
  }
145
169
  }
@@ -248,6 +272,19 @@ This indicates a problem with the menu component's close functionality.`
248
272
  if (shouldSkipTest) {
249
273
  continue;
250
274
  }
275
+ if (componentContract.selectors.panel && componentContract.selectors.tab && componentContract.selectors.tablist) {
276
+ if (dynamicTest.isVertical !== void 0 && componentContract.selectors.tablist) {
277
+ const tablistSelector = componentContract.selectors.tablist;
278
+ const tablist = page.locator(tablistSelector).first();
279
+ const orientation = await tablist.getAttribute("aria-orientation");
280
+ const isVertical = orientation === "vertical";
281
+ if (dynamicTest.isVertical !== isVertical) {
282
+ const skipReason = dynamicTest.isVertical ? `Skipping vertical tabs test - component has horizontal orientation` : `Skipping horizontal tabs test - component has vertical orientation`;
283
+ reporter.reportTest(dynamicTest, "skip", skipReason);
284
+ continue;
285
+ }
286
+ }
287
+ }
251
288
  for (const act of action) {
252
289
  if (!page || page.isClosed()) {
253
290
  failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);