aria-ease 6.2.2 → 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.
- package/README.md +91 -12
- package/bin/{chunk-7RMRFSJL.js → chunk-XLG3MIPQ.js} +5 -1
- package/bin/cli.cjs +52 -11
- package/bin/cli.js +1 -1
- package/bin/{contractTestRunnerPlaywright-ACAWN34W.js → contractTestRunnerPlaywright-JXQUUKFO.js} +48 -11
- package/bin/{test-A3ESFXOR.js → test-XSDP2NX3.js} +2 -2
- package/dist/{chunk-PDZQOXUN.js → chunk-RDEAG4KE.js} +5 -1
- package/dist/{contractTestRunnerPlaywright-O7FF7GV4.js → contractTestRunnerPlaywright-EUXD6ZZK.js} +48 -11
- package/dist/index.cjs +316 -69
- package/dist/index.d.cts +34 -4
- package/dist/index.d.ts +34 -4
- package/dist/index.js +265 -60
- package/dist/src/{Types.d-CRjhbrcw.d.cts → Types.d-DYfYR3Vc.d.cts} +18 -1
- package/dist/src/{Types.d-CRjhbrcw.d.ts → Types.d-DYfYR3Vc.d.ts} +18 -1
- package/dist/src/accordion/index.d.cts +2 -2
- package/dist/src/accordion/index.d.ts +2 -2
- package/dist/src/block/index.d.cts +1 -1
- package/dist/src/block/index.d.ts +1 -1
- package/dist/src/checkbox/index.cjs +0 -22
- package/dist/src/checkbox/index.d.cts +2 -2
- package/dist/src/checkbox/index.d.ts +2 -2
- package/dist/src/checkbox/index.js +0 -22
- package/dist/src/combobox/index.d.cts +1 -1
- package/dist/src/combobox/index.d.ts +1 -1
- package/dist/src/menu/index.d.cts +1 -1
- package/dist/src/menu/index.d.ts +1 -1
- package/dist/src/radio/index.cjs +0 -8
- package/dist/src/radio/index.d.cts +2 -2
- package/dist/src/radio/index.d.ts +2 -2
- package/dist/src/radio/index.js +0 -8
- package/dist/src/tabs/index.cjs +265 -0
- package/dist/src/tabs/index.d.cts +16 -0
- package/dist/src/tabs/index.d.ts +16 -0
- package/dist/src/tabs/index.js +263 -0
- package/dist/src/toggle/index.cjs +0 -28
- package/dist/src/toggle/index.d.cts +1 -1
- package/dist/src/toggle/index.d.ts +1 -1
- package/dist/src/toggle/index.js +0 -28
- package/dist/src/utils/test/{chunk-7RMRFSJL.js → chunk-XLG3MIPQ.js} +5 -1
- package/dist/src/utils/test/{contractTestRunnerPlaywright-7BPRTIN4.js → contractTestRunnerPlaywright-N77NEY25.js} +48 -11
- package/dist/src/utils/test/contracts/AccordionContract.json +18 -17
- package/dist/src/utils/test/contracts/ComboboxContract.json +32 -48
- package/dist/src/utils/test/contracts/MenuContract.json +19 -25
- package/dist/src/utils/test/contracts/TabsContract.json +348 -0
- package/dist/src/utils/test/index.cjs +52 -11
- package/dist/src/utils/test/index.js +2 -2
- 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
|
|
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
|
|
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://
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
792
|
-
|
|
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
|
-
|
|
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-
|
|
210
|
+
const { runTest } = await import("./test-XSDP2NX3.js");
|
|
211
211
|
runTest();
|
|
212
212
|
});
|
|
213
213
|
program.command("help").description("Display help information").action(() => {
|
package/bin/{contractTestRunnerPlaywright-ACAWN34W.js → contractTestRunnerPlaywright-JXQUUKFO.js}
RENAMED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
ContractReporter,
|
|
3
3
|
contract_default,
|
|
4
4
|
createTestPage
|
|
5
|
-
} from "./chunk-
|
|
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
|
|
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
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
|
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}`);
|
package/dist/{contractTestRunnerPlaywright-O7FF7GV4.js → contractTestRunnerPlaywright-EUXD6ZZK.js}
RENAMED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
__reExport,
|
|
5
5
|
contract_default,
|
|
6
6
|
createTestPage
|
|
7
|
-
} from "./chunk-
|
|
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
|
|
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
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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.`);
|