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
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
"meta": {
|
|
3
3
|
"id": "aria-ease.menu",
|
|
4
4
|
"version": "1.0.0",
|
|
5
|
-
"
|
|
5
|
+
"created": "11-02-2026",
|
|
6
|
+
"lastUpdated": "28-02-2026",
|
|
6
7
|
"description": "ARIA Menu interaction contract. Validates the ARIA and interaction contract for a custom menu component following the ARIA Authoring Practices Guide menu with popup pattern.",
|
|
7
8
|
"source": {
|
|
8
9
|
"apg": "https://www.w3.org/WAI/ARIA/apg/patterns/menubar/",
|
|
@@ -115,7 +116,7 @@
|
|
|
115
116
|
]
|
|
116
117
|
},
|
|
117
118
|
{
|
|
118
|
-
"description": "Pressing Enter
|
|
119
|
+
"description": "Pressing Enter on trigger opens the menu and focuses first item.",
|
|
119
120
|
"action": [
|
|
120
121
|
{ "type": "keypress", "target": "trigger", "key": "Enter" }
|
|
121
122
|
],
|
|
@@ -130,7 +131,7 @@
|
|
|
130
131
|
"assertion": "toHaveAttribute",
|
|
131
132
|
"attribute": "aria-expanded",
|
|
132
133
|
"expectedValue": "true",
|
|
133
|
-
"failureMessage": "Trigger's aria-expanded
|
|
134
|
+
"failureMessage": "Trigger's should have aria-expanded=true after pressing Enter."
|
|
134
135
|
},
|
|
135
136
|
{
|
|
136
137
|
"target": "relative",
|
|
@@ -141,7 +142,7 @@
|
|
|
141
142
|
]
|
|
142
143
|
},
|
|
143
144
|
{
|
|
144
|
-
"description": "Pressing Space
|
|
145
|
+
"description": "Pressing Space on trigger opens the menu and focuses first item.",
|
|
145
146
|
"action": [
|
|
146
147
|
{ "type": "keypress", "target": "trigger", "key": "Space" }
|
|
147
148
|
],
|
|
@@ -156,7 +157,7 @@
|
|
|
156
157
|
"assertion": "toHaveAttribute",
|
|
157
158
|
"attribute": "aria-expanded",
|
|
158
159
|
"expectedValue": "true",
|
|
159
|
-
"failureMessage": "Trigger's aria-expanded
|
|
160
|
+
"failureMessage": "Trigger's should have aria-expanded=true after pressing Space."
|
|
160
161
|
},
|
|
161
162
|
{
|
|
162
163
|
"target": "relative",
|
|
@@ -167,8 +168,7 @@
|
|
|
167
168
|
]
|
|
168
169
|
},
|
|
169
170
|
{
|
|
170
|
-
"description": "
|
|
171
|
-
"requiresBrowser": true,
|
|
171
|
+
"description": "Down Arrow moves focus to next menu item.",
|
|
172
172
|
"action": [
|
|
173
173
|
{ "type": "click", "target": "trigger" },
|
|
174
174
|
{ "type": "keypress", "target": "focusable", "key": "ArrowDown" }
|
|
@@ -178,13 +178,12 @@
|
|
|
178
178
|
"target": "relative",
|
|
179
179
|
"assertion": "toHaveFocus",
|
|
180
180
|
"expectedValue": "second",
|
|
181
|
-
"failureMessage": "Second menu item should have focus after pressing Arrow
|
|
181
|
+
"failureMessage": "Second menu item should have focus after pressing Down Arrow."
|
|
182
182
|
}
|
|
183
183
|
]
|
|
184
184
|
},
|
|
185
185
|
{
|
|
186
|
-
"description": "
|
|
187
|
-
"requiresBrowser": true,
|
|
186
|
+
"description": "Up Arrow moves focus to previous menu item.",
|
|
188
187
|
"action": [
|
|
189
188
|
{ "type": "click", "target": "trigger" },
|
|
190
189
|
{ "type": "keypress", "target": "focusable", "key": "ArrowDown" },
|
|
@@ -195,13 +194,12 @@
|
|
|
195
194
|
"target": "relative",
|
|
196
195
|
"assertion": "toHaveFocus",
|
|
197
196
|
"expectedValue": "first",
|
|
198
|
-
"failureMessage": "First menu item should have focus after Arrow
|
|
197
|
+
"failureMessage": "First menu item should have focus after Down Arrow then Up Arrow."
|
|
199
198
|
}
|
|
200
199
|
]
|
|
201
200
|
},
|
|
202
201
|
{
|
|
203
|
-
"description": "
|
|
204
|
-
"requiresBrowser": true,
|
|
202
|
+
"description": "Right Arrow moves focus to next menu item.",
|
|
205
203
|
"action": [
|
|
206
204
|
{ "type": "click", "target": "trigger" },
|
|
207
205
|
{ "type": "keypress", "target": "focusable", "key": "ArrowRight" }
|
|
@@ -211,13 +209,12 @@
|
|
|
211
209
|
"target": "relative",
|
|
212
210
|
"assertion": "toHaveFocus",
|
|
213
211
|
"expectedValue": "second",
|
|
214
|
-
"failureMessage": "Second menu item should have focus after pressing Arrow
|
|
212
|
+
"failureMessage": "Second menu item should have focus after pressing Right Arrow."
|
|
215
213
|
}
|
|
216
214
|
]
|
|
217
215
|
},
|
|
218
216
|
{
|
|
219
|
-
"description": "
|
|
220
|
-
"requiresBrowser": true,
|
|
217
|
+
"description": "Left Arrow moves focus to previous menu item.",
|
|
221
218
|
"action": [
|
|
222
219
|
{ "type": "click", "target": "trigger" },
|
|
223
220
|
{ "type": "keypress", "target": "focusable", "key": "ArrowRight" },
|
|
@@ -228,12 +225,12 @@
|
|
|
228
225
|
"target": "relative",
|
|
229
226
|
"assertion": "toHaveFocus",
|
|
230
227
|
"expectedValue": "first",
|
|
231
|
-
"failureMessage": "First menu item should have focus after Arrow
|
|
228
|
+
"failureMessage": "First menu item should have focus after Right Arrow then Left Arrow."
|
|
232
229
|
}
|
|
233
230
|
]
|
|
234
231
|
},
|
|
235
232
|
{
|
|
236
|
-
"description": "Escape
|
|
233
|
+
"description": "Escape closes the menu and returns focus to trigger.",
|
|
237
234
|
"action": [
|
|
238
235
|
{ "type": "click", "target": "trigger" },
|
|
239
236
|
{ "type": "keypress", "target": "focusable", "key": "Escape" }
|
|
@@ -327,8 +324,7 @@
|
|
|
327
324
|
]
|
|
328
325
|
},
|
|
329
326
|
{
|
|
330
|
-
"description": "Right
|
|
331
|
-
"requiresBrowser": true,
|
|
327
|
+
"description": "Right Arrow on menuitem with submenu opens the submenu",
|
|
332
328
|
"action": [
|
|
333
329
|
{ "type": "click", "target": "trigger" },
|
|
334
330
|
{ "type": "keypress", "target": "submenuTrigger", "key": "ArrowRight" }
|
|
@@ -337,13 +333,12 @@
|
|
|
337
333
|
{
|
|
338
334
|
"target": "submenu",
|
|
339
335
|
"assertion": "toBeVisible",
|
|
340
|
-
"failureMessage": "Submenu should open when Right
|
|
336
|
+
"failureMessage": "Submenu should open when Right Arrow pressed on item with submenu"
|
|
341
337
|
}
|
|
342
338
|
]
|
|
343
339
|
},
|
|
344
340
|
{
|
|
345
341
|
"description": "Tab on a menuitem closes the menu and update aria-expanded",
|
|
346
|
-
"requiresBrowser": true,
|
|
347
342
|
"action": [
|
|
348
343
|
{ "type": "click", "target": "trigger" },
|
|
349
344
|
{ "type": "keypress", "target": "focusable", "key": "Tab" }
|
|
@@ -359,13 +354,12 @@
|
|
|
359
354
|
"assertion": "toHaveAttribute",
|
|
360
355
|
"attribute": "aria-expanded",
|
|
361
356
|
"expectedValue": "false",
|
|
362
|
-
"failureMessage": "Trigger's aria-expanded should be false after Tab
|
|
357
|
+
"failureMessage": "Trigger's aria-expanded should be false after Tab closes the menu."
|
|
363
358
|
}
|
|
364
359
|
]
|
|
365
360
|
},
|
|
366
361
|
{
|
|
367
362
|
"description": "Shift+Tab on a menuitem closes the menu and update aria-expanded",
|
|
368
|
-
"requiresBrowser": true,
|
|
369
363
|
"action": [
|
|
370
364
|
{ "type": "click", "target": "trigger" },
|
|
371
365
|
{ "type": "keypress", "target": "focusable", "key": "Shift+Tab" }
|
|
@@ -381,7 +375,7 @@
|
|
|
381
375
|
"assertion": "toHaveAttribute",
|
|
382
376
|
"attribute": "aria-expanded",
|
|
383
377
|
"expectedValue": "false",
|
|
384
|
-
"failureMessage": "Trigger's aria-expanded should be false after Shift+Tab
|
|
378
|
+
"failureMessage": "Trigger's aria-expanded should be false after Shift+Tab closes the menu."
|
|
385
379
|
}
|
|
386
380
|
]
|
|
387
381
|
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"id": "aria-ease.tabs",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"created": "28-02-2026",
|
|
6
|
+
"lastUpdated": "28-02-2026",
|
|
7
|
+
"description": "ARIA tabs interaction contract. Validates the ARIA and interaction contract for a custom tabs component following the ARIA Authoring Practices Guide pattern.",
|
|
8
|
+
"source": {
|
|
9
|
+
"apg": "https://www.w3.org/WAI/ARIA/apg/patterns/tabs/",
|
|
10
|
+
"wcag": ["2.2 AA"]
|
|
11
|
+
},
|
|
12
|
+
"W3CName": "Tabs"
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
"selectors": {
|
|
16
|
+
"tablist": "[role=tablist]",
|
|
17
|
+
"tab": "[role=tab]",
|
|
18
|
+
"panel": "[role=tabpanel]",
|
|
19
|
+
"focusable": "[role=tab]",
|
|
20
|
+
"relative": "[role=tab]"
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
"observables": {
|
|
24
|
+
"observable": "focus | visible | attribute | role",
|
|
25
|
+
"target": "tab | relative | panel | tablist",
|
|
26
|
+
"relative": "first | last | next | previous"
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
"static": [
|
|
30
|
+
{
|
|
31
|
+
"assertions": [
|
|
32
|
+
{
|
|
33
|
+
"target": "tablist",
|
|
34
|
+
"assertion": "toHaveAttribute",
|
|
35
|
+
"attribute": "aria-label | aria-labelledby",
|
|
36
|
+
"failureMessage": "Tab list doesn't conform to the ARIA Tab pattern as specified in APG 1.2. Tablist should have 'aria-label' or 'aria-labelledby' attribute to provide an accessible label for assistive technology."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"target": "tab",
|
|
40
|
+
"assertion": "toHaveAttribute",
|
|
41
|
+
"attribute": "aria-selected",
|
|
42
|
+
"expectedValue": "true | false",
|
|
43
|
+
"failureMessage": "Tab element doesn't conform to the ARIA Tab pattern as specified in APG 1.2. Tab element should have 'aria-selected=true | false' attribute. This helps assistive technology to keep track of the active state of the tab panel that the tab controls."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"target": "tab",
|
|
47
|
+
"assertion": "toHaveAttribute",
|
|
48
|
+
"attribute": "aria-controls",
|
|
49
|
+
"failureMessage": "Tab element doesn't conform to the ARIA Tab pattern as specified in APG 1.2. Tab element should have 'aria-controls' attribute that points to the id of the tab panel it controls. This helps assistive technology to associate the tab with its corresponding tab panel."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"target": "panel",
|
|
53
|
+
"assertion": "toHaveAttribute",
|
|
54
|
+
"attribute": "role",
|
|
55
|
+
"expectedValue": "tabpanel",
|
|
56
|
+
"failureMessage": "Tab panel doesn't conform to the ARIA Tab pattern as specified in APG 1.2. Each tab panel should have 'role=tabpanel' attribute. This helps assistive technology identify the panel as a significant region of the page."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"target": "panel",
|
|
60
|
+
"assertion": "toHaveAttribute",
|
|
61
|
+
"attribute": "aria-labelledby",
|
|
62
|
+
"failureMessage": "Tab panel doesn't conform to the ARIA Tab pattern as specified in APG 1.2. Each tab panel should have 'aria-labelledby' attribute that points to the id of the tab that controls it. This helps assistive technology associate the tab panel with its corresponding tab."
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"target": "tab",
|
|
66
|
+
"assertion": "toHaveAttribute",
|
|
67
|
+
"attribute": "tabindex",
|
|
68
|
+
"expectedValue": "0 | -1",
|
|
69
|
+
"failureMessage": "Tab element doesn't conform to the ARIA Tab pattern. Tab should have 'tabindex' to enable roving tabindex keyboard navigation. Active tab should have tabindex='0', inactive tabs should have tabindex='-1'."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"target": "tablist",
|
|
73
|
+
"assertion": "toHaveAttribute",
|
|
74
|
+
"attribute": "aria-orientation",
|
|
75
|
+
"expectedValue": "horizontal | vertical",
|
|
76
|
+
"failureMessage": "Tab list doesn't conform to the ARIA Tab pattern as specified in APG 1.2. Tab list should have 'aria-orientation' attribute set to 'horizontal' or 'vertical'."
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
|
|
82
|
+
"dynamic": [
|
|
83
|
+
{
|
|
84
|
+
"description": "Only one tab panel is visible at a time.",
|
|
85
|
+
"action": [],
|
|
86
|
+
"assertions": [
|
|
87
|
+
{
|
|
88
|
+
"target": "panel",
|
|
89
|
+
"assertion": "toBeVisible",
|
|
90
|
+
"failureMessage": "At least one tab panel should be visible initially."
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"description": "Right Arrow moves focus to next tab and activates it (automatic activation).",
|
|
96
|
+
"isVertical": false,
|
|
97
|
+
"action": [
|
|
98
|
+
{ "type": "focus", "target": "focusable" },
|
|
99
|
+
{ "type": "keypress", "target": "focusable", "key": "ArrowRight" }
|
|
100
|
+
],
|
|
101
|
+
"assertions": [
|
|
102
|
+
{
|
|
103
|
+
"target": "relative",
|
|
104
|
+
"assertion": "toHaveFocus",
|
|
105
|
+
"expectedValue": "second",
|
|
106
|
+
"failureMessage": "Focus should move to the second tab after pressing Right Arrow."
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"target": "relative",
|
|
110
|
+
"assertion": "toHaveAttribute",
|
|
111
|
+
"attribute": "aria-selected",
|
|
112
|
+
"expectedValue": "true",
|
|
113
|
+
"relativeTarget": "second",
|
|
114
|
+
"failureMessage": "Second tab should be activated (aria-selected=true) after Right Arrow press."
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"target": "relative",
|
|
118
|
+
"assertion": "toHaveAttribute",
|
|
119
|
+
"attribute": "tabindex",
|
|
120
|
+
"expectedValue": "0",
|
|
121
|
+
"relativeTarget": "second",
|
|
122
|
+
"failureMessage": "Second tab should have tabindex='0' after activation."
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"target": "relative",
|
|
126
|
+
"assertion": "toHaveAttribute",
|
|
127
|
+
"attribute": "aria-selected",
|
|
128
|
+
"expectedValue": "false",
|
|
129
|
+
"relativeTarget": "first",
|
|
130
|
+
"failureMessage": "Previous tab should have aria-selected=false after moving to another tab."
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"description": "Left Arrow moves focus to previous tab and activates it (wraps to last).",
|
|
136
|
+
"isVertical": false,
|
|
137
|
+
"action": [
|
|
138
|
+
{ "type": "focus", "target": "focusable" },
|
|
139
|
+
{ "type": "keypress", "target": "focusable", "key": "ArrowLeft" }
|
|
140
|
+
],
|
|
141
|
+
"assertions": [
|
|
142
|
+
{
|
|
143
|
+
"target": "relative",
|
|
144
|
+
"assertion": "toHaveFocus",
|
|
145
|
+
"expectedValue": "last",
|
|
146
|
+
"failureMessage": "Focus should wrap to last tab after pressing Left Arrow on first tab."
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"target": "relative",
|
|
150
|
+
"assertion": "toHaveAttribute",
|
|
151
|
+
"attribute": "aria-selected",
|
|
152
|
+
"expectedValue": "true",
|
|
153
|
+
"relativeTarget": "last",
|
|
154
|
+
"failureMessage": "Last tab should be activated (aria-selected=true) after Left Arrow press."
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"target": "relative",
|
|
158
|
+
"assertion": "toHaveAttribute",
|
|
159
|
+
"attribute": "tabindex",
|
|
160
|
+
"expectedValue": "0",
|
|
161
|
+
"relativeTarget": "last",
|
|
162
|
+
"failureMessage": "Last tab should have tabindex='0' after activation."
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"description": "Down Arrow moves focus to next tab and activates it (vertical tabs).",
|
|
168
|
+
"isVertical": true,
|
|
169
|
+
"action": [
|
|
170
|
+
{ "type": "focus", "target": "focusable" },
|
|
171
|
+
{ "type": "keypress", "target": "focusable", "key": "ArrowDown" }
|
|
172
|
+
],
|
|
173
|
+
"assertions": [
|
|
174
|
+
{
|
|
175
|
+
"target": "relative",
|
|
176
|
+
"assertion": "toHaveFocus",
|
|
177
|
+
"expectedValue": "second",
|
|
178
|
+
"failureMessage": "Focus should move to second tab after pressing Down Arrow (vertical orientation)."
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"target": "relative",
|
|
182
|
+
"assertion": "toHaveAttribute",
|
|
183
|
+
"attribute": "aria-selected",
|
|
184
|
+
"expectedValue": "true",
|
|
185
|
+
"relativeTarget": "second",
|
|
186
|
+
"failureMessage": "Second tab should be activated after Down Arrow press."
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"description": "Up Arrow moves focus to previous tab and activates it (wraps to last, vertical tabs).",
|
|
192
|
+
"isVertical": true,
|
|
193
|
+
"action": [
|
|
194
|
+
{ "type": "focus", "target": "focusable" },
|
|
195
|
+
{ "type": "keypress", "target": "focusable", "key": "ArrowUp" }
|
|
196
|
+
],
|
|
197
|
+
"assertions": [
|
|
198
|
+
{
|
|
199
|
+
"target": "relative",
|
|
200
|
+
"assertion": "toHaveFocus",
|
|
201
|
+
"expectedValue": "last",
|
|
202
|
+
"failureMessage": "Focus should wrap to last tab after pressing Up Arrow on first tab (vertical orientation)."
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"target": "relative",
|
|
206
|
+
"assertion": "toHaveAttribute",
|
|
207
|
+
"attribute": "aria-selected",
|
|
208
|
+
"expectedValue": "true",
|
|
209
|
+
"relativeTarget": "last",
|
|
210
|
+
"failureMessage": "Last tab should be activated after Up Arrow press."
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"description": "Home moves focus to first tab and activates it.",
|
|
216
|
+
"isOptional": true,
|
|
217
|
+
"action": [
|
|
218
|
+
{ "type": "keypress", "target": "focusable", "key": "ArrowRight" },
|
|
219
|
+
{ "type": "keypress", "target": "focusable", "key": "Home" }
|
|
220
|
+
],
|
|
221
|
+
"assertions": [
|
|
222
|
+
{
|
|
223
|
+
"target": "relative",
|
|
224
|
+
"assertion": "toHaveFocus",
|
|
225
|
+
"expectedValue": "first",
|
|
226
|
+
"failureMessage": "Focus should move to first tab after pressing Home."
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"target": "relative",
|
|
230
|
+
"assertion": "toHaveAttribute",
|
|
231
|
+
"attribute": "aria-selected",
|
|
232
|
+
"expectedValue": "true",
|
|
233
|
+
"relativeTarget": "first",
|
|
234
|
+
"failureMessage": "First tab should be activated after Home press."
|
|
235
|
+
}
|
|
236
|
+
]
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"description": "End moves focus to last tab and activates it.",
|
|
240
|
+
"isOptional": true,
|
|
241
|
+
"action": [
|
|
242
|
+
{ "type": "keypress", "target": "focusable", "key": "End" }
|
|
243
|
+
],
|
|
244
|
+
"assertions": [
|
|
245
|
+
{
|
|
246
|
+
"target": "relative",
|
|
247
|
+
"assertion": "toHaveFocus",
|
|
248
|
+
"expectedValue": "last",
|
|
249
|
+
"failureMessage": "Focus should move to last tab after pressing End."
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"target": "relative",
|
|
253
|
+
"assertion": "toHaveAttribute",
|
|
254
|
+
"attribute": "aria-selected",
|
|
255
|
+
"expectedValue": "true",
|
|
256
|
+
"relativeTarget": "last",
|
|
257
|
+
"failureMessage": "Last tab should be activated after End press."
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"description": "Clicking a tab activates it and displays its panel.",
|
|
263
|
+
"action": [
|
|
264
|
+
{ "type": "click", "target": "relative", "relativeTarget": "second" }
|
|
265
|
+
],
|
|
266
|
+
"assertions": [
|
|
267
|
+
{
|
|
268
|
+
"target": "relative",
|
|
269
|
+
"assertion": "toHaveAttribute",
|
|
270
|
+
"attribute": "aria-selected",
|
|
271
|
+
"expectedValue": "true",
|
|
272
|
+
"relativeTarget": "second",
|
|
273
|
+
"failureMessage": "Clicked tab should have aria-selected=true."
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"target": "relative",
|
|
277
|
+
"assertion": "toHaveAttribute",
|
|
278
|
+
"attribute": "tabindex",
|
|
279
|
+
"expectedValue": "0",
|
|
280
|
+
"relativeTarget": "second",
|
|
281
|
+
"failureMessage": "Clicked tab should have tabindex='0'."
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"target": "relative",
|
|
285
|
+
"assertion": "toHaveAttribute",
|
|
286
|
+
"attribute": "aria-selected",
|
|
287
|
+
"expectedValue": "false",
|
|
288
|
+
"relativeTarget": "first",
|
|
289
|
+
"failureMessage": "Previous tab should have aria-selected=false after clicking another tab."
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"description": "Pressing Enter on tab element activates its associated tab panel.",
|
|
295
|
+
"action": [
|
|
296
|
+
{ "type": "keypress", "target": "tab", "key": "Enter" }
|
|
297
|
+
],
|
|
298
|
+
"assertions": [
|
|
299
|
+
{
|
|
300
|
+
"target": "panel",
|
|
301
|
+
"assertion": "toBeVisible",
|
|
302
|
+
"failureMessage": "Panel should be visible after pressing Enter on trigger."
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"target": "tab",
|
|
306
|
+
"assertion": "toHaveAttribute",
|
|
307
|
+
"attribute": "aria-selected",
|
|
308
|
+
"expectedValue": "true",
|
|
309
|
+
"failureMessage": "Tab element's should have aria-selected=true after pressing Enter."
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
"target": "tab",
|
|
313
|
+
"assertion": "toHaveAttribute",
|
|
314
|
+
"attribute": "tabindex",
|
|
315
|
+
"expectedValue": "0",
|
|
316
|
+
"failureMessage": "Tab element's should have tabindex='0' after pressing Enter."
|
|
317
|
+
}
|
|
318
|
+
]
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"description": "Pressing Space on tab element activates its associated tab panel.",
|
|
322
|
+
"action": [
|
|
323
|
+
{ "type": "keypress", "target": "tab", "key": "Space" }
|
|
324
|
+
],
|
|
325
|
+
"assertions": [
|
|
326
|
+
{
|
|
327
|
+
"target": "panel",
|
|
328
|
+
"assertion": "toBeVisible",
|
|
329
|
+
"failureMessage": "Panel should be visible after pressing Space on trigger."
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"target": "tab",
|
|
333
|
+
"assertion": "toHaveAttribute",
|
|
334
|
+
"attribute": "aria-selected",
|
|
335
|
+
"expectedValue": "true",
|
|
336
|
+
"failureMessage": "Tab element's should have aria-selected=true after pressing Space."
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
"target": "tab",
|
|
340
|
+
"assertion": "toHaveAttribute",
|
|
341
|
+
"attribute": "tabindex",
|
|
342
|
+
"expectedValue": "0",
|
|
343
|
+
"failureMessage": "Tab element's should have tabindex='0' after pressing Enter."
|
|
344
|
+
}
|
|
345
|
+
]
|
|
346
|
+
}
|
|
347
|
+
]
|
|
348
|
+
}
|
|
@@ -37,6 +37,10 @@ var init_contract = __esm({
|
|
|
37
37
|
accordion: {
|
|
38
38
|
path: "./contracts/AccordionContract.json",
|
|
39
39
|
component: "accordion"
|
|
40
|
+
},
|
|
41
|
+
tabs: {
|
|
42
|
+
path: "./contracts/TabsContract.json",
|
|
43
|
+
component: "tabs"
|
|
40
44
|
}
|
|
41
45
|
};
|
|
42
46
|
}
|
|
@@ -156,7 +160,7 @@ ${"\u2500".repeat(60)}`);
|
|
|
156
160
|
this.log(`\u{1F4A1} Optional Enhancements (${suggestions.length}):
|
|
157
161
|
`);
|
|
158
162
|
this.log(`These features are optional per APG guidelines but recommended`);
|
|
159
|
-
this.log(`for improved user experience and keyboard
|
|
163
|
+
this.log(`for improved user experience and keyboard interaction:
|
|
160
164
|
`);
|
|
161
165
|
suggestions.forEach((test, index) => {
|
|
162
166
|
this.log(`${index + 1}. ${test.description}`);
|
|
@@ -356,9 +360,9 @@ async function runContractTestsPlaywright(componentName, url) {
|
|
|
356
360
|
}
|
|
357
361
|
await page.addStyleTag({ content: `* { transition: none !important; animation: none !important; }` });
|
|
358
362
|
}
|
|
359
|
-
const mainSelector = componentContract.selectors.trigger || componentContract.selectors.input || componentContract.selectors.container;
|
|
363
|
+
const mainSelector = componentContract.selectors.trigger || componentContract.selectors.input || componentContract.selectors.container || componentContract.selectors.tablist || componentContract.selectors.tab;
|
|
360
364
|
if (!mainSelector) {
|
|
361
|
-
throw new Error(`CRITICAL: No main selector (trigger, input, or
|
|
365
|
+
throw new Error(`CRITICAL: No main selector (trigger, input, container, tablist, or tab) found in contract for ${componentName}`);
|
|
362
366
|
}
|
|
363
367
|
try {
|
|
364
368
|
await page.locator(mainSelector).first().waitFor({ state: "attached", timeout: 3e4 });
|
|
@@ -420,28 +424,52 @@ async function runContractTestsPlaywright(componentName, url) {
|
|
|
420
424
|
failures.push(`Target ${test.target} not found.`);
|
|
421
425
|
continue;
|
|
422
426
|
}
|
|
427
|
+
const isRedundantCheck = (selector, attrName, expectedVal) => {
|
|
428
|
+
const attrPattern = new RegExp(`\\[${attrName}(?:=["']?([^\\]"']+)["']?)?\\]`);
|
|
429
|
+
const match = selector.match(attrPattern);
|
|
430
|
+
if (!match) return false;
|
|
431
|
+
if (!expectedVal) return true;
|
|
432
|
+
const selectorValue = match[1];
|
|
433
|
+
if (selectorValue) {
|
|
434
|
+
const expectedValues = expectedVal.split(" | ");
|
|
435
|
+
return expectedValues.includes(selectorValue);
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
};
|
|
423
439
|
if (!test.expectedValue) {
|
|
424
440
|
const attributes = test.attribute.split(" | ");
|
|
425
441
|
let hasAny = false;
|
|
442
|
+
let allRedundant = true;
|
|
426
443
|
for (const attr of attributes) {
|
|
427
|
-
const
|
|
444
|
+
const attrTrimmed = attr.trim();
|
|
445
|
+
if (isRedundantCheck(targetSelector, attrTrimmed)) {
|
|
446
|
+
passes.push(`${attrTrimmed} on ${test.target} verified by selector (already present in: ${targetSelector}).`);
|
|
447
|
+
hasAny = true;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
allRedundant = false;
|
|
451
|
+
const value = await target.getAttribute(attrTrimmed);
|
|
428
452
|
if (value !== null) {
|
|
429
453
|
hasAny = true;
|
|
430
454
|
break;
|
|
431
455
|
}
|
|
432
456
|
}
|
|
433
|
-
if (!hasAny) {
|
|
457
|
+
if (!hasAny && !allRedundant) {
|
|
434
458
|
failures.push(test.failureMessage + ` None of the attributes "${test.attribute}" are present.`);
|
|
435
|
-
} else {
|
|
459
|
+
} else if (!allRedundant && hasAny) {
|
|
436
460
|
passes.push(`At least one of the attributes "${test.attribute}" exists on the element.`);
|
|
437
461
|
}
|
|
438
462
|
} else {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (!attributeValue || !expectedValues.includes(attributeValue)) {
|
|
442
|
-
failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
|
|
463
|
+
if (isRedundantCheck(targetSelector, test.attribute, test.expectedValue)) {
|
|
464
|
+
passes.push(`${test.attribute}="${test.expectedValue}" on ${test.target} verified by selector (already present in: ${targetSelector}).`);
|
|
443
465
|
} else {
|
|
444
|
-
|
|
466
|
+
const attributeValue = await target.getAttribute(test.attribute);
|
|
467
|
+
const expectedValues = test.expectedValue.split(" | ");
|
|
468
|
+
if (!attributeValue || !expectedValues.includes(attributeValue)) {
|
|
469
|
+
failures.push(test.failureMessage + ` Attribute value does not match expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
|
|
470
|
+
} else {
|
|
471
|
+
passes.push(`Attribute value matches expected value. Expected: ${test.expectedValue}, Found: ${attributeValue}`);
|
|
472
|
+
}
|
|
445
473
|
}
|
|
446
474
|
}
|
|
447
475
|
}
|
|
@@ -550,6 +578,19 @@ This indicates a problem with the menu component's close functionality.`
|
|
|
550
578
|
if (shouldSkipTest) {
|
|
551
579
|
continue;
|
|
552
580
|
}
|
|
581
|
+
if (componentContract.selectors.panel && componentContract.selectors.tab && componentContract.selectors.tablist) {
|
|
582
|
+
if (dynamicTest.isVertical !== void 0 && componentContract.selectors.tablist) {
|
|
583
|
+
const tablistSelector = componentContract.selectors.tablist;
|
|
584
|
+
const tablist = page.locator(tablistSelector).first();
|
|
585
|
+
const orientation = await tablist.getAttribute("aria-orientation");
|
|
586
|
+
const isVertical = orientation === "vertical";
|
|
587
|
+
if (dynamicTest.isVertical !== isVertical) {
|
|
588
|
+
const skipReason = dynamicTest.isVertical ? `Skipping vertical tabs test - component has horizontal orientation` : `Skipping horizontal tabs test - component has vertical orientation`;
|
|
589
|
+
reporter.reportTest(dynamicTest, "skip", skipReason);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
553
594
|
for (const act of action) {
|
|
554
595
|
if (!page || page.isClosed()) {
|
|
555
596
|
failures.push(`CRITICAL: Browser/page closed during test execution. Remaining actions skipped.`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { closeSharedBrowser, ContractReporter, contract_default } from './chunk-
|
|
1
|
+
import { closeSharedBrowser, ContractReporter, contract_default } from './chunk-XLG3MIPQ.js';
|
|
2
2
|
import { axe } from 'jest-axe';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
|
|
@@ -106,7 +106,7 @@ Error: ${error instanceof Error ? error.message : String(error)}`
|
|
|
106
106
|
const devServerUrl = await checkDevServer(url);
|
|
107
107
|
if (devServerUrl) {
|
|
108
108
|
console.log(`\u{1F3AD} Running Playwright tests on ${devServerUrl}`);
|
|
109
|
-
const { runContractTestsPlaywright } = await import('./contractTestRunnerPlaywright-
|
|
109
|
+
const { runContractTestsPlaywright } = await import('./contractTestRunnerPlaywright-N77NEY25.js');
|
|
110
110
|
contract = await runContractTestsPlaywright(componentName, devServerUrl);
|
|
111
111
|
} else {
|
|
112
112
|
throw new Error(
|