@object-ui/components 2.0.0 → 3.0.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/.turbo/turbo-build.log +12 -12
- package/CHANGELOG.md +19 -0
- package/dist/index.css +1 -1
- package/dist/index.js +19610 -19344
- package/dist/index.umd.cjs +29 -29
- package/dist/src/custom/index.d.ts +2 -0
- package/dist/src/custom/view-skeleton.d.ts +37 -0
- package/dist/src/custom/view-states.d.ts +33 -0
- package/package.json +17 -17
- package/src/__tests__/__snapshots__/snapshot-critical.test.tsx.snap +811 -0
- package/src/__tests__/__snapshots__/snapshot.test.tsx.snap +327 -0
- package/src/__tests__/accessibility.test.tsx +137 -0
- package/src/__tests__/api-consistency.test.tsx +596 -0
- package/src/__tests__/color-contrast.test.tsx +212 -0
- package/src/__tests__/edge-cases.test.tsx +285 -0
- package/src/__tests__/snapshot-critical.test.tsx +317 -0
- package/src/__tests__/snapshot.test.tsx +205 -0
- package/src/__tests__/wcag-audit.test.tsx +493 -0
- package/src/custom/index.ts +2 -0
- package/src/custom/view-skeleton.tsx +243 -0
- package/src/custom/view-states.tsx +153 -0
- package/src/renderers/complex/data-table.tsx +28 -13
- package/src/renderers/complex/resizable.tsx +20 -17
- package/src/renderers/data-display/list.tsx +1 -1
- package/src/renderers/data-display/table.tsx +1 -1
- package/src/renderers/data-display/tree-view.tsx +2 -1
- package/src/renderers/form/form.tsx +10 -6
- package/src/renderers/layout/aspect-ratio.tsx +1 -1
- package/src/stories-json/Accessibility.mdx +297 -0
- package/src/stories-json/EdgeCases.stories.tsx +160 -0
- package/src/stories-json/GettingStarted.mdx +89 -0
- package/src/stories-json/Introduction.mdx +127 -0
- package/src/ui/slider.tsx +6 -2
- package/src/stories/Introduction.mdx +0 -61
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`Badge snapshots > renders variant="default" 1`] = `
|
|
4
|
+
<div
|
|
5
|
+
class="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-primary text-primary-foreground hover:bg-primary/80"
|
|
6
|
+
>
|
|
7
|
+
Label
|
|
8
|
+
</div>
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
exports[`Badge snapshots > renders variant="destructive" 1`] = `
|
|
12
|
+
<div
|
|
13
|
+
class="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80"
|
|
14
|
+
>
|
|
15
|
+
Label
|
|
16
|
+
</div>
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
exports[`Badge snapshots > renders variant="outline" 1`] = `
|
|
20
|
+
<div
|
|
21
|
+
class="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-foreground"
|
|
22
|
+
>
|
|
23
|
+
Label
|
|
24
|
+
</div>
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
exports[`Badge snapshots > renders variant="secondary" 1`] = `
|
|
28
|
+
<div
|
|
29
|
+
class="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80"
|
|
30
|
+
>
|
|
31
|
+
Label
|
|
32
|
+
</div>
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
exports[`Badge snapshots > renders with custom className 1`] = `
|
|
36
|
+
<div
|
|
37
|
+
class="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-primary text-primary-foreground hover:bg-primary/80 custom-class"
|
|
38
|
+
>
|
|
39
|
+
Custom
|
|
40
|
+
</div>
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
exports[`Button snapshots > renders disabled state 1`] = `
|
|
44
|
+
<button
|
|
45
|
+
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2"
|
|
46
|
+
disabled=""
|
|
47
|
+
>
|
|
48
|
+
Disabled
|
|
49
|
+
</button>
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
exports[`Button snapshots > renders size="icon" 1`] = `
|
|
53
|
+
<button
|
|
54
|
+
aria-label="Icon button"
|
|
55
|
+
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-primary/90 h-10 w-10"
|
|
56
|
+
>
|
|
57
|
+
★
|
|
58
|
+
</button>
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
exports[`Button snapshots > renders size="lg" 1`] = `
|
|
62
|
+
<button
|
|
63
|
+
class="inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-primary/90 h-11 rounded-md px-8"
|
|
64
|
+
>
|
|
65
|
+
Large
|
|
66
|
+
</button>
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
exports[`Button snapshots > renders size="sm" 1`] = `
|
|
70
|
+
<button
|
|
71
|
+
class="inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-primary/90 h-9 rounded-md px-3"
|
|
72
|
+
>
|
|
73
|
+
Small
|
|
74
|
+
</button>
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
exports[`Button snapshots > renders variant="default" 1`] = `
|
|
78
|
+
<button
|
|
79
|
+
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2"
|
|
80
|
+
>
|
|
81
|
+
Click me
|
|
82
|
+
</button>
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
exports[`Button snapshots > renders variant="destructive" 1`] = `
|
|
86
|
+
<button
|
|
87
|
+
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-destructive text-destructive-foreground hover:bg-destructive/90 h-10 px-4 py-2"
|
|
88
|
+
>
|
|
89
|
+
Click me
|
|
90
|
+
</button>
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
exports[`Button snapshots > renders variant="ghost" 1`] = `
|
|
94
|
+
<button
|
|
95
|
+
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
|
|
96
|
+
>
|
|
97
|
+
Click me
|
|
98
|
+
</button>
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
exports[`Button snapshots > renders variant="link" 1`] = `
|
|
102
|
+
<button
|
|
103
|
+
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-primary underline-offset-4 hover:underline h-10 px-4 py-2"
|
|
104
|
+
>
|
|
105
|
+
Click me
|
|
106
|
+
</button>
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
exports[`Button snapshots > renders variant="outline" 1`] = `
|
|
110
|
+
<button
|
|
111
|
+
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
|
|
112
|
+
>
|
|
113
|
+
Click me
|
|
114
|
+
</button>
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
exports[`Button snapshots > renders variant="secondary" 1`] = `
|
|
118
|
+
<button
|
|
119
|
+
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-secondary text-secondary-foreground hover:bg-secondary/80 h-10 px-4 py-2"
|
|
120
|
+
>
|
|
121
|
+
Click me
|
|
122
|
+
</button>
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
exports[`Card snapshots > renders content-only card 1`] = `
|
|
126
|
+
<div
|
|
127
|
+
class="rounded-lg border bg-card text-card-foreground shadow-sm"
|
|
128
|
+
>
|
|
129
|
+
<div
|
|
130
|
+
class="p-6 pt-0"
|
|
131
|
+
>
|
|
132
|
+
Just content
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
`;
|
|
136
|
+
|
|
137
|
+
exports[`Card snapshots > renders header-only card 1`] = `
|
|
138
|
+
<div
|
|
139
|
+
class="rounded-lg border bg-card text-card-foreground shadow-sm"
|
|
140
|
+
>
|
|
141
|
+
<div
|
|
142
|
+
class="flex flex-col space-y-1.5 p-6"
|
|
143
|
+
>
|
|
144
|
+
<div
|
|
145
|
+
class="text-2xl font-semibold leading-none tracking-tight"
|
|
146
|
+
>
|
|
147
|
+
Minimal Card
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
`;
|
|
152
|
+
|
|
153
|
+
exports[`Card snapshots > renders with custom className 1`] = `
|
|
154
|
+
<div
|
|
155
|
+
class="rounded-lg border bg-card text-card-foreground w-96 shadow-lg"
|
|
156
|
+
>
|
|
157
|
+
<div
|
|
158
|
+
class="p-6 pt-0"
|
|
159
|
+
>
|
|
160
|
+
Styled card
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
`;
|
|
164
|
+
|
|
165
|
+
exports[`Card snapshots > renders with header, content, and footer 1`] = `
|
|
166
|
+
<div
|
|
167
|
+
class="rounded-lg border bg-card text-card-foreground shadow-sm"
|
|
168
|
+
>
|
|
169
|
+
<div
|
|
170
|
+
class="flex flex-col space-y-1.5 p-6"
|
|
171
|
+
>
|
|
172
|
+
<div
|
|
173
|
+
class="text-2xl font-semibold leading-none tracking-tight"
|
|
174
|
+
>
|
|
175
|
+
Card Title
|
|
176
|
+
</div>
|
|
177
|
+
<div
|
|
178
|
+
class="text-sm text-muted-foreground"
|
|
179
|
+
>
|
|
180
|
+
A brief description
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
<div
|
|
184
|
+
class="p-6 pt-0"
|
|
185
|
+
>
|
|
186
|
+
<p>
|
|
187
|
+
Card body content
|
|
188
|
+
</p>
|
|
189
|
+
</div>
|
|
190
|
+
<div
|
|
191
|
+
class="flex items-center p-6 pt-0"
|
|
192
|
+
>
|
|
193
|
+
<button
|
|
194
|
+
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2"
|
|
195
|
+
>
|
|
196
|
+
Action
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
`;
|
|
201
|
+
|
|
202
|
+
exports[`Empty state snapshots > renders empty media with default variant 1`] = `
|
|
203
|
+
<div
|
|
204
|
+
class="flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-transparent"
|
|
205
|
+
data-slot="empty-icon"
|
|
206
|
+
data-variant="default"
|
|
207
|
+
>
|
|
208
|
+
<span>
|
|
209
|
+
🔍
|
|
210
|
+
</span>
|
|
211
|
+
</div>
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
exports[`Empty state snapshots > renders full empty state with media, title, description, and content 1`] = `
|
|
215
|
+
<div
|
|
216
|
+
class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12"
|
|
217
|
+
data-slot="empty"
|
|
218
|
+
>
|
|
219
|
+
<div
|
|
220
|
+
class="flex max-w-sm flex-col items-center gap-2 text-center"
|
|
221
|
+
data-slot="empty-header"
|
|
222
|
+
>
|
|
223
|
+
<div
|
|
224
|
+
class="mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6"
|
|
225
|
+
data-slot="empty-icon"
|
|
226
|
+
data-variant="icon"
|
|
227
|
+
>
|
|
228
|
+
<span>
|
|
229
|
+
📭
|
|
230
|
+
</span>
|
|
231
|
+
</div>
|
|
232
|
+
<div
|
|
233
|
+
class="text-lg font-medium tracking-tight"
|
|
234
|
+
data-slot="empty-title"
|
|
235
|
+
>
|
|
236
|
+
No results found
|
|
237
|
+
</div>
|
|
238
|
+
<div
|
|
239
|
+
class="text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4"
|
|
240
|
+
data-slot="empty-description"
|
|
241
|
+
>
|
|
242
|
+
Try adjusting your search or filter criteria.
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
<div
|
|
246
|
+
class="flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance"
|
|
247
|
+
data-slot="empty-content"
|
|
248
|
+
>
|
|
249
|
+
<button
|
|
250
|
+
class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
|
|
251
|
+
>
|
|
252
|
+
Clear filters
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
`;
|
|
257
|
+
|
|
258
|
+
exports[`Empty state snapshots > renders minimal empty state with title only 1`] = `
|
|
259
|
+
<div
|
|
260
|
+
class="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12"
|
|
261
|
+
data-slot="empty"
|
|
262
|
+
>
|
|
263
|
+
<div
|
|
264
|
+
class="flex max-w-sm flex-col items-center gap-2 text-center"
|
|
265
|
+
data-slot="empty-header"
|
|
266
|
+
>
|
|
267
|
+
<div
|
|
268
|
+
class="text-lg font-medium tracking-tight"
|
|
269
|
+
data-slot="empty-title"
|
|
270
|
+
>
|
|
271
|
+
Nothing here yet
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
`;
|
|
276
|
+
|
|
277
|
+
exports[`Spinner snapshots > renders default spinner 1`] = `
|
|
278
|
+
<div
|
|
279
|
+
aria-label="Loading"
|
|
280
|
+
class="flex items-center justify-center"
|
|
281
|
+
role="status"
|
|
282
|
+
>
|
|
283
|
+
<svg
|
|
284
|
+
aria-hidden="true"
|
|
285
|
+
class="lucide lucide-loader-circle animate-spin text-muted-foreground w-full h-full min-w-4 min-h-4"
|
|
286
|
+
fill="none"
|
|
287
|
+
height="24"
|
|
288
|
+
stroke="currentColor"
|
|
289
|
+
stroke-linecap="round"
|
|
290
|
+
stroke-linejoin="round"
|
|
291
|
+
stroke-width="2"
|
|
292
|
+
viewBox="0 0 24 24"
|
|
293
|
+
width="24"
|
|
294
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
295
|
+
>
|
|
296
|
+
<path
|
|
297
|
+
d="M21 12a9 9 0 1 1-6.219-8.56"
|
|
298
|
+
/>
|
|
299
|
+
</svg>
|
|
300
|
+
</div>
|
|
301
|
+
`;
|
|
302
|
+
|
|
303
|
+
exports[`Spinner snapshots > renders with custom className 1`] = `
|
|
304
|
+
<div
|
|
305
|
+
aria-label="Loading"
|
|
306
|
+
class="flex items-center justify-center h-8 w-8"
|
|
307
|
+
role="status"
|
|
308
|
+
>
|
|
309
|
+
<svg
|
|
310
|
+
aria-hidden="true"
|
|
311
|
+
class="lucide lucide-loader-circle animate-spin text-muted-foreground w-full h-full min-w-4 min-h-4"
|
|
312
|
+
fill="none"
|
|
313
|
+
height="24"
|
|
314
|
+
stroke="currentColor"
|
|
315
|
+
stroke-linecap="round"
|
|
316
|
+
stroke-linejoin="round"
|
|
317
|
+
stroke-width="2"
|
|
318
|
+
viewBox="0 0 24 24"
|
|
319
|
+
width="24"
|
|
320
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
321
|
+
>
|
|
322
|
+
<path
|
|
323
|
+
d="M21 12a9 9 0 1 1-6.219-8.56"
|
|
324
|
+
/>
|
|
325
|
+
</svg>
|
|
326
|
+
</div>
|
|
327
|
+
`;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Axe-core accessibility test suite for ObjectUI Shadcn components.
|
|
11
|
+
*
|
|
12
|
+
* Tests core UI primitives against WCAG 2.1 AA standards using axe-core.
|
|
13
|
+
* Part of Q1 2026 roadmap §1.4 Test Coverage Improvement.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { render } from '@testing-library/react';
|
|
18
|
+
import { axe } from 'vitest-axe';
|
|
19
|
+
import React from 'react';
|
|
20
|
+
import {
|
|
21
|
+
Button,
|
|
22
|
+
Badge,
|
|
23
|
+
Card,
|
|
24
|
+
CardContent,
|
|
25
|
+
CardHeader,
|
|
26
|
+
CardTitle,
|
|
27
|
+
CardDescription,
|
|
28
|
+
Input,
|
|
29
|
+
Label,
|
|
30
|
+
Skeleton,
|
|
31
|
+
} from '@object-ui/components';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Helper to assert no axe violations.
|
|
35
|
+
* Works with vitest-axe's AxeResults format.
|
|
36
|
+
*/
|
|
37
|
+
async function expectNoViolations(container: HTMLElement) {
|
|
38
|
+
const results = await axe(container);
|
|
39
|
+
const violations = results.violations || [];
|
|
40
|
+
if (violations.length > 0) {
|
|
41
|
+
const messages = violations.map(
|
|
42
|
+
(v: any) => `[${v.impact}] ${v.id}: ${v.description} (${v.nodes.length} instance(s))`
|
|
43
|
+
);
|
|
44
|
+
throw new Error(`Expected no accessibility violations, but found ${violations.length}:\n${messages.join('\n')}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('Accessibility — axe-core (WCAG 2.1 AA)', () => {
|
|
49
|
+
it('Button should have no a11y violations', async () => {
|
|
50
|
+
const { container } = render(<Button>Click me</Button>);
|
|
51
|
+
await expectNoViolations(container);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('Icon-only Button with aria-label should have no violations', async () => {
|
|
55
|
+
const { container } = render(
|
|
56
|
+
<Button aria-label="Close" size="icon">
|
|
57
|
+
✕
|
|
58
|
+
</Button>
|
|
59
|
+
);
|
|
60
|
+
await expectNoViolations(container);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('Badge should have no a11y violations', async () => {
|
|
64
|
+
const { container } = render(<Badge>Status</Badge>);
|
|
65
|
+
await expectNoViolations(container);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('Card with proper heading hierarchy should have no violations', async () => {
|
|
69
|
+
const { container } = render(
|
|
70
|
+
<Card>
|
|
71
|
+
<CardHeader>
|
|
72
|
+
<CardTitle>Card Title</CardTitle>
|
|
73
|
+
<CardDescription>Card description text</CardDescription>
|
|
74
|
+
</CardHeader>
|
|
75
|
+
<CardContent>
|
|
76
|
+
<p>Card content</p>
|
|
77
|
+
</CardContent>
|
|
78
|
+
</Card>
|
|
79
|
+
);
|
|
80
|
+
await expectNoViolations(container);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('Input with associated Label should have no violations', async () => {
|
|
84
|
+
const { container } = render(
|
|
85
|
+
<div>
|
|
86
|
+
<Label htmlFor="email">Email</Label>
|
|
87
|
+
<Input id="email" type="email" placeholder="Enter email" />
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
await expectNoViolations(container);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('Input with aria-label should have no violations', async () => {
|
|
94
|
+
const { container } = render(
|
|
95
|
+
<Input aria-label="Search" type="search" placeholder="Search..." />
|
|
96
|
+
);
|
|
97
|
+
await expectNoViolations(container);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('Skeleton should have no a11y violations', async () => {
|
|
101
|
+
const { container } = render(
|
|
102
|
+
<div role="status" aria-label="Loading">
|
|
103
|
+
<Skeleton className="h-10 w-full" />
|
|
104
|
+
<Skeleton className="h-4 w-3/4" />
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
await expectNoViolations(container);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('Multiple Buttons in a group should have no violations', async () => {
|
|
111
|
+
const { container } = render(
|
|
112
|
+
<div role="group" aria-label="Actions">
|
|
113
|
+
<Button variant="default">Save</Button>
|
|
114
|
+
<Button variant="outline">Cancel</Button>
|
|
115
|
+
<Button variant="destructive">Delete</Button>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
await expectNoViolations(container);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('Form with proper labels should have no violations', async () => {
|
|
122
|
+
const { container } = render(
|
|
123
|
+
<form aria-label="Login form">
|
|
124
|
+
<div>
|
|
125
|
+
<Label htmlFor="username">Username</Label>
|
|
126
|
+
<Input id="username" type="text" />
|
|
127
|
+
</div>
|
|
128
|
+
<div>
|
|
129
|
+
<Label htmlFor="password">Password</Label>
|
|
130
|
+
<Input id="password" type="password" />
|
|
131
|
+
</div>
|
|
132
|
+
<Button type="submit">Sign In</Button>
|
|
133
|
+
</form>
|
|
134
|
+
);
|
|
135
|
+
await expectNoViolations(container);
|
|
136
|
+
});
|
|
137
|
+
});
|