@zap-wunschlachen/wl-shared-components 1.0.76 → 1.0.78

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 (227) hide show
  1. package/.github/workflows/playwright.yml +229 -229
  2. package/.github/workflows/static.yml +61 -61
  3. package/.github/workflows/update-snapshots.yml +37 -37
  4. package/.prettierrc.json +8 -8
  5. package/.storybook/main.ts +18 -18
  6. package/.storybook/preview.ts +37 -37
  7. package/.storybook/storyWrapper.vue +18 -18
  8. package/.storybook/withVuetifyTheme.decorator.ts +21 -21
  9. package/App.vue +139 -139
  10. package/README.md +56 -56
  11. package/docs/assets.md +62 -62
  12. package/heroicons.ts +75 -75
  13. package/index.html +19 -19
  14. package/package.json +71 -71
  15. package/playwright.config.ts +48 -48
  16. package/public/background.svg +60 -60
  17. package/public/style.css +187 -187
  18. package/public/technologies.svg +22 -22
  19. package/scripts/check-translations.ts +352 -352
  20. package/src/assets/css/base.css +242 -242
  21. package/src/assets/css/variables.css +176 -176
  22. package/src/components/Accordion/Accordion.css +65 -65
  23. package/src/components/Accordion/AccordionGroup.vue +88 -88
  24. package/src/components/Accordion/AccordionItem.vue +272 -272
  25. package/src/components/Accordion/presets/default.css +4 -4
  26. package/src/components/Accordion/presets/elevated.css +25 -25
  27. package/src/components/Accordion/presets/filled.css +26 -26
  28. package/src/components/Accordion/presets/index.css +5 -5
  29. package/src/components/Accordion/presets/plain.css +34 -34
  30. package/src/components/Appointment/Card/Actions.css +54 -54
  31. package/src/components/Appointment/Card/Actions.vue +99 -99
  32. package/src/components/Appointment/Card/AnamneseNotification.css +20 -20
  33. package/src/components/Appointment/Card/AnamneseNotification.vue +23 -23
  34. package/src/components/Appointment/Card/Card.css +99 -99
  35. package/src/components/Appointment/Card/Card.vue +97 -97
  36. package/src/components/Appointment/Card/Details.css +62 -62
  37. package/src/components/Appointment/Card/Details.vue +44 -44
  38. package/src/components/Audio/Audio.vue +187 -187
  39. package/src/components/Audio/Waveform.vue +118 -118
  40. package/src/components/Banner/Banner.css +29 -29
  41. package/src/components/Banner/Banner.vue +89 -89
  42. package/src/components/Button/Button.vue +257 -257
  43. package/src/components/CheckBox/CheckBox.css +234 -234
  44. package/src/components/CheckBox/Checkbox.vue +184 -184
  45. package/src/components/DateInput/DateInput.css +2 -2
  46. package/src/components/DateInput/DateInput.vue +376 -370
  47. package/src/components/Dialog/Dialog.css +6 -6
  48. package/src/components/Dialog/Dialog.vue +46 -46
  49. package/src/components/EditField/EditField.css +19 -19
  50. package/src/components/EditField/EditField.vue +211 -211
  51. package/src/components/ErrorPage/ErrorPage.css +172 -172
  52. package/src/components/IconBullet/IconBullet.vue +104 -104
  53. package/src/components/IconBullet/IconBulletList.vue +55 -55
  54. package/src/components/Icons/AdvanceAppointments.vue +161 -161
  55. package/src/components/Icons/Audio/CloudFailed.vue +27 -27
  56. package/src/components/Icons/Audio/CloudSaved.vue +28 -28
  57. package/src/components/Icons/Audio/Delete.vue +22 -22
  58. package/src/components/Icons/Audio/Pause.vue +25 -25
  59. package/src/components/Icons/Audio/Play.vue +22 -22
  60. package/src/components/Icons/Calendar.vue +28 -28
  61. package/src/components/Icons/CalendarNotification.vue +137 -137
  62. package/src/components/Icons/Chair.vue +43 -43
  63. package/src/components/Icons/ChairNotification.vue +46 -46
  64. package/src/components/Icons/Circle.vue +66 -66
  65. package/src/components/Icons/FavIcon.vue +69 -69
  66. package/src/components/Icons/FilledCircle.vue +11 -11
  67. package/src/components/Icons/Group3.vue +57 -57
  68. package/src/components/Icons/Play.vue +16 -16
  69. package/src/components/Icons/RingNotification.vue +65 -65
  70. package/src/components/Icons/SolidArrowRight.vue +14 -14
  71. package/src/components/Icons/checkbox.vue +19 -19
  72. package/src/components/Icons/outlineChecked.vue +38 -38
  73. package/src/components/Input/Input.css +234 -234
  74. package/src/components/Input/Input.vue +281 -281
  75. package/src/components/Laboratory/AppointmentCard/AppointmentCard.css +7 -7
  76. package/src/components/Laboratory/AppointmentCard/AppointmentCard.vue +116 -116
  77. package/src/components/Laboratory/ChatBoxImage/ChatBoxImage.vue +81 -81
  78. package/src/components/Laboratory/ChatMessage/ChatMessage.vue +113 -113
  79. package/src/components/Laboratory/ChatMessage/ChatMessageBadge.css +4 -4
  80. package/src/components/Laboratory/ChatMessage/ChatMessageBadge.vue +99 -99
  81. package/src/components/Laboratory/ChatNotification/ChatNotification.vue +130 -130
  82. package/src/components/Laboratory/DocumentCard/DocumentCard.css +3 -3
  83. package/src/components/Laboratory/DocumentCard/DocumentCard.vue +50 -50
  84. package/src/components/Laboratory/DocumentCard/DocumentCardItem.vue +53 -53
  85. package/src/components/Laboratory/InfoCard/InfoCard.vue +162 -162
  86. package/src/components/Laboratory/MainColumnsBar/MainColumnsBar.vue +102 -102
  87. package/src/components/Laboratory/ProgressCircle/ProgressCircle.vue +152 -152
  88. package/src/components/Laboratory/ProgressLinear/ProgressLinear.css +33 -33
  89. package/src/components/Laboratory/ProgressLinear/ProgressLinear.vue +75 -75
  90. package/src/components/Laboratory/SelectionColumnBar/SelectionColumnBar.vue +92 -92
  91. package/src/components/Laboratory/StatusNotification/StatusNotification.vue +49 -49
  92. package/src/components/Laboratory/TagLabel/TagLabel.vue +126 -126
  93. package/src/components/Laboratory/TagLabelGroup/TagLabelGroup.vue +97 -97
  94. package/src/components/Laboratory/TicketCard/TicketCard.css +3 -3
  95. package/src/components/Laboratory/TicketCard/TicketCard.vue +143 -143
  96. package/src/components/Laboratory/TimeLine/TimeLineEvent.css +18 -18
  97. package/src/components/Laboratory/TimeLine/TimeLineEvent.vue +119 -119
  98. package/src/components/Laboratory/TimeLine/Timeline.css +4 -4
  99. package/src/components/Laboratory/TimeLine/Timeline.vue +30 -30
  100. package/src/components/Loader/Loader.css +78 -78
  101. package/src/components/MaintenanceBanner/MaintenanceBanner.css +353 -353
  102. package/src/components/MaintenanceBanner/MaintenanceBanner.vue +140 -140
  103. package/src/components/MaintenanceBanner/MaintenanceIllustration.vue +54 -54
  104. package/src/components/Modal/Modal.css +5 -5
  105. package/src/components/Modal/Modal.vue +29 -29
  106. package/src/components/NotificationBubble/NotificationBubble.css +4 -4
  107. package/src/components/NotificationBubble/NotificationBubble.vue +90 -90
  108. package/src/components/OtpInput/OtpInput.css +43 -43
  109. package/src/components/OtpInput/OtpInput.vue +181 -181
  110. package/src/components/PhoneInput/PhoneInput.css +151 -126
  111. package/src/components/PhoneInput/PhoneInput.vue +230 -139
  112. package/src/components/RadioGroup/RadioGroup.css +65 -0
  113. package/src/components/RadioGroup/RadioGroup.vue +134 -0
  114. package/src/components/Select/Select.css +172 -172
  115. package/src/components/Select/Select.vue +377 -377
  116. package/src/components/SelectAutocomplete/SelectAutocomplete.css +172 -172
  117. package/src/components/SelectAutocomplete/SelectAutocomplete.vue +414 -414
  118. package/src/components/TextArea/TextArea.css +269 -269
  119. package/src/components/TextArea/TextArea.vue +207 -207
  120. package/src/components/TickBox/TickBox.css +116 -116
  121. package/src/components/TickBox/TickBox.vue +172 -172
  122. package/src/components/Tile/Tile.css +106 -106
  123. package/src/components/Tile/Tile.vue +173 -173
  124. package/src/components/accessibility.css +218 -218
  125. package/src/components/index.ts +110 -109
  126. package/src/constants/iconEnums.ts +3 -3
  127. package/src/i18n/i18n.ts +15 -15
  128. package/src/i18n/locales/de.json +30 -30
  129. package/src/i18n/locales/en.json +30 -30
  130. package/src/index.ts +43 -43
  131. package/src/main.ts +11 -11
  132. package/src/pages/AccordionGroupPage.vue +873 -873
  133. package/src/pages/AllPage.vue +2483 -2365
  134. package/src/pages/SelectPage.vue +1302 -1302
  135. package/src/pages/TilePage.vue +902 -902
  136. package/src/plugins/vuetify.ts +54 -54
  137. package/src/shims-vue.d.ts +30 -30
  138. package/src/utils/index.ts +733 -733
  139. package/src/vite-env.d.ts +1 -1
  140. package/tests/unit/accessibility/component-a11y.spec.ts +657 -657
  141. package/tests/unit/components/Accordion/AccordionGroup.spec.ts +228 -228
  142. package/tests/unit/components/Accordion/AccordionItem.spec.ts +257 -257
  143. package/tests/unit/components/Appointment/AnamneseNotification.spec.ts +176 -176
  144. package/tests/unit/components/Appointment/Card/Actions.spec.ts +436 -436
  145. package/tests/unit/components/Appointment/Card/Card.spec.ts +531 -531
  146. package/tests/unit/components/Appointment/Card/Details.spec.ts +395 -395
  147. package/tests/unit/components/Audio/Audio.spec.ts +403 -403
  148. package/tests/unit/components/Audio/Waveform.spec.ts +483 -483
  149. package/tests/unit/components/Background/Background.spec.ts +177 -177
  150. package/tests/unit/components/Core/AnamneseAnswerDialog.spec.ts +344 -0
  151. package/tests/unit/components/Core/Banner.spec.ts +187 -0
  152. package/tests/unit/components/Core/Button.spec.ts +346 -346
  153. package/tests/unit/components/Core/Checkbox.spec.ts +544 -544
  154. package/tests/unit/components/Core/DateInput.spec.ts +702 -702
  155. package/tests/unit/components/Core/Dialog.spec.ts +448 -448
  156. package/tests/unit/components/Core/EditField.spec.ts +541 -541
  157. package/tests/unit/components/Core/Input.spec.ts +512 -512
  158. package/tests/unit/components/Core/List.spec.ts +163 -0
  159. package/tests/unit/components/Core/ListItem.spec.ts +205 -0
  160. package/tests/unit/components/Core/Modal.spec.ts +518 -518
  161. package/tests/unit/components/Core/NotificationBubble.spec.ts +606 -606
  162. package/tests/unit/components/Core/OtpInput.spec.ts +708 -708
  163. package/tests/unit/components/Core/PhoneInput.spec.ts +757 -619
  164. package/tests/unit/components/Core/RadioGroup.spec.ts +318 -0
  165. package/tests/unit/components/Core/Select.spec.ts +712 -712
  166. package/tests/unit/components/Core/SelectAutocomplete.spec.ts +361 -0
  167. package/tests/unit/components/Core/TextArea.spec.ts +565 -565
  168. package/tests/unit/components/Core/TickBox.spec.ts +836 -836
  169. package/tests/unit/components/Core/Tile.spec.ts +286 -0
  170. package/tests/unit/components/DateInput/DateInput.spec.ts +128 -0
  171. package/tests/unit/components/ErrorPage/ErrorPage.spec.ts +313 -313
  172. package/tests/unit/components/ErrorPage/ErrorPageLogo.spec.ts +153 -153
  173. package/tests/unit/components/IconBullet/IconBullet.spec.ts +356 -356
  174. package/tests/unit/components/IconBullet/IconBulletList.spec.ts +371 -371
  175. package/tests/unit/components/Icons/AdvanceAppointments.spec.ts +186 -186
  176. package/tests/unit/components/Icons/Audio/CloudFailed.spec.ts +108 -108
  177. package/tests/unit/components/Icons/Audio/CloudSaved.spec.ts +149 -149
  178. package/tests/unit/components/Icons/Audio/Delete.spec.ts +158 -158
  179. package/tests/unit/components/Icons/Audio/Pause.spec.ts +208 -208
  180. package/tests/unit/components/Icons/Audio/Play.spec.ts +217 -217
  181. package/tests/unit/components/Icons/CalendarNotification.spec.ts +193 -193
  182. package/tests/unit/components/Icons/Chair.spec.ts +241 -241
  183. package/tests/unit/components/Icons/ChairNotification.spec.ts +318 -318
  184. package/tests/unit/components/Icons/Circle.spec.ts +255 -255
  185. package/tests/unit/components/Icons/FavIcon.spec.ts +259 -259
  186. package/tests/unit/components/Icons/FilledCircle.spec.ts +274 -274
  187. package/tests/unit/components/Icons/Group3.spec.ts +362 -362
  188. package/tests/unit/components/Icons/Logo.spec.ts +229 -229
  189. package/tests/unit/components/Icons/MiniLogo.spec.ts +38 -38
  190. package/tests/unit/components/Icons/RingNotification.spec.ts +400 -400
  191. package/tests/unit/components/Icons/SolidArrowRight.spec.ts +49 -49
  192. package/tests/unit/components/Icons/calendar.spec.ts +293 -293
  193. package/tests/unit/components/Icons/checkbox.spec.ts +315 -315
  194. package/tests/unit/components/Icons/outlineChecked.spec.ts +441 -441
  195. package/tests/unit/components/Icons/play.spec.ts +315 -315
  196. package/tests/unit/components/Laboratory/AppointmentCard.spec.ts +167 -167
  197. package/tests/unit/components/Laboratory/ChatBoxImage.spec.ts +179 -179
  198. package/tests/unit/components/Laboratory/ChatMessage.spec.ts +263 -263
  199. package/tests/unit/components/Laboratory/ChatMessageBadge.spec.ts +282 -282
  200. package/tests/unit/components/Laboratory/ChatNotification.spec.ts +256 -256
  201. package/tests/unit/components/Laboratory/DocumentCard.spec.ts +228 -228
  202. package/tests/unit/components/Laboratory/DocumentCardItem.spec.ts +236 -236
  203. package/tests/unit/components/Laboratory/InfoCard.spec.ts +308 -308
  204. package/tests/unit/components/Laboratory/MainColumnsBar.spec.ts +251 -251
  205. package/tests/unit/components/Laboratory/ProgressCircle.spec.ts +290 -290
  206. package/tests/unit/components/Laboratory/ProgressLinear.spec.ts +275 -275
  207. package/tests/unit/components/Laboratory/SelectionColumnBar.spec.ts +288 -288
  208. package/tests/unit/components/Laboratory/StatusNotification.spec.ts +296 -296
  209. package/tests/unit/components/Laboratory/TagLabel.spec.ts +353 -353
  210. package/tests/unit/components/Laboratory/TagLabelGroup.spec.ts +377 -377
  211. package/tests/unit/components/Laboratory/TicketCard.spec.ts +351 -351
  212. package/tests/unit/components/Laboratory/TimeLineEvent.spec.ts +381 -381
  213. package/tests/unit/components/Laboratory/Timeline.spec.ts +419 -419
  214. package/tests/unit/components/Loader/Loader.spec.ts +197 -197
  215. package/tests/unit/components/MaintenanceBanner/MaintenanceBanner.spec.ts +302 -302
  216. package/tests/unit/constants/iconEnums.spec.ts +39 -39
  217. package/tests/unit/i18n/i18n.spec.ts +88 -88
  218. package/tests/unit/plugins/vuetify.spec.ts +182 -182
  219. package/tests/unit/setup.ts +237 -237
  220. package/tests/unit/src/components/index.spec.ts.skip +192 -192
  221. package/tests/unit/src/index.spec.ts.skip +182 -182
  222. package/tests/unit/src/main.spec.ts +111 -111
  223. package/tests/unit/utils/accessibility.spec.ts +318 -318
  224. package/tests/unit/utils/anamnese.spec.ts +531 -0
  225. package/tsconfig.json +26 -26
  226. package/vite.config.ts +29 -29
  227. package/vitest.config.ts +91 -91
@@ -1,657 +1,657 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { mount } from '@vue/test-utils';
3
- import { nextTick } from 'vue';
4
- import Button from '@components/Button/Button.vue';
5
- import Checkbox from '@components/CheckBox/Checkbox.vue';
6
- import Dialog from '@components/Dialog/Dialog.vue';
7
- import Loader from '@components/Loader/Loader.vue';
8
- import DateInput from '@components/DateInput/DateInput.vue';
9
-
10
- // Mock vue-i18n for DateInput
11
- vi.mock('vue-i18n', () => ({
12
- useI18n: () => ({
13
- t: (key: string, fallback?: string | Record<string, any>) => {
14
- const translations: Record<string, string> = {
15
- 'wl.date_input.placeholder': 'TT.MM.JJJJ',
16
- 'wl.date_input.invalid_date': 'Bitte ein gültiges Datum eingeben',
17
- 'wl.date_input.invalid_char': 'Dieses Zeichen ist nicht erlaubt',
18
- 'wl.date_input.format_error': 'Das Datum muss im Format TT.MM.JJJJ sein',
19
- 'wl.input.placeholder': 'Enter text',
20
- };
21
- if (typeof fallback === 'string') return fallback;
22
- return translations[key] || key;
23
- },
24
- }),
25
- }));
26
-
27
- /**
28
- * Component Accessibility Tests
29
- *
30
- * These tests verify WCAG 2.1 AA compliance for all components.
31
- * Required for German accessibility requirements (BITV 2.0).
32
- */
33
-
34
- describe('Component Accessibility Compliance', () => {
35
- describe('Button Component - WCAG Compliance', () => {
36
- it('has focus-visible styles defined', () => {
37
- const wrapper = mount(Button);
38
-
39
- // Button should have the wl-button class that has focus-visible styles
40
- expect(wrapper.find('.wl-button').exists()).toBe(true);
41
- });
42
-
43
- it('supports keyboard activation', async () => {
44
- const wrapper = mount(Button);
45
- const button = wrapper.find('[data-testid="root"]');
46
-
47
- // Button should be focusable
48
- expect(button.element.tagName.toLowerCase()).toBe('button');
49
- });
50
-
51
- it('has accessible disabled state', () => {
52
- const wrapper = mount(Button, {
53
- props: { disabled: true }
54
- });
55
-
56
- expect(wrapper.vm.disabled).toBe(true);
57
- // Disabled buttons should have disabled attribute
58
- expect(wrapper.find('button').attributes('disabled')).toBeDefined();
59
- });
60
-
61
- it('has visible text label', () => {
62
- const wrapper = mount(Button, {
63
- props: { label: 'Submit Form' }
64
- });
65
-
66
- expect(wrapper.text()).toBe('Submit Form');
67
- });
68
-
69
- it('supports loading state announcement', () => {
70
- const wrapper = mount(Button, {
71
- props: { loading: true }
72
- });
73
-
74
- expect(wrapper.vm.loading).toBe(true);
75
- });
76
- });
77
-
78
- describe('Checkbox Component - WCAG Compliance', () => {
79
- it('has proper label association', () => {
80
- const wrapper = mount(Checkbox, {
81
- props: {
82
- label: 'Accept terms',
83
- value: 'terms'
84
- }
85
- });
86
-
87
- const label = wrapper.find('label');
88
- const input = wrapper.find('input[type="checkbox"]');
89
-
90
- expect(label.exists()).toBe(true);
91
- expect(input.exists()).toBe(true);
92
-
93
- // Label should have for attribute matching input id
94
- const inputId = input.attributes('id');
95
- const labelFor = label.attributes('for');
96
- expect(labelFor).toBe(inputId);
97
- });
98
-
99
- it('has aria-checked attribute', () => {
100
- const wrapper = mount(Checkbox, {
101
- props: {
102
- label: 'Test',
103
- value: 'test',
104
- modelValue: true
105
- }
106
- });
107
-
108
- const input = wrapper.find('input');
109
- expect(input.attributes('aria-checked')).toBeDefined();
110
- });
111
-
112
- it('has aria-disabled for disabled state', () => {
113
- const wrapper = mount(Checkbox, {
114
- props: {
115
- label: 'Test',
116
- value: 'test',
117
- disabled: true
118
- }
119
- });
120
-
121
- const input = wrapper.find('input');
122
- expect(input.attributes('aria-disabled')).toBe('true');
123
- expect(input.attributes('disabled')).toBeDefined();
124
- });
125
-
126
- it('has aria-invalid for error state', () => {
127
- const wrapper = mount(Checkbox, {
128
- props: {
129
- label: 'Test',
130
- value: 'test',
131
- error: true
132
- }
133
- });
134
-
135
- const input = wrapper.find('input');
136
- expect(input.attributes('aria-invalid')).toBe('true');
137
- });
138
-
139
- it('has aria-describedby for error messages', () => {
140
- const wrapper = mount(Checkbox, {
141
- props: {
142
- label: 'Test',
143
- value: 'test',
144
- error: true
145
- }
146
- });
147
-
148
- const input = wrapper.find('input');
149
- expect(input.attributes('aria-describedby')).toBeDefined();
150
- });
151
-
152
- it('is keyboard focusable', () => {
153
- const wrapper = mount(Checkbox, {
154
- props: {
155
- label: 'Test',
156
- value: 'test'
157
- }
158
- });
159
-
160
- const input = wrapper.find('input');
161
- // Input should be focusable (no tabindex=-1)
162
- expect(input.attributes('tabindex')).not.toBe('-1');
163
- });
164
- });
165
-
166
- describe('Dialog Component - WCAG Compliance', () => {
167
- it('has role="dialog" attribute', () => {
168
- const wrapper = mount(Dialog);
169
-
170
- const dialog = wrapper.find('v-dialog-stub, .v-dialog');
171
- expect(dialog.exists()).toBe(true);
172
- expect(dialog.attributes('role')).toBe('dialog');
173
- });
174
-
175
- it('has aria-modal="true"', () => {
176
- const wrapper = mount(Dialog);
177
-
178
- const dialog = wrapper.find('v-dialog-stub, .v-dialog');
179
- expect(dialog.exists()).toBe(true);
180
- expect(dialog.attributes('aria-modal')).toBe('true');
181
- });
182
-
183
- it('has aria-labelledby for title', () => {
184
- const wrapper = mount(Dialog);
185
-
186
- const dialog = wrapper.find('v-dialog-stub, .v-dialog');
187
- expect(dialog.exists()).toBe(true);
188
- expect(dialog.attributes('aria-labelledby')).toBeDefined();
189
- });
190
-
191
- it('provides titleId to slot for proper labeling', () => {
192
- const wrapper = mount(Dialog, {
193
- slots: {
194
- content: '<template #content="{ titleId }"><h2 :id="titleId">Dialog Title</h2></template>'
195
- }
196
- });
197
- const dialog = wrapper.find('v-dialog-stub, .v-dialog');
198
- expect(dialog.exists()).toBe(true);
199
-
200
- const labelledBy = dialog.attributes('aria-labelledby');
201
- expect(labelledBy).toBeDefined();
202
-
203
- const heading = wrapper.find('h2');
204
- expect(heading.exists()).toBe(true);
205
- // The heading should have an id that matches aria-labelledby
206
- expect(heading.attributes('id')).toBe(labelledBy);
207
- });
208
-
209
- it('supports escape key to close', async () => {
210
- const wrapper = mount(Dialog);
211
- const dialog = wrapper.find('v-dialog-stub, .v-dialog');
212
- expect(dialog.exists()).toBe(true);
213
- expect(dialog.attributes('role')).toBe('dialog');
214
- expect(dialog.attributes('aria-modal')).toBe('true');
215
- });
216
- });
217
-
218
- describe('Loader Component - WCAG Compliance', () => {
219
- it('has role="alert" for loading announcement', () => {
220
- const wrapper = mount(Loader);
221
-
222
- expect(wrapper.find('[role="alert"]').exists()).toBe(true);
223
- });
224
-
225
- it('has aria-live="assertive"', () => {
226
- const wrapper = mount(Loader);
227
-
228
- expect(wrapper.find('[aria-live="assertive"]').exists()).toBe(true);
229
- });
230
-
231
- it('has aria-busy="true"', () => {
232
- const wrapper = mount(Loader);
233
-
234
- expect(wrapper.find('[aria-busy="true"]').exists()).toBe(true);
235
- });
236
-
237
- it('has aria-hidden on decorative SVG', () => {
238
- const wrapper = mount(Loader);
239
-
240
- const svg = wrapper.find('svg');
241
- expect(svg.attributes('aria-hidden')).toBe('true');
242
- });
243
-
244
- it('has role="status" for spinner', () => {
245
- const wrapper = mount(Loader);
246
-
247
- expect(wrapper.find('[role="status"]').exists()).toBe(true);
248
- });
249
-
250
- it('has screen reader text for loading announcement', () => {
251
- const wrapper = mount(Loader);
252
-
253
- const srText = wrapper.find('.sr-only');
254
- expect(srText.exists()).toBe(true);
255
- expect(srText.text()).toContain('Loading');
256
- });
257
- });
258
-
259
- describe('DateInput Component - WCAG Compliance', () => {
260
- const mountDateInput = (props = {}) => {
261
- return mount(DateInput, {
262
- props,
263
- global: {
264
- stubs: {
265
- 'v-icon': {
266
- template: '<i class="v-icon" data-testid="v-icon" aria-hidden="true"></i>',
267
- props: ['icon', 'color']
268
- }
269
- }
270
- }
271
- });
272
- };
273
-
274
- it('has proper input identification', () => {
275
- const wrapper = mountDateInput();
276
- const input = wrapper.find('input');
277
- expect(input.attributes('id')).toBe('date-input');
278
- wrapper.unmount();
279
- });
280
-
281
- it('uses type="tel" for numeric keyboard on mobile', () => {
282
- const wrapper = mountDateInput();
283
- const input = wrapper.find('input');
284
- expect(input.attributes('type')).toBe('tel');
285
- wrapper.unmount();
286
- });
287
-
288
- it('has placeholder indicating expected format', () => {
289
- const wrapper = mountDateInput();
290
- const input = wrapper.find('input');
291
- expect(input.attributes('placeholder')).toBe('TT.MM.JJJJ');
292
- wrapper.unmount();
293
- });
294
-
295
- it('has wl-date-input class for focus styling', () => {
296
- const wrapper = mountDateInput();
297
- expect(wrapper.find('.wl-date-input').exists()).toBe(true);
298
- wrapper.unmount();
299
- });
300
-
301
- it('calendar picker is keyboard accessible', () => {
302
- const wrapper = mountDateInput();
303
- const dateWrapper = wrapper.find('.date-wrapper');
304
- expect(dateWrapper.attributes('tabindex')).toBe('0');
305
- wrapper.unmount();
306
- });
307
-
308
- it('native date input is hidden from tab order', () => {
309
- const wrapper = mountDateInput();
310
- const nativeDate = wrapper.find('.native-date');
311
- expect(nativeDate.attributes('tabindex')).toBe('-1');
312
- wrapper.unmount();
313
- });
314
-
315
- it('provides error feedback for invalid input', async () => {
316
- vi.useFakeTimers();
317
- const wrapper = mountDateInput();
318
- const input = wrapper.find('input');
319
-
320
- await input.setValue('abc');
321
- await input.trigger('input');
322
-
323
- expect(wrapper.vm.inputState).toBe('error');
324
- expect(wrapper.vm.inputMessage).toBe('Dieses Zeichen ist nicht erlaubt');
325
-
326
- vi.useRealTimers();
327
- wrapper.unmount();
328
- });
329
-
330
- it('provides error feedback for format violations', async () => {
331
- vi.useFakeTimers();
332
- const wrapper = mountDateInput();
333
- const input = wrapper.find('input');
334
-
335
- await input.setValue('12.05.20245');
336
- await input.trigger('input');
337
-
338
- expect(wrapper.vm.inputState).toBe('error');
339
- expect(wrapper.vm.inputMessage).toBe('Das Datum muss im Format TT.MM.JJJJ sein');
340
-
341
- vi.useRealTimers();
342
- wrapper.unmount();
343
- });
344
-
345
- it('clears error state on valid input', async () => {
346
- const wrapper = mountDateInput();
347
- const input = wrapper.find('input');
348
-
349
- await input.setValue('15062024');
350
- await input.trigger('input');
351
-
352
- expect(wrapper.vm.inputState).toBe('success');
353
- expect(wrapper.vm.inputMessage).toBe('');
354
- wrapper.unmount();
355
- });
356
-
357
- it('native date picker has min/max constraints', () => {
358
- const minDate = new Date(2024, 0, 1);
359
- const maxDate = new Date(2024, 11, 31);
360
- const wrapper = mountDateInput({ minDate, maxDate });
361
-
362
- const nativeDate = wrapper.find('.native-date');
363
- expect(nativeDate.attributes('min')).toBe('2024-01-01');
364
- expect(nativeDate.attributes('max')).toBe('2024-12-31');
365
- wrapper.unmount();
366
- });
367
- });
368
- });
369
-
370
- describe('Focus Management', () => {
371
- it('Button is focusable', () => {
372
- const wrapper = mount(Button);
373
- const button = wrapper.find('button');
374
-
375
- expect(button.exists()).toBe(true);
376
- expect(button.attributes('tabindex')).not.toBe('-1');
377
- });
378
-
379
- it('Disabled button is still focusable for screen readers', () => {
380
- const wrapper = mount(Button, {
381
- props: { disabled: true }
382
- });
383
-
384
- const button = wrapper.find('button');
385
- // Disabled buttons should still be in tab order but non-interactive
386
- expect(button.attributes('disabled')).toBeDefined();
387
- });
388
-
389
- it('Checkbox input is focusable', () => {
390
- const wrapper = mount(Checkbox, {
391
- props: { label: 'Test', value: 'test' }
392
- });
393
-
394
- const input = wrapper.find('input');
395
- expect(input.exists()).toBe(true);
396
- expect(input.attributes('tabindex')).not.toBe('-1');
397
- });
398
-
399
- it('DateInput is focusable', () => {
400
- const wrapper = mount(DateInput, {
401
- global: {
402
- stubs: {
403
- 'v-icon': { template: '<i class="v-icon"></i>', props: ['icon', 'color'] }
404
- }
405
- }
406
- });
407
-
408
- const input = wrapper.find('input');
409
- expect(input.exists()).toBe(true);
410
- expect(input.attributes('tabindex')).not.toBe('-1');
411
- wrapper.unmount();
412
- });
413
-
414
- it('DateInput calendar button is focusable', () => {
415
- const wrapper = mount(DateInput, {
416
- global: {
417
- stubs: {
418
- 'v-icon': { template: '<i class="v-icon"></i>', props: ['icon', 'color'] }
419
- }
420
- }
421
- });
422
-
423
- const dateWrapper = wrapper.find('.date-wrapper');
424
- expect(dateWrapper.attributes('tabindex')).toBe('0');
425
- wrapper.unmount();
426
- });
427
- });
428
-
429
- describe('Color Contrast Requirements', () => {
430
- // Note: Actual contrast testing requires computed styles
431
- // These tests verify contrast-related CSS classes exist
432
-
433
- it('Button has appropriate text color for flat variant', () => {
434
- const wrapper = mount(Button, {
435
- props: { variant: 'flat', textColor: 'white' }
436
- });
437
-
438
- // White text on colored background
439
- expect(wrapper.vm.textColor).toBe('white');
440
- });
441
-
442
- it('Error states use high-contrast red', () => {
443
- const wrapper = mount(Checkbox, {
444
- props: { label: 'Test', value: 'test', error: true }
445
- });
446
-
447
- expect(wrapper.find('.input-error').exists()).toBe(true);
448
- });
449
-
450
- it('Success states use high-contrast green', () => {
451
- const wrapper = mount(Checkbox, {
452
- props: { label: 'Test', value: 'test', success: true }
453
- });
454
-
455
- expect(wrapper.find('.input-success').exists()).toBe(true);
456
- });
457
- });
458
-
459
- describe('Screen Reader Support', () => {
460
- it('Checkbox announces checked state', () => {
461
- const wrapper = mount(Checkbox, {
462
- props: {
463
- label: 'Test',
464
- value: 'test',
465
- modelValue: true
466
- }
467
- });
468
-
469
- const input = wrapper.find('input');
470
- expect(input.attributes('aria-checked')).toBe('true');
471
- });
472
-
473
- it('Checkbox announces unchecked state', () => {
474
- const wrapper = mount(Checkbox, {
475
- props: {
476
- label: 'Test',
477
- value: 'test',
478
- modelValue: false
479
- }
480
- });
481
-
482
- const input = wrapper.find('input');
483
- expect(input.attributes('aria-checked')).toBe('false');
484
- });
485
-
486
- it('Loader announces loading state with text', () => {
487
- const wrapper = mount(Loader);
488
-
489
- // Should have live region
490
- expect(wrapper.find('[aria-live]').exists()).toBe(true);
491
- expect(wrapper.find('[aria-busy="true"]').exists()).toBe(true);
492
-
493
- // Should have screen reader text
494
- const srText = wrapper.find('.sr-only');
495
- expect(srText.exists()).toBe(true);
496
- expect(srText.text()).toBeTruthy();
497
- });
498
-
499
- it('Loader decorative elements are hidden from screen readers', () => {
500
- const wrapper = mount(Loader);
501
-
502
- const svg = wrapper.find('svg');
503
- expect(svg.attributes('aria-hidden')).toBe('true');
504
- });
505
-
506
- it('Checkbox error state is announced', () => {
507
- const wrapper = mount(Checkbox, {
508
- props: {
509
- label: 'Test',
510
- value: 'test',
511
- error: true
512
- }
513
- });
514
-
515
- const input = wrapper.find('input');
516
- expect(input.attributes('aria-invalid')).toBe('true');
517
- });
518
-
519
- it('Dialog is announced as modal', () => {
520
- const wrapper = mount(Dialog);
521
-
522
- expect(wrapper.html()).toContain('aria-modal="true"');
523
- expect(wrapper.html()).toContain('role="dialog"');
524
- });
525
- });
526
-
527
- describe('Keyboard Navigation', () => {
528
- it('Button responds to keyboard events', async () => {
529
- const wrapper = mount(Button);
530
- const button = wrapper.find('button');
531
-
532
- // Button element naturally supports Enter and Space
533
- expect(button.element.tagName.toLowerCase()).toBe('button');
534
- });
535
-
536
- it('Checkbox can be toggled with Space key', async () => {
537
- const wrapper = mount(Checkbox, {
538
- props: {
539
- label: 'Test',
540
- value: 'test',
541
- modelValue: false
542
- }
543
- });
544
-
545
- const input = wrapper.find('input');
546
-
547
- // Native checkbox responds to space
548
- expect(input.element.type).toBe('checkbox');
549
- });
550
-
551
- it('DateInput can be operated via keyboard', async () => {
552
- const wrapper = mount(DateInput, {
553
- global: {
554
- stubs: {
555
- 'v-icon': { template: '<i class="v-icon"></i>', props: ['icon', 'color'] }
556
- }
557
- }
558
- });
559
-
560
- const input = wrapper.find('input');
561
- // Input should accept keyboard input
562
- expect(input.attributes('type')).toBe('tel');
563
- wrapper.unmount();
564
- });
565
-
566
- it('DateInput calendar picker responds to Enter key', async () => {
567
- const wrapper = mount(DateInput, {
568
- global: {
569
- stubs: {
570
- 'v-icon': { template: '<i class="v-icon"></i>', props: ['icon', 'color'] }
571
- }
572
- }
573
- });
574
-
575
- const dateWrapper = wrapper.find('.date-wrapper');
576
- // Has keydown.enter handler
577
- expect(dateWrapper.attributes('tabindex')).toBe('0');
578
- wrapper.unmount();
579
- });
580
- });
581
-
582
- describe('WCAG 2.1 AA Criteria Verification', () => {
583
- describe('1.3.1 Info and Relationships', () => {
584
- it('Form controls have proper labels', () => {
585
- const wrapper = mount(Checkbox, {
586
- props: { label: 'Accept terms', value: 'terms' }
587
- });
588
-
589
- const label = wrapper.find('label');
590
- const input = wrapper.find('input');
591
-
592
- expect(label.attributes('for')).toBe(input.attributes('id'));
593
- });
594
- });
595
-
596
- describe('1.4.3 Contrast (Minimum)', () => {
597
- it('Text colors are defined for readability', () => {
598
- const wrapper = mount(Button, {
599
- props: { label: 'Test', textColor: 'white' }
600
- });
601
-
602
- // White on dark background should meet contrast
603
- expect(wrapper.vm.textColor).toBe('white');
604
- });
605
- });
606
-
607
- describe('2.1.1 Keyboard', () => {
608
- it('All interactive elements are keyboard accessible', () => {
609
- const buttonWrapper = mount(Button);
610
- const checkboxWrapper = mount(Checkbox, {
611
- props: { label: 'Test', value: 'test' }
612
- });
613
-
614
- expect(buttonWrapper.find('button').exists()).toBe(true);
615
- expect(checkboxWrapper.find('input').exists()).toBe(true);
616
- });
617
- });
618
-
619
- describe('2.4.7 Focus Visible', () => {
620
- it('Button has focus-visible class capability', () => {
621
- const wrapper = mount(Button);
622
-
623
- // Button should have wl-button class that includes focus-visible styles
624
- expect(wrapper.find('.wl-button').exists()).toBe(true);
625
- });
626
-
627
- it('Checkbox input can receive focus', () => {
628
- const wrapper = mount(Checkbox, {
629
- props: { label: 'Test', value: 'test' }
630
- });
631
-
632
- const input = wrapper.find('input');
633
- expect(input.exists()).toBe(true);
634
- });
635
- });
636
-
637
- describe('4.1.2 Name, Role, Value', () => {
638
- it('Dialog has proper role', () => {
639
- const wrapper = mount(Dialog);
640
- expect(wrapper.html()).toContain('role="dialog"');
641
- });
642
-
643
- it('Checkbox has proper aria attributes', () => {
644
- const wrapper = mount(Checkbox, {
645
- props: { label: 'Test', value: 'test', modelValue: true }
646
- });
647
-
648
- const input = wrapper.find('input');
649
- expect(input.attributes('aria-checked')).toBeDefined();
650
- });
651
-
652
- it('Loader has proper status role', () => {
653
- const wrapper = mount(Loader);
654
- expect(wrapper.find('[role="status"]').exists()).toBe(true);
655
- });
656
- });
657
- });
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import { nextTick } from 'vue';
4
+ import Button from '@components/Button/Button.vue';
5
+ import Checkbox from '@components/CheckBox/Checkbox.vue';
6
+ import Dialog from '@components/Dialog/Dialog.vue';
7
+ import Loader from '@components/Loader/Loader.vue';
8
+ import DateInput from '@components/DateInput/DateInput.vue';
9
+
10
+ // Mock vue-i18n for DateInput
11
+ vi.mock('vue-i18n', () => ({
12
+ useI18n: () => ({
13
+ t: (key: string, fallback?: string | Record<string, any>) => {
14
+ const translations: Record<string, string> = {
15
+ 'wl.date_input.placeholder': 'TT.MM.JJJJ',
16
+ 'wl.date_input.invalid_date': 'Bitte ein gültiges Datum eingeben',
17
+ 'wl.date_input.invalid_char': 'Dieses Zeichen ist nicht erlaubt',
18
+ 'wl.date_input.format_error': 'Das Datum muss im Format TT.MM.JJJJ sein',
19
+ 'wl.input.placeholder': 'Enter text',
20
+ };
21
+ if (typeof fallback === 'string') return fallback;
22
+ return translations[key] || key;
23
+ },
24
+ }),
25
+ }));
26
+
27
+ /**
28
+ * Component Accessibility Tests
29
+ *
30
+ * These tests verify WCAG 2.1 AA compliance for all components.
31
+ * Required for German accessibility requirements (BITV 2.0).
32
+ */
33
+
34
+ describe('Component Accessibility Compliance', () => {
35
+ describe('Button Component - WCAG Compliance', () => {
36
+ it('has focus-visible styles defined', () => {
37
+ const wrapper = mount(Button);
38
+
39
+ // Button should have the wl-button class that has focus-visible styles
40
+ expect(wrapper.find('.wl-button').exists()).toBe(true);
41
+ });
42
+
43
+ it('supports keyboard activation', async () => {
44
+ const wrapper = mount(Button);
45
+ const button = wrapper.find('[data-testid="root"]');
46
+
47
+ // Button should be focusable
48
+ expect(button.element.tagName.toLowerCase()).toBe('button');
49
+ });
50
+
51
+ it('has accessible disabled state', () => {
52
+ const wrapper = mount(Button, {
53
+ props: { disabled: true }
54
+ });
55
+
56
+ expect(wrapper.vm.disabled).toBe(true);
57
+ // Disabled buttons should have disabled attribute
58
+ expect(wrapper.find('button').attributes('disabled')).toBeDefined();
59
+ });
60
+
61
+ it('has visible text label', () => {
62
+ const wrapper = mount(Button, {
63
+ props: { label: 'Submit Form' }
64
+ });
65
+
66
+ expect(wrapper.text()).toBe('Submit Form');
67
+ });
68
+
69
+ it('supports loading state announcement', () => {
70
+ const wrapper = mount(Button, {
71
+ props: { loading: true }
72
+ });
73
+
74
+ expect(wrapper.vm.loading).toBe(true);
75
+ });
76
+ });
77
+
78
+ describe('Checkbox Component - WCAG Compliance', () => {
79
+ it('has proper label association', () => {
80
+ const wrapper = mount(Checkbox, {
81
+ props: {
82
+ label: 'Accept terms',
83
+ value: 'terms'
84
+ }
85
+ });
86
+
87
+ const label = wrapper.find('label');
88
+ const input = wrapper.find('input[type="checkbox"]');
89
+
90
+ expect(label.exists()).toBe(true);
91
+ expect(input.exists()).toBe(true);
92
+
93
+ // Label should have for attribute matching input id
94
+ const inputId = input.attributes('id');
95
+ const labelFor = label.attributes('for');
96
+ expect(labelFor).toBe(inputId);
97
+ });
98
+
99
+ it('has aria-checked attribute', () => {
100
+ const wrapper = mount(Checkbox, {
101
+ props: {
102
+ label: 'Test',
103
+ value: 'test',
104
+ modelValue: true
105
+ }
106
+ });
107
+
108
+ const input = wrapper.find('input');
109
+ expect(input.attributes('aria-checked')).toBeDefined();
110
+ });
111
+
112
+ it('has aria-disabled for disabled state', () => {
113
+ const wrapper = mount(Checkbox, {
114
+ props: {
115
+ label: 'Test',
116
+ value: 'test',
117
+ disabled: true
118
+ }
119
+ });
120
+
121
+ const input = wrapper.find('input');
122
+ expect(input.attributes('aria-disabled')).toBe('true');
123
+ expect(input.attributes('disabled')).toBeDefined();
124
+ });
125
+
126
+ it('has aria-invalid for error state', () => {
127
+ const wrapper = mount(Checkbox, {
128
+ props: {
129
+ label: 'Test',
130
+ value: 'test',
131
+ error: true
132
+ }
133
+ });
134
+
135
+ const input = wrapper.find('input');
136
+ expect(input.attributes('aria-invalid')).toBe('true');
137
+ });
138
+
139
+ it('has aria-describedby for error messages', () => {
140
+ const wrapper = mount(Checkbox, {
141
+ props: {
142
+ label: 'Test',
143
+ value: 'test',
144
+ error: true
145
+ }
146
+ });
147
+
148
+ const input = wrapper.find('input');
149
+ expect(input.attributes('aria-describedby')).toBeDefined();
150
+ });
151
+
152
+ it('is keyboard focusable', () => {
153
+ const wrapper = mount(Checkbox, {
154
+ props: {
155
+ label: 'Test',
156
+ value: 'test'
157
+ }
158
+ });
159
+
160
+ const input = wrapper.find('input');
161
+ // Input should be focusable (no tabindex=-1)
162
+ expect(input.attributes('tabindex')).not.toBe('-1');
163
+ });
164
+ });
165
+
166
+ describe('Dialog Component - WCAG Compliance', () => {
167
+ it('has role="dialog" attribute', () => {
168
+ const wrapper = mount(Dialog);
169
+
170
+ const dialog = wrapper.find('v-dialog-stub, .v-dialog');
171
+ expect(dialog.exists()).toBe(true);
172
+ expect(dialog.attributes('role')).toBe('dialog');
173
+ });
174
+
175
+ it('has aria-modal="true"', () => {
176
+ const wrapper = mount(Dialog);
177
+
178
+ const dialog = wrapper.find('v-dialog-stub, .v-dialog');
179
+ expect(dialog.exists()).toBe(true);
180
+ expect(dialog.attributes('aria-modal')).toBe('true');
181
+ });
182
+
183
+ it('has aria-labelledby for title', () => {
184
+ const wrapper = mount(Dialog);
185
+
186
+ const dialog = wrapper.find('v-dialog-stub, .v-dialog');
187
+ expect(dialog.exists()).toBe(true);
188
+ expect(dialog.attributes('aria-labelledby')).toBeDefined();
189
+ });
190
+
191
+ it('provides titleId to slot for proper labeling', () => {
192
+ const wrapper = mount(Dialog, {
193
+ slots: {
194
+ content: '<template #content="{ titleId }"><h2 :id="titleId">Dialog Title</h2></template>'
195
+ }
196
+ });
197
+ const dialog = wrapper.find('v-dialog-stub, .v-dialog');
198
+ expect(dialog.exists()).toBe(true);
199
+
200
+ const labelledBy = dialog.attributes('aria-labelledby');
201
+ expect(labelledBy).toBeDefined();
202
+
203
+ const heading = wrapper.find('h2');
204
+ expect(heading.exists()).toBe(true);
205
+ // The heading should have an id that matches aria-labelledby
206
+ expect(heading.attributes('id')).toBe(labelledBy);
207
+ });
208
+
209
+ it('supports escape key to close', async () => {
210
+ const wrapper = mount(Dialog);
211
+ const dialog = wrapper.find('v-dialog-stub, .v-dialog');
212
+ expect(dialog.exists()).toBe(true);
213
+ expect(dialog.attributes('role')).toBe('dialog');
214
+ expect(dialog.attributes('aria-modal')).toBe('true');
215
+ });
216
+ });
217
+
218
+ describe('Loader Component - WCAG Compliance', () => {
219
+ it('has role="alert" for loading announcement', () => {
220
+ const wrapper = mount(Loader);
221
+
222
+ expect(wrapper.find('[role="alert"]').exists()).toBe(true);
223
+ });
224
+
225
+ it('has aria-live="assertive"', () => {
226
+ const wrapper = mount(Loader);
227
+
228
+ expect(wrapper.find('[aria-live="assertive"]').exists()).toBe(true);
229
+ });
230
+
231
+ it('has aria-busy="true"', () => {
232
+ const wrapper = mount(Loader);
233
+
234
+ expect(wrapper.find('[aria-busy="true"]').exists()).toBe(true);
235
+ });
236
+
237
+ it('has aria-hidden on decorative SVG', () => {
238
+ const wrapper = mount(Loader);
239
+
240
+ const svg = wrapper.find('svg');
241
+ expect(svg.attributes('aria-hidden')).toBe('true');
242
+ });
243
+
244
+ it('has role="status" for spinner', () => {
245
+ const wrapper = mount(Loader);
246
+
247
+ expect(wrapper.find('[role="status"]').exists()).toBe(true);
248
+ });
249
+
250
+ it('has screen reader text for loading announcement', () => {
251
+ const wrapper = mount(Loader);
252
+
253
+ const srText = wrapper.find('.sr-only');
254
+ expect(srText.exists()).toBe(true);
255
+ expect(srText.text()).toContain('Loading');
256
+ });
257
+ });
258
+
259
+ describe('DateInput Component - WCAG Compliance', () => {
260
+ const mountDateInput = (props = {}) => {
261
+ return mount(DateInput, {
262
+ props,
263
+ global: {
264
+ stubs: {
265
+ 'v-icon': {
266
+ template: '<i class="v-icon" data-testid="v-icon" aria-hidden="true"></i>',
267
+ props: ['icon', 'color']
268
+ }
269
+ }
270
+ }
271
+ });
272
+ };
273
+
274
+ it('has proper input identification', () => {
275
+ const wrapper = mountDateInput();
276
+ const input = wrapper.find('input');
277
+ expect(input.attributes('id')).toBe('date-input');
278
+ wrapper.unmount();
279
+ });
280
+
281
+ it('uses type="tel" for numeric keyboard on mobile', () => {
282
+ const wrapper = mountDateInput();
283
+ const input = wrapper.find('input');
284
+ expect(input.attributes('type')).toBe('tel');
285
+ wrapper.unmount();
286
+ });
287
+
288
+ it('has placeholder indicating expected format', () => {
289
+ const wrapper = mountDateInput();
290
+ const input = wrapper.find('input');
291
+ expect(input.attributes('placeholder')).toBe('TT.MM.JJJJ');
292
+ wrapper.unmount();
293
+ });
294
+
295
+ it('has wl-date-input class for focus styling', () => {
296
+ const wrapper = mountDateInput();
297
+ expect(wrapper.find('.wl-date-input').exists()).toBe(true);
298
+ wrapper.unmount();
299
+ });
300
+
301
+ it('calendar picker is keyboard accessible', () => {
302
+ const wrapper = mountDateInput();
303
+ const dateWrapper = wrapper.find('.date-wrapper');
304
+ expect(dateWrapper.attributes('tabindex')).toBe('0');
305
+ wrapper.unmount();
306
+ });
307
+
308
+ it('native date input is hidden from tab order', () => {
309
+ const wrapper = mountDateInput();
310
+ const nativeDate = wrapper.find('.native-date');
311
+ expect(nativeDate.attributes('tabindex')).toBe('-1');
312
+ wrapper.unmount();
313
+ });
314
+
315
+ it('provides error feedback for invalid input', async () => {
316
+ vi.useFakeTimers();
317
+ const wrapper = mountDateInput();
318
+ const input = wrapper.find('input');
319
+
320
+ await input.setValue('abc');
321
+ await input.trigger('input');
322
+
323
+ expect(wrapper.vm.inputState).toBe('error');
324
+ expect(wrapper.vm.inputMessage).toBe('Dieses Zeichen ist nicht erlaubt');
325
+
326
+ vi.useRealTimers();
327
+ wrapper.unmount();
328
+ });
329
+
330
+ it('provides error feedback for format violations', async () => {
331
+ vi.useFakeTimers();
332
+ const wrapper = mountDateInput();
333
+ const input = wrapper.find('input');
334
+
335
+ await input.setValue('12.05.20245');
336
+ await input.trigger('input');
337
+
338
+ expect(wrapper.vm.inputState).toBe('error');
339
+ expect(wrapper.vm.inputMessage).toBe('Das Datum muss im Format TT.MM.JJJJ sein');
340
+
341
+ vi.useRealTimers();
342
+ wrapper.unmount();
343
+ });
344
+
345
+ it('clears error state on valid input', async () => {
346
+ const wrapper = mountDateInput();
347
+ const input = wrapper.find('input');
348
+
349
+ await input.setValue('15062024');
350
+ await input.trigger('input');
351
+
352
+ expect(wrapper.vm.inputState).toBe('success');
353
+ expect(wrapper.vm.inputMessage).toBe('');
354
+ wrapper.unmount();
355
+ });
356
+
357
+ it('native date picker has min/max constraints', () => {
358
+ const minDate = new Date(2024, 0, 1);
359
+ const maxDate = new Date(2024, 11, 31);
360
+ const wrapper = mountDateInput({ minDate, maxDate });
361
+
362
+ const nativeDate = wrapper.find('.native-date');
363
+ expect(nativeDate.attributes('min')).toBe('2024-01-01');
364
+ expect(nativeDate.attributes('max')).toBe('2024-12-31');
365
+ wrapper.unmount();
366
+ });
367
+ });
368
+ });
369
+
370
+ describe('Focus Management', () => {
371
+ it('Button is focusable', () => {
372
+ const wrapper = mount(Button);
373
+ const button = wrapper.find('button');
374
+
375
+ expect(button.exists()).toBe(true);
376
+ expect(button.attributes('tabindex')).not.toBe('-1');
377
+ });
378
+
379
+ it('Disabled button is still focusable for screen readers', () => {
380
+ const wrapper = mount(Button, {
381
+ props: { disabled: true }
382
+ });
383
+
384
+ const button = wrapper.find('button');
385
+ // Disabled buttons should still be in tab order but non-interactive
386
+ expect(button.attributes('disabled')).toBeDefined();
387
+ });
388
+
389
+ it('Checkbox input is focusable', () => {
390
+ const wrapper = mount(Checkbox, {
391
+ props: { label: 'Test', value: 'test' }
392
+ });
393
+
394
+ const input = wrapper.find('input');
395
+ expect(input.exists()).toBe(true);
396
+ expect(input.attributes('tabindex')).not.toBe('-1');
397
+ });
398
+
399
+ it('DateInput is focusable', () => {
400
+ const wrapper = mount(DateInput, {
401
+ global: {
402
+ stubs: {
403
+ 'v-icon': { template: '<i class="v-icon"></i>', props: ['icon', 'color'] }
404
+ }
405
+ }
406
+ });
407
+
408
+ const input = wrapper.find('input');
409
+ expect(input.exists()).toBe(true);
410
+ expect(input.attributes('tabindex')).not.toBe('-1');
411
+ wrapper.unmount();
412
+ });
413
+
414
+ it('DateInput calendar button is focusable', () => {
415
+ const wrapper = mount(DateInput, {
416
+ global: {
417
+ stubs: {
418
+ 'v-icon': { template: '<i class="v-icon"></i>', props: ['icon', 'color'] }
419
+ }
420
+ }
421
+ });
422
+
423
+ const dateWrapper = wrapper.find('.date-wrapper');
424
+ expect(dateWrapper.attributes('tabindex')).toBe('0');
425
+ wrapper.unmount();
426
+ });
427
+ });
428
+
429
+ describe('Color Contrast Requirements', () => {
430
+ // Note: Actual contrast testing requires computed styles
431
+ // These tests verify contrast-related CSS classes exist
432
+
433
+ it('Button has appropriate text color for flat variant', () => {
434
+ const wrapper = mount(Button, {
435
+ props: { variant: 'flat', textColor: 'white' }
436
+ });
437
+
438
+ // White text on colored background
439
+ expect(wrapper.vm.textColor).toBe('white');
440
+ });
441
+
442
+ it('Error states use high-contrast red', () => {
443
+ const wrapper = mount(Checkbox, {
444
+ props: { label: 'Test', value: 'test', error: true }
445
+ });
446
+
447
+ expect(wrapper.find('.input-error').exists()).toBe(true);
448
+ });
449
+
450
+ it('Success states use high-contrast green', () => {
451
+ const wrapper = mount(Checkbox, {
452
+ props: { label: 'Test', value: 'test', success: true }
453
+ });
454
+
455
+ expect(wrapper.find('.input-success').exists()).toBe(true);
456
+ });
457
+ });
458
+
459
+ describe('Screen Reader Support', () => {
460
+ it('Checkbox announces checked state', () => {
461
+ const wrapper = mount(Checkbox, {
462
+ props: {
463
+ label: 'Test',
464
+ value: 'test',
465
+ modelValue: true
466
+ }
467
+ });
468
+
469
+ const input = wrapper.find('input');
470
+ expect(input.attributes('aria-checked')).toBe('true');
471
+ });
472
+
473
+ it('Checkbox announces unchecked state', () => {
474
+ const wrapper = mount(Checkbox, {
475
+ props: {
476
+ label: 'Test',
477
+ value: 'test',
478
+ modelValue: false
479
+ }
480
+ });
481
+
482
+ const input = wrapper.find('input');
483
+ expect(input.attributes('aria-checked')).toBe('false');
484
+ });
485
+
486
+ it('Loader announces loading state with text', () => {
487
+ const wrapper = mount(Loader);
488
+
489
+ // Should have live region
490
+ expect(wrapper.find('[aria-live]').exists()).toBe(true);
491
+ expect(wrapper.find('[aria-busy="true"]').exists()).toBe(true);
492
+
493
+ // Should have screen reader text
494
+ const srText = wrapper.find('.sr-only');
495
+ expect(srText.exists()).toBe(true);
496
+ expect(srText.text()).toBeTruthy();
497
+ });
498
+
499
+ it('Loader decorative elements are hidden from screen readers', () => {
500
+ const wrapper = mount(Loader);
501
+
502
+ const svg = wrapper.find('svg');
503
+ expect(svg.attributes('aria-hidden')).toBe('true');
504
+ });
505
+
506
+ it('Checkbox error state is announced', () => {
507
+ const wrapper = mount(Checkbox, {
508
+ props: {
509
+ label: 'Test',
510
+ value: 'test',
511
+ error: true
512
+ }
513
+ });
514
+
515
+ const input = wrapper.find('input');
516
+ expect(input.attributes('aria-invalid')).toBe('true');
517
+ });
518
+
519
+ it('Dialog is announced as modal', () => {
520
+ const wrapper = mount(Dialog);
521
+
522
+ expect(wrapper.html()).toContain('aria-modal="true"');
523
+ expect(wrapper.html()).toContain('role="dialog"');
524
+ });
525
+ });
526
+
527
+ describe('Keyboard Navigation', () => {
528
+ it('Button responds to keyboard events', async () => {
529
+ const wrapper = mount(Button);
530
+ const button = wrapper.find('button');
531
+
532
+ // Button element naturally supports Enter and Space
533
+ expect(button.element.tagName.toLowerCase()).toBe('button');
534
+ });
535
+
536
+ it('Checkbox can be toggled with Space key', async () => {
537
+ const wrapper = mount(Checkbox, {
538
+ props: {
539
+ label: 'Test',
540
+ value: 'test',
541
+ modelValue: false
542
+ }
543
+ });
544
+
545
+ const input = wrapper.find('input');
546
+
547
+ // Native checkbox responds to space
548
+ expect(input.element.type).toBe('checkbox');
549
+ });
550
+
551
+ it('DateInput can be operated via keyboard', async () => {
552
+ const wrapper = mount(DateInput, {
553
+ global: {
554
+ stubs: {
555
+ 'v-icon': { template: '<i class="v-icon"></i>', props: ['icon', 'color'] }
556
+ }
557
+ }
558
+ });
559
+
560
+ const input = wrapper.find('input');
561
+ // Input should accept keyboard input
562
+ expect(input.attributes('type')).toBe('tel');
563
+ wrapper.unmount();
564
+ });
565
+
566
+ it('DateInput calendar picker responds to Enter key', async () => {
567
+ const wrapper = mount(DateInput, {
568
+ global: {
569
+ stubs: {
570
+ 'v-icon': { template: '<i class="v-icon"></i>', props: ['icon', 'color'] }
571
+ }
572
+ }
573
+ });
574
+
575
+ const dateWrapper = wrapper.find('.date-wrapper');
576
+ // Has keydown.enter handler
577
+ expect(dateWrapper.attributes('tabindex')).toBe('0');
578
+ wrapper.unmount();
579
+ });
580
+ });
581
+
582
+ describe('WCAG 2.1 AA Criteria Verification', () => {
583
+ describe('1.3.1 Info and Relationships', () => {
584
+ it('Form controls have proper labels', () => {
585
+ const wrapper = mount(Checkbox, {
586
+ props: { label: 'Accept terms', value: 'terms' }
587
+ });
588
+
589
+ const label = wrapper.find('label');
590
+ const input = wrapper.find('input');
591
+
592
+ expect(label.attributes('for')).toBe(input.attributes('id'));
593
+ });
594
+ });
595
+
596
+ describe('1.4.3 Contrast (Minimum)', () => {
597
+ it('Text colors are defined for readability', () => {
598
+ const wrapper = mount(Button, {
599
+ props: { label: 'Test', textColor: 'white' }
600
+ });
601
+
602
+ // White on dark background should meet contrast
603
+ expect(wrapper.vm.textColor).toBe('white');
604
+ });
605
+ });
606
+
607
+ describe('2.1.1 Keyboard', () => {
608
+ it('All interactive elements are keyboard accessible', () => {
609
+ const buttonWrapper = mount(Button);
610
+ const checkboxWrapper = mount(Checkbox, {
611
+ props: { label: 'Test', value: 'test' }
612
+ });
613
+
614
+ expect(buttonWrapper.find('button').exists()).toBe(true);
615
+ expect(checkboxWrapper.find('input').exists()).toBe(true);
616
+ });
617
+ });
618
+
619
+ describe('2.4.7 Focus Visible', () => {
620
+ it('Button has focus-visible class capability', () => {
621
+ const wrapper = mount(Button);
622
+
623
+ // Button should have wl-button class that includes focus-visible styles
624
+ expect(wrapper.find('.wl-button').exists()).toBe(true);
625
+ });
626
+
627
+ it('Checkbox input can receive focus', () => {
628
+ const wrapper = mount(Checkbox, {
629
+ props: { label: 'Test', value: 'test' }
630
+ });
631
+
632
+ const input = wrapper.find('input');
633
+ expect(input.exists()).toBe(true);
634
+ });
635
+ });
636
+
637
+ describe('4.1.2 Name, Role, Value', () => {
638
+ it('Dialog has proper role', () => {
639
+ const wrapper = mount(Dialog);
640
+ expect(wrapper.html()).toContain('role="dialog"');
641
+ });
642
+
643
+ it('Checkbox has proper aria attributes', () => {
644
+ const wrapper = mount(Checkbox, {
645
+ props: { label: 'Test', value: 'test', modelValue: true }
646
+ });
647
+
648
+ const input = wrapper.find('input');
649
+ expect(input.attributes('aria-checked')).toBeDefined();
650
+ });
651
+
652
+ it('Loader has proper status role', () => {
653
+ const wrapper = mount(Loader);
654
+ expect(wrapper.find('[role="status"]').exists()).toBe(true);
655
+ });
656
+ });
657
+ });