@zap-wunschlachen/wl-shared-components 1.0.24 → 1.0.26
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/.github/workflows/playwright.yml +205 -205
- package/.github/workflows/static.yml +61 -61
- package/.github/workflows/update-snapshots.yml +37 -37
- package/.prettierrc +5 -5
- package/.storybook/main.ts +18 -18
- package/.storybook/preview.ts +37 -37
- package/.storybook/storyWrapper.vue +18 -18
- package/.storybook/withVuetifyTheme.decorator.ts +21 -21
- package/App.vue +34 -176
- package/README.md +56 -56
- package/heroicons.ts +75 -75
- package/index.html +19 -19
- package/package.json +67 -67
- package/playwright.config.ts +48 -48
- package/public/background.svg +60 -60
- package/public/style.css +187 -187
- package/public/technologies.svg +22 -22
- package/src/assets/css/base.css +235 -235
- package/src/assets/css/variables.css +107 -96
- package/src/components/Accordion/Accordion.css +59 -59
- package/src/components/Accordion/AccordionGroup.vue +51 -51
- package/src/components/Accordion/AccordionItem.vue +66 -66
- package/src/components/Appointment/Card/Actions.css +54 -54
- package/src/components/Appointment/Card/Actions.vue +99 -99
- package/src/components/Appointment/Card/AnamneseNotification.css +15 -15
- package/src/components/Appointment/Card/AnamneseNotification.vue +23 -23
- package/src/components/Appointment/Card/Card.css +80 -80
- package/src/components/Appointment/Card/Card.vue +93 -93
- package/src/components/Appointment/Card/Details.css +50 -50
- package/src/components/Appointment/Card/Details.vue +43 -43
- package/src/components/Audio/Audio.vue +187 -187
- package/src/components/Audio/Waveform.vue +117 -117
- package/src/components/Background/Background.css +39 -0
- package/src/components/Background/Background.vue +19 -0
- package/src/components/Background/WhiteCocoonBackground.vue +9 -0
- package/src/components/Background/WunschlachenBackground.vue +11 -0
- package/src/components/Button/Button.vue +119 -119
- package/src/components/CheckBox/CheckBox.css +185 -185
- package/src/components/CheckBox/Checkbox.vue +130 -130
- package/src/components/DateInput/DateInput.css +2 -2
- package/src/components/DateInput/DateInput.vue +262 -262
- package/src/components/Dialog/Dialog.css +6 -6
- package/src/components/Dialog/Dialog.vue +29 -29
- package/src/components/EditField/EditField.css +19 -19
- package/src/components/EditField/EditField.vue +202 -202
- package/src/components/ErrorPage/ErrorPage.css +124 -0
- package/src/components/ErrorPage/ErrorPage.vue +45 -0
- package/src/components/ErrorPage/ErrorPageLogo.vue +26 -0
- package/src/components/IconBullet/IconBullet.vue +86 -86
- package/src/components/IconBullet/IconBulletList.vue +41 -41
- package/src/components/Icons/AdvanceAppointments.vue +153 -153
- package/src/components/Icons/Audio/CloudFailed.vue +20 -20
- package/src/components/Icons/Audio/CloudSaved.vue +21 -21
- package/src/components/Icons/Audio/Delete.vue +15 -15
- package/src/components/Icons/Audio/Pause.vue +18 -18
- package/src/components/Icons/Audio/Play.vue +15 -15
- package/src/components/Icons/CalendarNotification.vue +126 -126
- package/src/components/Icons/Chair.vue +32 -32
- package/src/components/Icons/ChairNotification.vue +35 -35
- package/src/components/Icons/Circle.vue +66 -66
- package/src/components/Icons/FavIcon.vue +22 -22
- package/src/components/Icons/FilledCircle.vue +11 -11
- package/src/components/Icons/Group3.vue +46 -46
- package/src/components/Icons/Logo.vue +108 -0
- package/src/components/Icons/RingNotification.vue +54 -54
- package/src/components/Icons/SolidArrowRight.vue +14 -14
- package/src/components/Icons/calendar.vue +17 -17
- package/src/components/Icons/checkbox.vue +19 -19
- package/src/components/Icons/outlineChecked.vue +27 -27
- package/src/components/Icons/play.vue +5 -5
- package/src/components/Input/Input.css +187 -187
- package/src/components/Input/Input.vue +247 -247
- package/src/components/Laboratory/AppointmentCard/AppointmentCard.css +7 -7
- package/src/components/Laboratory/AppointmentCard/AppointmentCard.vue +116 -116
- package/src/components/Laboratory/ChatBoxImage/ChatBoxImage.vue +81 -81
- package/src/components/Laboratory/ChatMessage/ChatMessage.vue +113 -113
- package/src/components/Laboratory/ChatMessage/ChatMessageBadge.css +4 -4
- package/src/components/Laboratory/ChatMessage/ChatMessageBadge.vue +99 -99
- package/src/components/Laboratory/ChatNotification/ChatNotification.vue +130 -130
- package/src/components/Laboratory/DocumentCard/DocumentCard.css +3 -3
- package/src/components/Laboratory/DocumentCard/DocumentCard.vue +50 -50
- package/src/components/Laboratory/DocumentCard/DocumentCardItem.vue +53 -53
- package/src/components/Laboratory/InfoCard/InfoCard.vue +162 -162
- package/src/components/Laboratory/MainColumnsBar/MainColumnsBar.vue +102 -102
- package/src/components/Laboratory/ProgressCircle/ProgressCircle.vue +152 -152
- package/src/components/Laboratory/ProgressLinear/ProgressLinear.css +33 -33
- package/src/components/Laboratory/ProgressLinear/ProgressLinear.vue +75 -75
- package/src/components/Laboratory/SelectionColumnBar/SelectionColumnBar.vue +92 -92
- package/src/components/Laboratory/StatusNotification/StatusNotification.vue +49 -49
- package/src/components/Laboratory/TagLabel/TagLabel.vue +126 -126
- package/src/components/Laboratory/TagLabelGroup/TagLabelGroup.vue +97 -97
- package/src/components/Laboratory/TicketCard/TicketCard.css +3 -3
- package/src/components/Laboratory/TicketCard/TicketCard.vue +143 -143
- package/src/components/Laboratory/TimeLine/TimeLineEvent.css +18 -18
- package/src/components/Laboratory/TimeLine/TimeLineEvent.vue +119 -119
- package/src/components/Laboratory/TimeLine/Timeline.css +4 -4
- package/src/components/Laboratory/TimeLine/Timeline.vue +30 -30
- package/src/components/Loader/Loader.css +51 -51
- package/src/components/MaintenanceBanner/MaintenanceBanner.css +289 -0
- package/src/components/MaintenanceBanner/MaintenanceBanner.vue +127 -0
- package/src/components/MaintenanceBanner/MaintenanceIllustration.vue +54 -0
- package/src/components/Modal/Modal.css +5 -5
- package/src/components/Modal/Modal.vue +22 -22
- package/src/components/NotificationBubble/NotificationBubble.css +4 -4
- package/src/components/NotificationBubble/NotificationBubble.vue +90 -90
- package/src/components/OtpInput/OtpInput.css +39 -39
- package/src/components/OtpInput/OtpInput.vue +143 -143
- package/src/components/PhoneInput/PhoneInput.css +31 -31
- package/src/components/PhoneInput/PhoneInput.vue +113 -113
- package/src/components/Select/Select.css +150 -150
- package/src/components/Select/Select.vue +315 -304
- package/src/components/TextArea/TextArea.css +3 -3
- package/src/components/TextArea/TextArea.vue +126 -126
- package/src/components/TickBox/TickBox.css +49 -49
- package/src/components/TickBox/TickBox.vue +126 -126
- package/src/components/index.ts +26 -24
- package/src/constants/iconEnums.ts +3 -3
- package/src/i18n/i18n.ts +15 -15
- package/src/i18n/locales/de.json +30 -30
- package/src/i18n/locales/en.json +30 -30
- package/src/index.ts +34 -34
- package/src/main.ts +11 -11
- package/src/plugins/vuetify.ts +139 -131
- package/src/shims-vue.d.ts +10 -10
- package/src/stories/Accordion.stories.ts +650 -650
- package/src/stories/Audio.stories.ts +28 -28
- package/src/stories/Button.stories.ts +263 -263
- package/src/stories/CheckBox.stories.ts +348 -348
- package/src/stories/DateInput.stories.ts +53 -53
- package/src/stories/Dialog.stories.ts +147 -147
- package/src/stories/EditField.stories.ts +78 -78
- package/src/stories/IconBullet/IconBullet.stories.ts +201 -201
- package/src/stories/IconBullet/IconBulletList.stories.ts +275 -275
- package/src/stories/Input.stories.ts +351 -351
- package/src/stories/Laboratory/Cards/AppointmentCard/AppointmentCard.stories.ts +260 -260
- package/src/stories/Laboratory/Cards/DocumentCard/DocumentCard.stories.ts +176 -176
- package/src/stories/Laboratory/Cards/DocumentCard/DocumentCardItem.stories.ts +119 -119
- package/src/stories/Laboratory/Cards/InfoCard/InfoCard.stories.ts +320 -320
- package/src/stories/Laboratory/Cards/TicketCard/TicketCard.stories.ts +335 -335
- package/src/stories/Laboratory/Chat/ChatBoxImage.stories.ts +82 -82
- package/src/stories/Laboratory/Chat/ChatMessage.stories.ts +198 -198
- package/src/stories/Laboratory/Chat/ChatMessageBadge.stories.ts +204 -204
- package/src/stories/Laboratory/Chat/ChatNotification.stories.ts +144 -144
- package/src/stories/Laboratory/Chat/ProgressLinear.stories.ts +186 -186
- package/src/stories/Laboratory/Chat/StatusNotification.stories.ts +111 -111
- package/src/stories/Laboratory/MainColumnsBar.stories.ts +48 -48
- package/src/stories/Laboratory/ProgressCircle.stories.ts +261 -261
- package/src/stories/Laboratory/SelectionColumnBar.stories.ts +234 -234
- package/src/stories/Laboratory/TagLabel.stories.ts +418 -418
- package/src/stories/Laboratory/TagLabelGroup.stories.ts +234 -234
- package/src/stories/Laboratory/Timeline.stories.ts +403 -403
- package/src/stories/NotificationBubble.stories.ts +194 -194
- package/src/stories/OtpInput.stories.ts +100 -100
- package/src/stories/PhoneInput.stories.ts +52 -52
- package/src/stories/Select.stories.ts +419 -419
- package/src/stories/TextArea.stories.ts +112 -112
- package/src/stories/TickBox.stories.ts +294 -294
- package/src/stories/v-icon.stories.ts +91 -91
- package/src/utils/index.ts +106 -100
- package/src/vite-env.d.ts +1 -1
- package/tests/e2e/README.md +220 -220
- package/tests/e2e/accessibility.spec.ts +638 -638
- package/tests/e2e/accordion.spec.ts +42 -42
- package/tests/e2e/additional-components.spec.ts +437 -437
- package/tests/e2e/all-components.spec.ts +135 -135
- package/tests/e2e/appointment-card.spec.ts +816 -816
- package/tests/e2e/button-fixed.spec.ts +58 -58
- package/tests/e2e/button.spec.ts +76 -76
- package/tests/e2e/checkbox.spec.ts +50 -50
- package/tests/e2e/date-input.spec.ts +46 -46
- package/tests/e2e/debug.spec.ts +51 -51
- package/tests/e2e/dialog.spec.ts +58 -58
- package/tests/e2e/input.spec.ts +55 -55
- package/tests/e2e/laboratory-components.spec.ts +320 -320
- package/tests/e2e/otp-input.spec.ts +50 -50
- package/tests/e2e/select.spec.ts +52 -52
- package/tests/e2e/storybook-utils.ts +59 -59
- package/tests/e2e/test-basic.spec.ts +33 -33
- package/tests/e2e/visual-regression.spec.ts +350 -350
- package/tests/unit/components/Accordion/AccordionGroup.spec.ts.skip +342 -342
- package/tests/unit/components/Accordion/AccordionItem.spec.ts.skip +383 -383
- package/tests/unit/components/Appointment/Card/Actions.spec.ts +407 -407
- package/tests/unit/components/Appointment/Card/Card.spec.ts +485 -485
- package/tests/unit/components/Appointment/Card/Details.spec.ts +397 -397
- package/tests/unit/components/Audio/Audio.spec.ts +403 -403
- package/tests/unit/components/Audio/Waveform.spec.ts +483 -483
- package/tests/unit/components/Core/Button.spec.ts +336 -336
- package/tests/unit/components/Core/Checkbox.spec.ts +544 -544
- package/tests/unit/components/Core/DateInput.spec.ts +690 -690
- package/tests/unit/components/Core/Dialog.spec.ts +485 -485
- package/tests/unit/components/Core/EditField.spec.ts +782 -782
- package/tests/unit/components/Core/Input.spec.ts +512 -512
- package/tests/unit/components/Core/Modal.spec.ts +518 -518
- package/tests/unit/components/Core/NotificationBubble.spec.ts +606 -606
- package/tests/unit/components/Core/OtpInput.spec.ts +708 -708
- package/tests/unit/components/Core/PhoneInput.spec.ts +619 -619
- package/tests/unit/components/Core/Select.spec.ts +712 -712
- package/tests/unit/components/Core/TextArea.spec.ts +565 -565
- package/tests/unit/components/Core/TickBox.spec.ts +779 -779
- package/tests/unit/components/IconBullet/IconBullet.spec.ts +356 -356
- package/tests/unit/components/IconBullet/IconBulletList.spec.ts +371 -371
- package/tests/unit/components/Icons/Audio/CloudFailed.spec.ts +108 -108
- package/tests/unit/components/Icons/Audio/CloudSaved.spec.ts +149 -149
- package/tests/unit/components/Icons/Audio/Delete.spec.ts +158 -158
- package/tests/unit/components/Icons/Audio/Pause.spec.ts +208 -208
- package/tests/unit/components/Icons/Audio/Play.spec.ts +217 -217
- package/tests/unit/components/Icons/CalendarNotification.spec.ts +186 -186
- package/tests/unit/components/Icons/Chair.spec.ts +234 -234
- package/tests/unit/components/Icons/ChairNotification.spec.ts +311 -311
- package/tests/unit/components/Icons/Circle.spec.ts +255 -255
- package/tests/unit/components/Icons/FavIcon.spec.ts +251 -251
- package/tests/unit/components/Icons/FilledCircle.spec.ts +274 -274
- package/tests/unit/components/Icons/Group3.spec.ts +355 -355
- package/tests/unit/components/Icons/RingNotification.spec.ts +393 -393
- package/tests/unit/components/Icons/calendar.spec.ts +286 -286
- package/tests/unit/components/Icons/checkbox.spec.ts +315 -315
- package/tests/unit/components/Icons/outlineChecked.spec.ts +434 -434
- package/tests/unit/components/Icons/play.spec.ts +308 -308
- package/tests/unit/components/Laboratory/AppointmentCard.spec.ts +167 -167
- package/tests/unit/components/Laboratory/ChatBoxImage.spec.ts +179 -179
- package/tests/unit/components/Laboratory/ChatMessage.spec.ts +263 -263
- package/tests/unit/components/Laboratory/ChatMessageBadge.spec.ts +282 -282
- package/tests/unit/components/Laboratory/ChatNotification.spec.ts +256 -256
- package/tests/unit/components/Laboratory/DocumentCard.spec.ts +228 -228
- package/tests/unit/components/Laboratory/DocumentCardItem.spec.ts +236 -236
- package/tests/unit/components/Laboratory/InfoCard.spec.ts +308 -308
- package/tests/unit/components/Laboratory/MainColumnsBar.spec.ts +251 -251
- package/tests/unit/components/Laboratory/ProgressCircle.spec.ts +290 -290
- package/tests/unit/components/Laboratory/ProgressLinear.spec.ts +275 -275
- package/tests/unit/components/Laboratory/SelectionColumnBar.spec.ts +288 -288
- package/tests/unit/components/Laboratory/StatusNotification.spec.ts +296 -296
- package/tests/unit/components/Laboratory/TagLabel.spec.ts +353 -353
- package/tests/unit/components/Laboratory/TagLabelGroup.spec.ts +377 -377
- package/tests/unit/components/Laboratory/TicketCard.spec.ts +351 -351
- package/tests/unit/components/Laboratory/TimeLineEvent.spec.ts +381 -381
- package/tests/unit/components/Laboratory/Timeline.spec.ts +419 -419
- package/tests/unit/constants/iconEnums.spec.ts +39 -39
- package/tests/unit/i18n/i18n.spec.ts +88 -88
- package/tests/unit/plugins/vuetify.spec.ts +220 -220
- package/tests/unit/setup.ts +189 -189
- package/tests/unit/src/components/index.spec.ts.skip +192 -192
- package/tests/unit/src/index.spec.ts.skip +182 -182
- package/tests/unit/src/main.spec.ts +151 -151
- package/tsconfig.json +26 -26
- package/vite.config.ts +29 -29
- package/vitest.config.ts +83 -83
|
@@ -1,639 +1,639 @@
|
|
|
1
|
-
import { test, expect, Frame } from '@playwright/test';
|
|
2
|
-
import { navigateToStory } from './storybook-utils';
|
|
3
|
-
import AxeBuilder from '@axe-core/playwright';
|
|
4
|
-
|
|
5
|
-
test.describe('Accessibility Tests', () => {
|
|
6
|
-
let frame: Frame;
|
|
7
|
-
|
|
8
|
-
test.describe('Core Components Accessibility', () => {
|
|
9
|
-
test('Button - keyboard navigation and ARIA', async ({ page }) => {
|
|
10
|
-
frame = await navigateToStory(page, '/?path=/story/wl-button--default');
|
|
11
|
-
|
|
12
|
-
const button = frame.locator('button.v-btn').first();
|
|
13
|
-
|
|
14
|
-
// Keyboard navigation
|
|
15
|
-
await button.focus();
|
|
16
|
-
await expect(button).toBeFocused();
|
|
17
|
-
|
|
18
|
-
// Space key activation
|
|
19
|
-
await page.keyboard.press('Space');
|
|
20
|
-
|
|
21
|
-
// Enter key activation
|
|
22
|
-
await page.keyboard.press('Enter');
|
|
23
|
-
|
|
24
|
-
// Check ARIA attributes
|
|
25
|
-
const ariaLabel = await button.getAttribute('aria-label');
|
|
26
|
-
const ariaPressed = await button.getAttribute('aria-pressed');
|
|
27
|
-
const role = await button.getAttribute('role');
|
|
28
|
-
|
|
29
|
-
// Button should be keyboard accessible
|
|
30
|
-
const tabIndex = await button.getAttribute('tabindex');
|
|
31
|
-
expect(tabIndex === null || parseInt(tabIndex) >= 0).toBeTruthy();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('Input - label association and ARIA', async ({ page }) => {
|
|
35
|
-
frame = await navigateToStory(page, '/?path=/story/wl-input--default');
|
|
36
|
-
|
|
37
|
-
const input = frame.locator('input').first();
|
|
38
|
-
|
|
39
|
-
// Vuetify uses aria-label or placeholder instead of traditional labels
|
|
40
|
-
// Check for accessible labeling
|
|
41
|
-
const ariaLabel = await input.getAttribute('aria-label');
|
|
42
|
-
const placeholder = await input.getAttribute('placeholder');
|
|
43
|
-
const ariaLabelledBy = await input.getAttribute('aria-labelledby');
|
|
44
|
-
|
|
45
|
-
// At least one form of labeling should exist
|
|
46
|
-
expect(ariaLabel || placeholder || ariaLabelledBy).toBeTruthy();
|
|
47
|
-
|
|
48
|
-
// Check if there's a label element (Vuetify might generate one)
|
|
49
|
-
const labels = frame.locator('label');
|
|
50
|
-
if (await labels.count() > 0) {
|
|
51
|
-
const label = labels.first();
|
|
52
|
-
const inputId = await input.getAttribute('id');
|
|
53
|
-
const labelFor = await label.getAttribute('for');
|
|
54
|
-
|
|
55
|
-
if (inputId && labelFor) {
|
|
56
|
-
expect(inputId).toBe(labelFor);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Check ARIA attributes
|
|
61
|
-
const ariaRequired = await input.getAttribute('aria-required');
|
|
62
|
-
const ariaInvalid = await input.getAttribute('aria-invalid');
|
|
63
|
-
const ariaDescribedBy = await input.getAttribute('aria-describedby');
|
|
64
|
-
|
|
65
|
-
// Keyboard navigation
|
|
66
|
-
await input.focus();
|
|
67
|
-
await expect(input).toBeFocused();
|
|
68
|
-
|
|
69
|
-
// Tab navigation
|
|
70
|
-
await page.keyboard.press('Tab');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('Select - keyboard navigation and screen reader support', async ({ page }) => {
|
|
74
|
-
frame = await navigateToStory(page, '/?path=/story/wl-select--default');
|
|
75
|
-
|
|
76
|
-
const select = frame.locator('.wl-select').first();
|
|
77
|
-
|
|
78
|
-
// Vuetify v-combobox uses input element with combobox role
|
|
79
|
-
const comboboxInput = select.locator('input').first();
|
|
80
|
-
const comboboxContainer = select.locator('[role="combobox"]').first();
|
|
81
|
-
|
|
82
|
-
if (await comboboxContainer.count() > 0) {
|
|
83
|
-
// Check ARIA attributes on the container
|
|
84
|
-
const ariaExpanded = await comboboxContainer.getAttribute('aria-expanded');
|
|
85
|
-
const ariaHasPopup = await comboboxContainer.getAttribute('aria-haspopup');
|
|
86
|
-
const ariaControls = await comboboxContainer.getAttribute('aria-controls');
|
|
87
|
-
|
|
88
|
-
expect(ariaExpanded).toBeDefined();
|
|
89
|
-
expect(ariaHasPopup).toBeDefined();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Focus the input element within the select
|
|
93
|
-
await comboboxInput.click();
|
|
94
|
-
await comboboxInput.focus();
|
|
95
|
-
await expect(comboboxInput).toBeFocused();
|
|
96
|
-
|
|
97
|
-
// Arrow key navigation to open dropdown
|
|
98
|
-
await page.keyboard.press('ArrowDown');
|
|
99
|
-
await page.waitForTimeout(500);
|
|
100
|
-
|
|
101
|
-
// Check if dropdown opened (Vuetify renders menus in the frame)
|
|
102
|
-
const dropdown = frame.locator('[role="listbox"], .v-menu, .v-list').first();
|
|
103
|
-
if (await dropdown.count() > 0 && await dropdown.isVisible()) {
|
|
104
|
-
// Navigate options with arrow keys
|
|
105
|
-
await page.keyboard.press('ArrowDown');
|
|
106
|
-
await page.keyboard.press('ArrowUp');
|
|
107
|
-
|
|
108
|
-
// Select with Enter
|
|
109
|
-
await page.keyboard.press('Enter');
|
|
110
|
-
await page.waitForTimeout(300);
|
|
111
|
-
} else {
|
|
112
|
-
// If dropdown didn't open with ArrowDown, try clicking the dropdown icon
|
|
113
|
-
const dropdownIcon = select.locator('.v-input__icon, [aria-label*="menu"], .mdi-menu-down').first();
|
|
114
|
-
if (await dropdownIcon.count() > 0) {
|
|
115
|
-
await dropdownIcon.click();
|
|
116
|
-
await page.waitForTimeout(500);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Escape to close
|
|
121
|
-
await page.keyboard.press('Escape');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('Checkbox - keyboard control and ARIA states', async ({ page }) => {
|
|
125
|
-
frame = await navigateToStory(page, '/?path=/story/wl-checkbox--default');
|
|
126
|
-
|
|
127
|
-
const checkbox = frame.locator('input[type="checkbox"]').first();
|
|
128
|
-
|
|
129
|
-
// Focus and keyboard control
|
|
130
|
-
await checkbox.focus();
|
|
131
|
-
await expect(checkbox).toBeFocused();
|
|
132
|
-
|
|
133
|
-
// Space to toggle
|
|
134
|
-
const initialChecked = await checkbox.isChecked();
|
|
135
|
-
await page.keyboard.press('Space');
|
|
136
|
-
await expect(checkbox).toBeChecked({ checked: !initialChecked });
|
|
137
|
-
|
|
138
|
-
// Check ARIA attributes
|
|
139
|
-
const ariaChecked = await checkbox.getAttribute('aria-checked');
|
|
140
|
-
const role = await checkbox.getAttribute('role');
|
|
141
|
-
|
|
142
|
-
// Label association
|
|
143
|
-
const label = frame.locator('label').first();
|
|
144
|
-
const labelText = await label.textContent();
|
|
145
|
-
expect(labelText).toBeTruthy();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test('DateInput - date picker accessibility', async ({ page }) => {
|
|
149
|
-
frame = await navigateToStory(page, '/?path=/story/wl-dateinput--default');
|
|
150
|
-
|
|
151
|
-
const dateInput = frame.locator('input').first();
|
|
152
|
-
|
|
153
|
-
// Focus management
|
|
154
|
-
await dateInput.focus();
|
|
155
|
-
await expect(dateInput).toBeFocused();
|
|
156
|
-
|
|
157
|
-
// Check for accessible date format hint
|
|
158
|
-
const ariaDescribedBy = await dateInput.getAttribute('aria-describedby');
|
|
159
|
-
const placeholder = await dateInput.getAttribute('placeholder');
|
|
160
|
-
|
|
161
|
-
// Keyboard navigation for date picker
|
|
162
|
-
await dateInput.click();
|
|
163
|
-
await page.waitForTimeout(500);
|
|
164
|
-
|
|
165
|
-
// Check for calendar ARIA attributes if picker opens
|
|
166
|
-
const calendar = frame.locator('[role="grid"], [role="application"]').first();
|
|
167
|
-
if (await calendar.count() > 0) {
|
|
168
|
-
// Navigate with arrow keys
|
|
169
|
-
await page.keyboard.press('ArrowRight');
|
|
170
|
-
await page.keyboard.press('ArrowDown');
|
|
171
|
-
await page.keyboard.press('Enter');
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test('Dialog - focus trap and ARIA', async ({ page }) => {
|
|
176
|
-
frame = await navigateToStory(page, '/?path=/story/wl-dialog--default-dialog');
|
|
177
|
-
|
|
178
|
-
// Open dialog
|
|
179
|
-
const trigger = frame.locator('button').filter({ hasText: 'Neuen Entwurf erstellen' }).first();
|
|
180
|
-
await trigger.click();
|
|
181
|
-
|
|
182
|
-
await page.waitForTimeout(500);
|
|
183
|
-
|
|
184
|
-
const dialog = frame.locator('[role="dialog"], .v-dialog').first();
|
|
185
|
-
if (await dialog.count() > 0) {
|
|
186
|
-
await expect(dialog).toBeVisible();
|
|
187
|
-
|
|
188
|
-
// Check ARIA attributes
|
|
189
|
-
const ariaModal = await dialog.getAttribute('aria-modal');
|
|
190
|
-
const ariaLabelledBy = await dialog.getAttribute('aria-labelledby');
|
|
191
|
-
const role = await dialog.getAttribute('role');
|
|
192
|
-
|
|
193
|
-
// Focus should be trapped within dialog
|
|
194
|
-
await page.keyboard.press('Tab');
|
|
195
|
-
const focusedElement = await page.evaluate(() => document.activeElement?.tagName);
|
|
196
|
-
|
|
197
|
-
// Escape to close
|
|
198
|
-
await page.keyboard.press('Escape');
|
|
199
|
-
await expect(dialog).not.toBeVisible();
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test('TextArea - multiline text accessibility', async ({ page }) => {
|
|
204
|
-
frame = await navigateToStory(page, '/?path=/story/wl-textarea--default');
|
|
205
|
-
|
|
206
|
-
const textarea = frame.locator('textarea').first();
|
|
207
|
-
|
|
208
|
-
// Focus and keyboard input
|
|
209
|
-
await textarea.focus();
|
|
210
|
-
await expect(textarea).toBeFocused();
|
|
211
|
-
|
|
212
|
-
// Multiline input with keyboard
|
|
213
|
-
await textarea.type('Line 1');
|
|
214
|
-
await page.keyboard.press('Enter');
|
|
215
|
-
await textarea.type('Line 2');
|
|
216
|
-
|
|
217
|
-
// Check ARIA attributes
|
|
218
|
-
const ariaMultiline = await textarea.getAttribute('aria-multiline');
|
|
219
|
-
const rows = await textarea.getAttribute('rows');
|
|
220
|
-
|
|
221
|
-
// Character count accessibility if present
|
|
222
|
-
const counter = frame.locator('[role="status"], .v-counter').first();
|
|
223
|
-
if (await counter.count() > 0) {
|
|
224
|
-
const ariaLive = await counter.getAttribute('aria-live');
|
|
225
|
-
expect(ariaLive).toBeTruthy();
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
test('OtpInput - multiple input fields navigation', async ({ page }) => {
|
|
230
|
-
frame = await navigateToStory(page, '/?path=/story/wl-otpinput--default');
|
|
231
|
-
|
|
232
|
-
const inputs = frame.locator('input');
|
|
233
|
-
const inputCount = await inputs.count();
|
|
234
|
-
|
|
235
|
-
if (inputCount > 0) {
|
|
236
|
-
// Focus first input
|
|
237
|
-
await inputs.first().focus();
|
|
238
|
-
await expect(inputs.first()).toBeFocused();
|
|
239
|
-
|
|
240
|
-
// Type and auto-advance
|
|
241
|
-
await inputs.first().type('1');
|
|
242
|
-
await page.waitForTimeout(100);
|
|
243
|
-
|
|
244
|
-
// Should auto-focus next input
|
|
245
|
-
if (inputCount > 1) {
|
|
246
|
-
await expect(inputs.nth(1)).toBeFocused();
|
|
247
|
-
|
|
248
|
-
// Backspace to go back
|
|
249
|
-
await page.keyboard.press('Backspace');
|
|
250
|
-
await page.waitForTimeout(100);
|
|
251
|
-
await expect(inputs.first()).toBeFocused();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Arrow key navigation
|
|
255
|
-
await page.keyboard.press('ArrowRight');
|
|
256
|
-
if (inputCount > 1) {
|
|
257
|
-
await expect(inputs.nth(1)).toBeFocused();
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
await page.keyboard.press('ArrowLeft');
|
|
261
|
-
await expect(inputs.first()).toBeFocused();
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
test.describe('Laboratory Components Accessibility', () => {
|
|
267
|
-
test('AppointmentCard - semantic structure and ARIA', async ({ page }) => {
|
|
268
|
-
frame = await navigateToStory(page, '/?path=/story/wl-laboratory-cards-appointmentcard--draft-status');
|
|
269
|
-
|
|
270
|
-
const card = frame.locator('.appointment-card').first();
|
|
271
|
-
|
|
272
|
-
// Check semantic HTML
|
|
273
|
-
const headings = card.locator('h1, h2, h3, h4, h5, h6');
|
|
274
|
-
const headingCount = await headings.count();
|
|
275
|
-
expect(headingCount).toBeGreaterThan(0);
|
|
276
|
-
|
|
277
|
-
// Interactive elements should be keyboard accessible
|
|
278
|
-
const buttons = card.locator('button');
|
|
279
|
-
if (await buttons.count() > 0) {
|
|
280
|
-
await buttons.first().focus();
|
|
281
|
-
await expect(buttons.first()).toBeFocused();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Check for proper heading hierarchy
|
|
285
|
-
const h3 = card.locator('h3').first();
|
|
286
|
-
if (await h3.count() > 0) {
|
|
287
|
-
const text = await h3.textContent();
|
|
288
|
-
expect(text).toBeTruthy();
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
test('ChatMessage - message accessibility', async ({ page }) => {
|
|
293
|
-
frame = await navigateToStory(page, '/?path=/story/wl-laboratory-chat-chatmessage--default');
|
|
294
|
-
|
|
295
|
-
const message = frame.locator('.v-alert, [role="alert"]').first();
|
|
296
|
-
|
|
297
|
-
if (await message.count() > 0) {
|
|
298
|
-
// Check ARIA role
|
|
299
|
-
const role = await message.getAttribute('role');
|
|
300
|
-
|
|
301
|
-
// Check for accessible timestamp
|
|
302
|
-
const time = message.locator('time, [datetime]').first();
|
|
303
|
-
if (await time.count() > 0) {
|
|
304
|
-
const datetime = await time.getAttribute('datetime');
|
|
305
|
-
expect(datetime).toBeTruthy();
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
test('ProgressCircle - progress indication accessibility', async ({ page }) => {
|
|
311
|
-
frame = await navigateToStory(page, '/?path=/story/wl-laboratory-progresscircle--default');
|
|
312
|
-
|
|
313
|
-
const progress = frame.locator('[role="progressbar"], .v-progress-circular').first();
|
|
314
|
-
|
|
315
|
-
if (await progress.count() > 0) {
|
|
316
|
-
// Check ARIA attributes for progress
|
|
317
|
-
const ariaValueNow = await progress.getAttribute('aria-valuenow');
|
|
318
|
-
const ariaValueMin = await progress.getAttribute('aria-valuemin');
|
|
319
|
-
const ariaValueMax = await progress.getAttribute('aria-valuemax');
|
|
320
|
-
|
|
321
|
-
// Should have accessible label
|
|
322
|
-
const ariaLabel = await progress.getAttribute('aria-label');
|
|
323
|
-
if (!ariaLabel) {
|
|
324
|
-
const ariaLabelledBy = await progress.getAttribute('aria-labelledby');
|
|
325
|
-
expect(ariaLabel || ariaLabelledBy).toBeTruthy();
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
test('Timeline - list semantics and navigation', async ({ page }) => {
|
|
331
|
-
frame = await navigateToStory(page, '/?path=/story/wl-laboratory-timeline--default');
|
|
332
|
-
|
|
333
|
-
const timeline = frame.locator('.v-timeline').first();
|
|
334
|
-
|
|
335
|
-
if (await timeline.count() > 0) {
|
|
336
|
-
// Check for list semantics
|
|
337
|
-
const listItems = timeline.locator('[role="listitem"], li, .v-timeline-item');
|
|
338
|
-
const itemCount = await listItems.count();
|
|
339
|
-
expect(itemCount).toBeGreaterThan(0);
|
|
340
|
-
|
|
341
|
-
// Each item should have meaningful content
|
|
342
|
-
if (itemCount > 0) {
|
|
343
|
-
const firstItem = listItems.first();
|
|
344
|
-
const content = await firstItem.textContent();
|
|
345
|
-
expect(content?.trim()).toBeTruthy();
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
test.describe('Color Contrast and Visual Accessibility', () => {
|
|
352
|
-
test('Button color contrast', async ({ page }) => {
|
|
353
|
-
frame = await navigateToStory(page, '/?path=/story/wl-button--default');
|
|
354
|
-
|
|
355
|
-
const button = frame.locator('button.v-btn').first();
|
|
356
|
-
|
|
357
|
-
// Get computed styles
|
|
358
|
-
const styles = await button.evaluate((el) => {
|
|
359
|
-
const computed = window.getComputedStyle(el);
|
|
360
|
-
return {
|
|
361
|
-
color: computed.color,
|
|
362
|
-
backgroundColor: computed.backgroundColor,
|
|
363
|
-
fontSize: computed.fontSize
|
|
364
|
-
};
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
// Ensure text is readable size
|
|
368
|
-
const fontSize = parseInt(styles.fontSize);
|
|
369
|
-
expect(fontSize).toBeGreaterThanOrEqual(12);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
test('Focus indicators visibility', async ({ page }) => {
|
|
373
|
-
frame = await navigateToStory(page, '/?path=/story/wl-input--default');
|
|
374
|
-
|
|
375
|
-
const input = frame.locator('input').first();
|
|
376
|
-
|
|
377
|
-
// Get initial outline
|
|
378
|
-
const initialOutline = await input.evaluate((el) => {
|
|
379
|
-
const computed = window.getComputedStyle(el);
|
|
380
|
-
return computed.outline;
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
// Focus and check for visible focus indicator
|
|
384
|
-
await input.focus();
|
|
385
|
-
|
|
386
|
-
const focusedOutline = await input.evaluate((el) => {
|
|
387
|
-
const computed = window.getComputedStyle(el);
|
|
388
|
-
return {
|
|
389
|
-
outline: computed.outline,
|
|
390
|
-
outlineWidth: computed.outlineWidth,
|
|
391
|
-
boxShadow: computed.boxShadow,
|
|
392
|
-
border: computed.border
|
|
393
|
-
};
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
// Should have some visual focus indicator
|
|
397
|
-
const hasFocusIndicator =
|
|
398
|
-
focusedOutline.outline !== 'none' ||
|
|
399
|
-
focusedOutline.boxShadow !== 'none' ||
|
|
400
|
-
focusedOutline.border !== initialOutline;
|
|
401
|
-
|
|
402
|
-
expect(hasFocusIndicator).toBeTruthy();
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
test('Error state contrast and messaging', async ({ page }) => {
|
|
406
|
-
frame = await navigateToStory(page, '/?path=/story/wl-input--error');
|
|
407
|
-
|
|
408
|
-
const errorMessage = frame.locator('[role="alert"], .v-messages__message').first();
|
|
409
|
-
|
|
410
|
-
if (await errorMessage.count() > 0) {
|
|
411
|
-
await expect(errorMessage).toBeVisible();
|
|
412
|
-
|
|
413
|
-
// Check error color contrast
|
|
414
|
-
const errorColor = await errorMessage.evaluate((el) => {
|
|
415
|
-
const computed = window.getComputedStyle(el);
|
|
416
|
-
return computed.color;
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
// Error messages should use high contrast colors
|
|
420
|
-
expect(errorColor).toBeTruthy();
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
test.describe('Screen Reader Support', () => {
|
|
426
|
-
test('Form field descriptions and instructions', async ({ page }) => {
|
|
427
|
-
frame = await navigateToStory(page, '/?path=/story/wl-input--default');
|
|
428
|
-
|
|
429
|
-
const input = frame.locator('input').first();
|
|
430
|
-
const helperText = frame.locator('.v-messages, [id*="hint"], [id*="helper"]').first();
|
|
431
|
-
|
|
432
|
-
if (await helperText.count() > 0) {
|
|
433
|
-
const helperId = await helperText.getAttribute('id');
|
|
434
|
-
const ariaDescribedBy = await input.getAttribute('aria-describedby');
|
|
435
|
-
|
|
436
|
-
if (helperId && ariaDescribedBy) {
|
|
437
|
-
expect(ariaDescribedBy).toContain(helperId);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
test('Live regions for dynamic content', async ({ page }) => {
|
|
443
|
-
frame = await navigateToStory(page, '/?path=/story/wl-notificationbubble--with-text');
|
|
444
|
-
|
|
445
|
-
const notification = frame.locator('.notification-bubble').first();
|
|
446
|
-
|
|
447
|
-
// Check for live region attributes
|
|
448
|
-
const ariaLive = await notification.getAttribute('aria-live');
|
|
449
|
-
const role = await notification.getAttribute('role');
|
|
450
|
-
|
|
451
|
-
// Notifications should announce to screen readers
|
|
452
|
-
if (role === 'alert' || role === 'status') {
|
|
453
|
-
expect(role).toBeTruthy();
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
test('Icon-only buttons have accessible labels', async ({ page }) => {
|
|
458
|
-
frame = await navigateToStory(page, '/?path=/story/wl-button--with-prepend-icon');
|
|
459
|
-
|
|
460
|
-
const iconButtons = frame.locator('button.v-btn').filter({ hasText: '' });
|
|
461
|
-
|
|
462
|
-
for (let i = 0; i < await iconButtons.count(); i++) {
|
|
463
|
-
const button = iconButtons.nth(i);
|
|
464
|
-
const ariaLabel = await button.getAttribute('aria-label');
|
|
465
|
-
const title = await button.getAttribute('title');
|
|
466
|
-
const innerText = await button.innerText();
|
|
467
|
-
|
|
468
|
-
// Icon-only buttons must have accessible text
|
|
469
|
-
if (!innerText.trim()) {
|
|
470
|
-
expect(ariaLabel || title).toBeTruthy();
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
test.describe('Keyboard Navigation Patterns', () => {
|
|
477
|
-
test('Tab order follows visual layout', async ({ page }) => {
|
|
478
|
-
frame = await navigateToStory(page, '/?path=/story/wl-input--default');
|
|
479
|
-
|
|
480
|
-
const focusableElements = frame.locator('button:visible, input:visible, select:visible, textarea:visible, a[href]:visible, [tabindex]:not([tabindex="-1"])');
|
|
481
|
-
const elementCount = await focusableElements.count();
|
|
482
|
-
|
|
483
|
-
if (elementCount > 1) {
|
|
484
|
-
// Tab through elements
|
|
485
|
-
for (let i = 0; i < elementCount; i++) {
|
|
486
|
-
await page.keyboard.press('Tab');
|
|
487
|
-
await page.waitForTimeout(100);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Shift+Tab to go backwards
|
|
491
|
-
for (let i = 0; i < elementCount; i++) {
|
|
492
|
-
await page.keyboard.press('Shift+Tab');
|
|
493
|
-
await page.waitForTimeout(100);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
test('Escape key closes overlays', async ({ page }) => {
|
|
499
|
-
frame = await navigateToStory(page, '/?path=/story/wl-select--default');
|
|
500
|
-
|
|
501
|
-
const select = frame.locator('.wl-select input').first();
|
|
502
|
-
|
|
503
|
-
// Open dropdown
|
|
504
|
-
await select.click();
|
|
505
|
-
await page.waitForTimeout(500);
|
|
506
|
-
|
|
507
|
-
const dropdown = frame.locator('.v-menu, [role="listbox"]').first();
|
|
508
|
-
if (await dropdown.count() > 0) {
|
|
509
|
-
await expect(dropdown).toBeVisible();
|
|
510
|
-
|
|
511
|
-
// Escape to close
|
|
512
|
-
await page.keyboard.press('Escape');
|
|
513
|
-
await page.waitForTimeout(500);
|
|
514
|
-
await expect(dropdown).not.toBeVisible();
|
|
515
|
-
}
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
test('Arrow keys navigate within components', async ({ page }) => {
|
|
519
|
-
frame = await navigateToStory(page, '/?path=/story/wl-accordion--outlined-inset');
|
|
520
|
-
|
|
521
|
-
const accordion = frame.locator('.v-expansion-panels').first();
|
|
522
|
-
const panels = accordion.locator('.v-expansion-panel');
|
|
523
|
-
|
|
524
|
-
if (await panels.count() > 1) {
|
|
525
|
-
await panels.first().focus();
|
|
526
|
-
|
|
527
|
-
// Arrow down to next panel
|
|
528
|
-
await page.keyboard.press('ArrowDown');
|
|
529
|
-
await page.waitForTimeout(100);
|
|
530
|
-
|
|
531
|
-
// Arrow up to previous panel
|
|
532
|
-
await page.keyboard.press('ArrowUp');
|
|
533
|
-
await page.waitForTimeout(100);
|
|
534
|
-
|
|
535
|
-
// Space or Enter to expand
|
|
536
|
-
await page.keyboard.press('Space');
|
|
537
|
-
await page.waitForTimeout(500);
|
|
538
|
-
}
|
|
539
|
-
});
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
test.describe('Automated Accessibility Testing with Axe', () => {
|
|
543
|
-
const componentsToTest = [
|
|
544
|
-
{ name: 'Button', path: '/?path=/story/wl-button--default' },
|
|
545
|
-
{ name: 'Input', path: '/?path=/story/wl-input--default' },
|
|
546
|
-
{ name: 'Select', path: '/?path=/story/wl-select--default' },
|
|
547
|
-
{ name: 'Checkbox', path: '/?path=/story/wl-checkbox--default' },
|
|
548
|
-
{ name: 'DateInput', path: '/?path=/story/wl-dateinput--default' },
|
|
549
|
-
{ name: 'Dialog', path: '/?path=/story/wl-dialog--default-dialog' },
|
|
550
|
-
{ name: 'Accordion', path: '/?path=/story/wl-accordion--outlined-inset' },
|
|
551
|
-
{ name: 'OtpInput', path: '/?path=/story/wl-otpinput--default' },
|
|
552
|
-
{ name: 'TextArea', path: '/?path=/story/wl-textarea--default' },
|
|
553
|
-
{ name: 'NotificationBubble', path: '/?path=/story/wl-notificationbubble--with-text' }
|
|
554
|
-
];
|
|
555
|
-
|
|
556
|
-
componentsToTest.forEach(({ name, path }) => {
|
|
557
|
-
test(`${name} - automated accessibility scan`, async ({ page }) => {
|
|
558
|
-
await page.goto(path);
|
|
559
|
-
await page.waitForTimeout(1000);
|
|
560
|
-
|
|
561
|
-
try {
|
|
562
|
-
const accessibilityScanResults = await new AxeBuilder({ page })
|
|
563
|
-
.include('[data-testid="root"]')
|
|
564
|
-
.analyze();
|
|
565
|
-
|
|
566
|
-
// Check for violations
|
|
567
|
-
expect(accessibilityScanResults.violations).toEqual([]);
|
|
568
|
-
} catch (error) {
|
|
569
|
-
// If axe-core is not installed, do manual checks
|
|
570
|
-
const frame = await page.frame({ url: /iframe\.html/ });
|
|
571
|
-
if (frame) {
|
|
572
|
-
// At minimum, check for basic accessibility
|
|
573
|
-
const root = frame.locator('[data-testid="root"]').first();
|
|
574
|
-
if (await root.count() > 0) {
|
|
575
|
-
await expect(root).toBeVisible();
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
});
|
|
580
|
-
});
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
test.describe('Mobile Accessibility', () => {
|
|
584
|
-
test.beforeEach(async ({ page }) => {
|
|
585
|
-
// Set mobile viewport
|
|
586
|
-
await page.setViewportSize({ width: 375, height: 667 });
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
test('Touch targets are adequately sized', async ({ page }) => {
|
|
590
|
-
frame = await navigateToStory(page, '/?path=/story/wl-button--default');
|
|
591
|
-
|
|
592
|
-
const button = frame.locator('button.v-btn').first();
|
|
593
|
-
|
|
594
|
-
// Get button dimensions
|
|
595
|
-
const dimensions = await button.boundingBox();
|
|
596
|
-
|
|
597
|
-
if (dimensions) {
|
|
598
|
-
// WCAG recommends minimum 44x44 pixels for touch targets
|
|
599
|
-
expect(dimensions.width).toBeGreaterThanOrEqual(44);
|
|
600
|
-
expect(dimensions.height).toBeGreaterThanOrEqual(44);
|
|
601
|
-
}
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
test('Form inputs are properly sized for mobile', async ({ page }) => {
|
|
605
|
-
frame = await navigateToStory(page, '/?path=/story/wl-input--default');
|
|
606
|
-
|
|
607
|
-
const input = frame.locator('input').first();
|
|
608
|
-
|
|
609
|
-
// Get input dimensions
|
|
610
|
-
const dimensions = await input.boundingBox();
|
|
611
|
-
|
|
612
|
-
if (dimensions) {
|
|
613
|
-
// Input should be tall enough for touch
|
|
614
|
-
expect(dimensions.height).toBeGreaterThanOrEqual(40);
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
test('Modals are accessible on mobile', async ({ page }) => {
|
|
619
|
-
frame = await navigateToStory(page, '/?path=/story/wl-dialog--default-dialog');
|
|
620
|
-
|
|
621
|
-
const trigger = frame.locator('button').filter({ hasText: 'Neuen Entwurf erstellen' }).first();
|
|
622
|
-
await trigger.click();
|
|
623
|
-
|
|
624
|
-
await page.waitForTimeout(500);
|
|
625
|
-
|
|
626
|
-
const dialog = frame.locator('[role="dialog"], .v-dialog').first();
|
|
627
|
-
if (await dialog.count() > 0) {
|
|
628
|
-
// Dialog should be visible and properly sized
|
|
629
|
-
await expect(dialog).toBeVisible();
|
|
630
|
-
|
|
631
|
-
const dimensions = await dialog.boundingBox();
|
|
632
|
-
if (dimensions) {
|
|
633
|
-
// Should not exceed viewport width
|
|
634
|
-
expect(dimensions.width).toBeLessThanOrEqual(375);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
});
|
|
638
|
-
});
|
|
1
|
+
import { test, expect, Frame } from '@playwright/test';
|
|
2
|
+
import { navigateToStory } from './storybook-utils';
|
|
3
|
+
import AxeBuilder from '@axe-core/playwright';
|
|
4
|
+
|
|
5
|
+
test.describe('Accessibility Tests', () => {
|
|
6
|
+
let frame: Frame;
|
|
7
|
+
|
|
8
|
+
test.describe('Core Components Accessibility', () => {
|
|
9
|
+
test('Button - keyboard navigation and ARIA', async ({ page }) => {
|
|
10
|
+
frame = await navigateToStory(page, '/?path=/story/wl-button--default');
|
|
11
|
+
|
|
12
|
+
const button = frame.locator('button.v-btn').first();
|
|
13
|
+
|
|
14
|
+
// Keyboard navigation
|
|
15
|
+
await button.focus();
|
|
16
|
+
await expect(button).toBeFocused();
|
|
17
|
+
|
|
18
|
+
// Space key activation
|
|
19
|
+
await page.keyboard.press('Space');
|
|
20
|
+
|
|
21
|
+
// Enter key activation
|
|
22
|
+
await page.keyboard.press('Enter');
|
|
23
|
+
|
|
24
|
+
// Check ARIA attributes
|
|
25
|
+
const ariaLabel = await button.getAttribute('aria-label');
|
|
26
|
+
const ariaPressed = await button.getAttribute('aria-pressed');
|
|
27
|
+
const role = await button.getAttribute('role');
|
|
28
|
+
|
|
29
|
+
// Button should be keyboard accessible
|
|
30
|
+
const tabIndex = await button.getAttribute('tabindex');
|
|
31
|
+
expect(tabIndex === null || parseInt(tabIndex) >= 0).toBeTruthy();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('Input - label association and ARIA', async ({ page }) => {
|
|
35
|
+
frame = await navigateToStory(page, '/?path=/story/wl-input--default');
|
|
36
|
+
|
|
37
|
+
const input = frame.locator('input').first();
|
|
38
|
+
|
|
39
|
+
// Vuetify uses aria-label or placeholder instead of traditional labels
|
|
40
|
+
// Check for accessible labeling
|
|
41
|
+
const ariaLabel = await input.getAttribute('aria-label');
|
|
42
|
+
const placeholder = await input.getAttribute('placeholder');
|
|
43
|
+
const ariaLabelledBy = await input.getAttribute('aria-labelledby');
|
|
44
|
+
|
|
45
|
+
// At least one form of labeling should exist
|
|
46
|
+
expect(ariaLabel || placeholder || ariaLabelledBy).toBeTruthy();
|
|
47
|
+
|
|
48
|
+
// Check if there's a label element (Vuetify might generate one)
|
|
49
|
+
const labels = frame.locator('label');
|
|
50
|
+
if (await labels.count() > 0) {
|
|
51
|
+
const label = labels.first();
|
|
52
|
+
const inputId = await input.getAttribute('id');
|
|
53
|
+
const labelFor = await label.getAttribute('for');
|
|
54
|
+
|
|
55
|
+
if (inputId && labelFor) {
|
|
56
|
+
expect(inputId).toBe(labelFor);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check ARIA attributes
|
|
61
|
+
const ariaRequired = await input.getAttribute('aria-required');
|
|
62
|
+
const ariaInvalid = await input.getAttribute('aria-invalid');
|
|
63
|
+
const ariaDescribedBy = await input.getAttribute('aria-describedby');
|
|
64
|
+
|
|
65
|
+
// Keyboard navigation
|
|
66
|
+
await input.focus();
|
|
67
|
+
await expect(input).toBeFocused();
|
|
68
|
+
|
|
69
|
+
// Tab navigation
|
|
70
|
+
await page.keyboard.press('Tab');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('Select - keyboard navigation and screen reader support', async ({ page }) => {
|
|
74
|
+
frame = await navigateToStory(page, '/?path=/story/wl-select--default');
|
|
75
|
+
|
|
76
|
+
const select = frame.locator('.wl-select').first();
|
|
77
|
+
|
|
78
|
+
// Vuetify v-combobox uses input element with combobox role
|
|
79
|
+
const comboboxInput = select.locator('input').first();
|
|
80
|
+
const comboboxContainer = select.locator('[role="combobox"]').first();
|
|
81
|
+
|
|
82
|
+
if (await comboboxContainer.count() > 0) {
|
|
83
|
+
// Check ARIA attributes on the container
|
|
84
|
+
const ariaExpanded = await comboboxContainer.getAttribute('aria-expanded');
|
|
85
|
+
const ariaHasPopup = await comboboxContainer.getAttribute('aria-haspopup');
|
|
86
|
+
const ariaControls = await comboboxContainer.getAttribute('aria-controls');
|
|
87
|
+
|
|
88
|
+
expect(ariaExpanded).toBeDefined();
|
|
89
|
+
expect(ariaHasPopup).toBeDefined();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Focus the input element within the select
|
|
93
|
+
await comboboxInput.click();
|
|
94
|
+
await comboboxInput.focus();
|
|
95
|
+
await expect(comboboxInput).toBeFocused();
|
|
96
|
+
|
|
97
|
+
// Arrow key navigation to open dropdown
|
|
98
|
+
await page.keyboard.press('ArrowDown');
|
|
99
|
+
await page.waitForTimeout(500);
|
|
100
|
+
|
|
101
|
+
// Check if dropdown opened (Vuetify renders menus in the frame)
|
|
102
|
+
const dropdown = frame.locator('[role="listbox"], .v-menu, .v-list').first();
|
|
103
|
+
if (await dropdown.count() > 0 && await dropdown.isVisible()) {
|
|
104
|
+
// Navigate options with arrow keys
|
|
105
|
+
await page.keyboard.press('ArrowDown');
|
|
106
|
+
await page.keyboard.press('ArrowUp');
|
|
107
|
+
|
|
108
|
+
// Select with Enter
|
|
109
|
+
await page.keyboard.press('Enter');
|
|
110
|
+
await page.waitForTimeout(300);
|
|
111
|
+
} else {
|
|
112
|
+
// If dropdown didn't open with ArrowDown, try clicking the dropdown icon
|
|
113
|
+
const dropdownIcon = select.locator('.v-input__icon, [aria-label*="menu"], .mdi-menu-down').first();
|
|
114
|
+
if (await dropdownIcon.count() > 0) {
|
|
115
|
+
await dropdownIcon.click();
|
|
116
|
+
await page.waitForTimeout(500);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Escape to close
|
|
121
|
+
await page.keyboard.press('Escape');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('Checkbox - keyboard control and ARIA states', async ({ page }) => {
|
|
125
|
+
frame = await navigateToStory(page, '/?path=/story/wl-checkbox--default');
|
|
126
|
+
|
|
127
|
+
const checkbox = frame.locator('input[type="checkbox"]').first();
|
|
128
|
+
|
|
129
|
+
// Focus and keyboard control
|
|
130
|
+
await checkbox.focus();
|
|
131
|
+
await expect(checkbox).toBeFocused();
|
|
132
|
+
|
|
133
|
+
// Space to toggle
|
|
134
|
+
const initialChecked = await checkbox.isChecked();
|
|
135
|
+
await page.keyboard.press('Space');
|
|
136
|
+
await expect(checkbox).toBeChecked({ checked: !initialChecked });
|
|
137
|
+
|
|
138
|
+
// Check ARIA attributes
|
|
139
|
+
const ariaChecked = await checkbox.getAttribute('aria-checked');
|
|
140
|
+
const role = await checkbox.getAttribute('role');
|
|
141
|
+
|
|
142
|
+
// Label association
|
|
143
|
+
const label = frame.locator('label').first();
|
|
144
|
+
const labelText = await label.textContent();
|
|
145
|
+
expect(labelText).toBeTruthy();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('DateInput - date picker accessibility', async ({ page }) => {
|
|
149
|
+
frame = await navigateToStory(page, '/?path=/story/wl-dateinput--default');
|
|
150
|
+
|
|
151
|
+
const dateInput = frame.locator('input').first();
|
|
152
|
+
|
|
153
|
+
// Focus management
|
|
154
|
+
await dateInput.focus();
|
|
155
|
+
await expect(dateInput).toBeFocused();
|
|
156
|
+
|
|
157
|
+
// Check for accessible date format hint
|
|
158
|
+
const ariaDescribedBy = await dateInput.getAttribute('aria-describedby');
|
|
159
|
+
const placeholder = await dateInput.getAttribute('placeholder');
|
|
160
|
+
|
|
161
|
+
// Keyboard navigation for date picker
|
|
162
|
+
await dateInput.click();
|
|
163
|
+
await page.waitForTimeout(500);
|
|
164
|
+
|
|
165
|
+
// Check for calendar ARIA attributes if picker opens
|
|
166
|
+
const calendar = frame.locator('[role="grid"], [role="application"]').first();
|
|
167
|
+
if (await calendar.count() > 0) {
|
|
168
|
+
// Navigate with arrow keys
|
|
169
|
+
await page.keyboard.press('ArrowRight');
|
|
170
|
+
await page.keyboard.press('ArrowDown');
|
|
171
|
+
await page.keyboard.press('Enter');
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('Dialog - focus trap and ARIA', async ({ page }) => {
|
|
176
|
+
frame = await navigateToStory(page, '/?path=/story/wl-dialog--default-dialog');
|
|
177
|
+
|
|
178
|
+
// Open dialog
|
|
179
|
+
const trigger = frame.locator('button').filter({ hasText: 'Neuen Entwurf erstellen' }).first();
|
|
180
|
+
await trigger.click();
|
|
181
|
+
|
|
182
|
+
await page.waitForTimeout(500);
|
|
183
|
+
|
|
184
|
+
const dialog = frame.locator('[role="dialog"], .v-dialog').first();
|
|
185
|
+
if (await dialog.count() > 0) {
|
|
186
|
+
await expect(dialog).toBeVisible();
|
|
187
|
+
|
|
188
|
+
// Check ARIA attributes
|
|
189
|
+
const ariaModal = await dialog.getAttribute('aria-modal');
|
|
190
|
+
const ariaLabelledBy = await dialog.getAttribute('aria-labelledby');
|
|
191
|
+
const role = await dialog.getAttribute('role');
|
|
192
|
+
|
|
193
|
+
// Focus should be trapped within dialog
|
|
194
|
+
await page.keyboard.press('Tab');
|
|
195
|
+
const focusedElement = await page.evaluate(() => document.activeElement?.tagName);
|
|
196
|
+
|
|
197
|
+
// Escape to close
|
|
198
|
+
await page.keyboard.press('Escape');
|
|
199
|
+
await expect(dialog).not.toBeVisible();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('TextArea - multiline text accessibility', async ({ page }) => {
|
|
204
|
+
frame = await navigateToStory(page, '/?path=/story/wl-textarea--default');
|
|
205
|
+
|
|
206
|
+
const textarea = frame.locator('textarea').first();
|
|
207
|
+
|
|
208
|
+
// Focus and keyboard input
|
|
209
|
+
await textarea.focus();
|
|
210
|
+
await expect(textarea).toBeFocused();
|
|
211
|
+
|
|
212
|
+
// Multiline input with keyboard
|
|
213
|
+
await textarea.type('Line 1');
|
|
214
|
+
await page.keyboard.press('Enter');
|
|
215
|
+
await textarea.type('Line 2');
|
|
216
|
+
|
|
217
|
+
// Check ARIA attributes
|
|
218
|
+
const ariaMultiline = await textarea.getAttribute('aria-multiline');
|
|
219
|
+
const rows = await textarea.getAttribute('rows');
|
|
220
|
+
|
|
221
|
+
// Character count accessibility if present
|
|
222
|
+
const counter = frame.locator('[role="status"], .v-counter').first();
|
|
223
|
+
if (await counter.count() > 0) {
|
|
224
|
+
const ariaLive = await counter.getAttribute('aria-live');
|
|
225
|
+
expect(ariaLive).toBeTruthy();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('OtpInput - multiple input fields navigation', async ({ page }) => {
|
|
230
|
+
frame = await navigateToStory(page, '/?path=/story/wl-otpinput--default');
|
|
231
|
+
|
|
232
|
+
const inputs = frame.locator('input');
|
|
233
|
+
const inputCount = await inputs.count();
|
|
234
|
+
|
|
235
|
+
if (inputCount > 0) {
|
|
236
|
+
// Focus first input
|
|
237
|
+
await inputs.first().focus();
|
|
238
|
+
await expect(inputs.first()).toBeFocused();
|
|
239
|
+
|
|
240
|
+
// Type and auto-advance
|
|
241
|
+
await inputs.first().type('1');
|
|
242
|
+
await page.waitForTimeout(100);
|
|
243
|
+
|
|
244
|
+
// Should auto-focus next input
|
|
245
|
+
if (inputCount > 1) {
|
|
246
|
+
await expect(inputs.nth(1)).toBeFocused();
|
|
247
|
+
|
|
248
|
+
// Backspace to go back
|
|
249
|
+
await page.keyboard.press('Backspace');
|
|
250
|
+
await page.waitForTimeout(100);
|
|
251
|
+
await expect(inputs.first()).toBeFocused();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Arrow key navigation
|
|
255
|
+
await page.keyboard.press('ArrowRight');
|
|
256
|
+
if (inputCount > 1) {
|
|
257
|
+
await expect(inputs.nth(1)).toBeFocused();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
await page.keyboard.press('ArrowLeft');
|
|
261
|
+
await expect(inputs.first()).toBeFocused();
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test.describe('Laboratory Components Accessibility', () => {
|
|
267
|
+
test('AppointmentCard - semantic structure and ARIA', async ({ page }) => {
|
|
268
|
+
frame = await navigateToStory(page, '/?path=/story/wl-laboratory-cards-appointmentcard--draft-status');
|
|
269
|
+
|
|
270
|
+
const card = frame.locator('.appointment-card').first();
|
|
271
|
+
|
|
272
|
+
// Check semantic HTML
|
|
273
|
+
const headings = card.locator('h1, h2, h3, h4, h5, h6');
|
|
274
|
+
const headingCount = await headings.count();
|
|
275
|
+
expect(headingCount).toBeGreaterThan(0);
|
|
276
|
+
|
|
277
|
+
// Interactive elements should be keyboard accessible
|
|
278
|
+
const buttons = card.locator('button');
|
|
279
|
+
if (await buttons.count() > 0) {
|
|
280
|
+
await buttons.first().focus();
|
|
281
|
+
await expect(buttons.first()).toBeFocused();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Check for proper heading hierarchy
|
|
285
|
+
const h3 = card.locator('h3').first();
|
|
286
|
+
if (await h3.count() > 0) {
|
|
287
|
+
const text = await h3.textContent();
|
|
288
|
+
expect(text).toBeTruthy();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('ChatMessage - message accessibility', async ({ page }) => {
|
|
293
|
+
frame = await navigateToStory(page, '/?path=/story/wl-laboratory-chat-chatmessage--default');
|
|
294
|
+
|
|
295
|
+
const message = frame.locator('.v-alert, [role="alert"]').first();
|
|
296
|
+
|
|
297
|
+
if (await message.count() > 0) {
|
|
298
|
+
// Check ARIA role
|
|
299
|
+
const role = await message.getAttribute('role');
|
|
300
|
+
|
|
301
|
+
// Check for accessible timestamp
|
|
302
|
+
const time = message.locator('time, [datetime]').first();
|
|
303
|
+
if (await time.count() > 0) {
|
|
304
|
+
const datetime = await time.getAttribute('datetime');
|
|
305
|
+
expect(datetime).toBeTruthy();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('ProgressCircle - progress indication accessibility', async ({ page }) => {
|
|
311
|
+
frame = await navigateToStory(page, '/?path=/story/wl-laboratory-progresscircle--default');
|
|
312
|
+
|
|
313
|
+
const progress = frame.locator('[role="progressbar"], .v-progress-circular').first();
|
|
314
|
+
|
|
315
|
+
if (await progress.count() > 0) {
|
|
316
|
+
// Check ARIA attributes for progress
|
|
317
|
+
const ariaValueNow = await progress.getAttribute('aria-valuenow');
|
|
318
|
+
const ariaValueMin = await progress.getAttribute('aria-valuemin');
|
|
319
|
+
const ariaValueMax = await progress.getAttribute('aria-valuemax');
|
|
320
|
+
|
|
321
|
+
// Should have accessible label
|
|
322
|
+
const ariaLabel = await progress.getAttribute('aria-label');
|
|
323
|
+
if (!ariaLabel) {
|
|
324
|
+
const ariaLabelledBy = await progress.getAttribute('aria-labelledby');
|
|
325
|
+
expect(ariaLabel || ariaLabelledBy).toBeTruthy();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('Timeline - list semantics and navigation', async ({ page }) => {
|
|
331
|
+
frame = await navigateToStory(page, '/?path=/story/wl-laboratory-timeline--default');
|
|
332
|
+
|
|
333
|
+
const timeline = frame.locator('.v-timeline').first();
|
|
334
|
+
|
|
335
|
+
if (await timeline.count() > 0) {
|
|
336
|
+
// Check for list semantics
|
|
337
|
+
const listItems = timeline.locator('[role="listitem"], li, .v-timeline-item');
|
|
338
|
+
const itemCount = await listItems.count();
|
|
339
|
+
expect(itemCount).toBeGreaterThan(0);
|
|
340
|
+
|
|
341
|
+
// Each item should have meaningful content
|
|
342
|
+
if (itemCount > 0) {
|
|
343
|
+
const firstItem = listItems.first();
|
|
344
|
+
const content = await firstItem.textContent();
|
|
345
|
+
expect(content?.trim()).toBeTruthy();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test.describe('Color Contrast and Visual Accessibility', () => {
|
|
352
|
+
test('Button color contrast', async ({ page }) => {
|
|
353
|
+
frame = await navigateToStory(page, '/?path=/story/wl-button--default');
|
|
354
|
+
|
|
355
|
+
const button = frame.locator('button.v-btn').first();
|
|
356
|
+
|
|
357
|
+
// Get computed styles
|
|
358
|
+
const styles = await button.evaluate((el) => {
|
|
359
|
+
const computed = window.getComputedStyle(el);
|
|
360
|
+
return {
|
|
361
|
+
color: computed.color,
|
|
362
|
+
backgroundColor: computed.backgroundColor,
|
|
363
|
+
fontSize: computed.fontSize
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Ensure text is readable size
|
|
368
|
+
const fontSize = parseInt(styles.fontSize);
|
|
369
|
+
expect(fontSize).toBeGreaterThanOrEqual(12);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('Focus indicators visibility', async ({ page }) => {
|
|
373
|
+
frame = await navigateToStory(page, '/?path=/story/wl-input--default');
|
|
374
|
+
|
|
375
|
+
const input = frame.locator('input').first();
|
|
376
|
+
|
|
377
|
+
// Get initial outline
|
|
378
|
+
const initialOutline = await input.evaluate((el) => {
|
|
379
|
+
const computed = window.getComputedStyle(el);
|
|
380
|
+
return computed.outline;
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Focus and check for visible focus indicator
|
|
384
|
+
await input.focus();
|
|
385
|
+
|
|
386
|
+
const focusedOutline = await input.evaluate((el) => {
|
|
387
|
+
const computed = window.getComputedStyle(el);
|
|
388
|
+
return {
|
|
389
|
+
outline: computed.outline,
|
|
390
|
+
outlineWidth: computed.outlineWidth,
|
|
391
|
+
boxShadow: computed.boxShadow,
|
|
392
|
+
border: computed.border
|
|
393
|
+
};
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Should have some visual focus indicator
|
|
397
|
+
const hasFocusIndicator =
|
|
398
|
+
focusedOutline.outline !== 'none' ||
|
|
399
|
+
focusedOutline.boxShadow !== 'none' ||
|
|
400
|
+
focusedOutline.border !== initialOutline;
|
|
401
|
+
|
|
402
|
+
expect(hasFocusIndicator).toBeTruthy();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('Error state contrast and messaging', async ({ page }) => {
|
|
406
|
+
frame = await navigateToStory(page, '/?path=/story/wl-input--error');
|
|
407
|
+
|
|
408
|
+
const errorMessage = frame.locator('[role="alert"], .v-messages__message').first();
|
|
409
|
+
|
|
410
|
+
if (await errorMessage.count() > 0) {
|
|
411
|
+
await expect(errorMessage).toBeVisible();
|
|
412
|
+
|
|
413
|
+
// Check error color contrast
|
|
414
|
+
const errorColor = await errorMessage.evaluate((el) => {
|
|
415
|
+
const computed = window.getComputedStyle(el);
|
|
416
|
+
return computed.color;
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Error messages should use high contrast colors
|
|
420
|
+
expect(errorColor).toBeTruthy();
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test.describe('Screen Reader Support', () => {
|
|
426
|
+
test('Form field descriptions and instructions', async ({ page }) => {
|
|
427
|
+
frame = await navigateToStory(page, '/?path=/story/wl-input--default');
|
|
428
|
+
|
|
429
|
+
const input = frame.locator('input').first();
|
|
430
|
+
const helperText = frame.locator('.v-messages, [id*="hint"], [id*="helper"]').first();
|
|
431
|
+
|
|
432
|
+
if (await helperText.count() > 0) {
|
|
433
|
+
const helperId = await helperText.getAttribute('id');
|
|
434
|
+
const ariaDescribedBy = await input.getAttribute('aria-describedby');
|
|
435
|
+
|
|
436
|
+
if (helperId && ariaDescribedBy) {
|
|
437
|
+
expect(ariaDescribedBy).toContain(helperId);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('Live regions for dynamic content', async ({ page }) => {
|
|
443
|
+
frame = await navigateToStory(page, '/?path=/story/wl-notificationbubble--with-text');
|
|
444
|
+
|
|
445
|
+
const notification = frame.locator('.notification-bubble').first();
|
|
446
|
+
|
|
447
|
+
// Check for live region attributes
|
|
448
|
+
const ariaLive = await notification.getAttribute('aria-live');
|
|
449
|
+
const role = await notification.getAttribute('role');
|
|
450
|
+
|
|
451
|
+
// Notifications should announce to screen readers
|
|
452
|
+
if (role === 'alert' || role === 'status') {
|
|
453
|
+
expect(role).toBeTruthy();
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test('Icon-only buttons have accessible labels', async ({ page }) => {
|
|
458
|
+
frame = await navigateToStory(page, '/?path=/story/wl-button--with-prepend-icon');
|
|
459
|
+
|
|
460
|
+
const iconButtons = frame.locator('button.v-btn').filter({ hasText: '' });
|
|
461
|
+
|
|
462
|
+
for (let i = 0; i < await iconButtons.count(); i++) {
|
|
463
|
+
const button = iconButtons.nth(i);
|
|
464
|
+
const ariaLabel = await button.getAttribute('aria-label');
|
|
465
|
+
const title = await button.getAttribute('title');
|
|
466
|
+
const innerText = await button.innerText();
|
|
467
|
+
|
|
468
|
+
// Icon-only buttons must have accessible text
|
|
469
|
+
if (!innerText.trim()) {
|
|
470
|
+
expect(ariaLabel || title).toBeTruthy();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test.describe('Keyboard Navigation Patterns', () => {
|
|
477
|
+
test('Tab order follows visual layout', async ({ page }) => {
|
|
478
|
+
frame = await navigateToStory(page, '/?path=/story/wl-input--default');
|
|
479
|
+
|
|
480
|
+
const focusableElements = frame.locator('button:visible, input:visible, select:visible, textarea:visible, a[href]:visible, [tabindex]:not([tabindex="-1"])');
|
|
481
|
+
const elementCount = await focusableElements.count();
|
|
482
|
+
|
|
483
|
+
if (elementCount > 1) {
|
|
484
|
+
// Tab through elements
|
|
485
|
+
for (let i = 0; i < elementCount; i++) {
|
|
486
|
+
await page.keyboard.press('Tab');
|
|
487
|
+
await page.waitForTimeout(100);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Shift+Tab to go backwards
|
|
491
|
+
for (let i = 0; i < elementCount; i++) {
|
|
492
|
+
await page.keyboard.press('Shift+Tab');
|
|
493
|
+
await page.waitForTimeout(100);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test('Escape key closes overlays', async ({ page }) => {
|
|
499
|
+
frame = await navigateToStory(page, '/?path=/story/wl-select--default');
|
|
500
|
+
|
|
501
|
+
const select = frame.locator('.wl-select input').first();
|
|
502
|
+
|
|
503
|
+
// Open dropdown
|
|
504
|
+
await select.click();
|
|
505
|
+
await page.waitForTimeout(500);
|
|
506
|
+
|
|
507
|
+
const dropdown = frame.locator('.v-menu, [role="listbox"]').first();
|
|
508
|
+
if (await dropdown.count() > 0) {
|
|
509
|
+
await expect(dropdown).toBeVisible();
|
|
510
|
+
|
|
511
|
+
// Escape to close
|
|
512
|
+
await page.keyboard.press('Escape');
|
|
513
|
+
await page.waitForTimeout(500);
|
|
514
|
+
await expect(dropdown).not.toBeVisible();
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test('Arrow keys navigate within components', async ({ page }) => {
|
|
519
|
+
frame = await navigateToStory(page, '/?path=/story/wl-accordion--outlined-inset');
|
|
520
|
+
|
|
521
|
+
const accordion = frame.locator('.v-expansion-panels').first();
|
|
522
|
+
const panels = accordion.locator('.v-expansion-panel');
|
|
523
|
+
|
|
524
|
+
if (await panels.count() > 1) {
|
|
525
|
+
await panels.first().focus();
|
|
526
|
+
|
|
527
|
+
// Arrow down to next panel
|
|
528
|
+
await page.keyboard.press('ArrowDown');
|
|
529
|
+
await page.waitForTimeout(100);
|
|
530
|
+
|
|
531
|
+
// Arrow up to previous panel
|
|
532
|
+
await page.keyboard.press('ArrowUp');
|
|
533
|
+
await page.waitForTimeout(100);
|
|
534
|
+
|
|
535
|
+
// Space or Enter to expand
|
|
536
|
+
await page.keyboard.press('Space');
|
|
537
|
+
await page.waitForTimeout(500);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test.describe('Automated Accessibility Testing with Axe', () => {
|
|
543
|
+
const componentsToTest = [
|
|
544
|
+
{ name: 'Button', path: '/?path=/story/wl-button--default' },
|
|
545
|
+
{ name: 'Input', path: '/?path=/story/wl-input--default' },
|
|
546
|
+
{ name: 'Select', path: '/?path=/story/wl-select--default' },
|
|
547
|
+
{ name: 'Checkbox', path: '/?path=/story/wl-checkbox--default' },
|
|
548
|
+
{ name: 'DateInput', path: '/?path=/story/wl-dateinput--default' },
|
|
549
|
+
{ name: 'Dialog', path: '/?path=/story/wl-dialog--default-dialog' },
|
|
550
|
+
{ name: 'Accordion', path: '/?path=/story/wl-accordion--outlined-inset' },
|
|
551
|
+
{ name: 'OtpInput', path: '/?path=/story/wl-otpinput--default' },
|
|
552
|
+
{ name: 'TextArea', path: '/?path=/story/wl-textarea--default' },
|
|
553
|
+
{ name: 'NotificationBubble', path: '/?path=/story/wl-notificationbubble--with-text' }
|
|
554
|
+
];
|
|
555
|
+
|
|
556
|
+
componentsToTest.forEach(({ name, path }) => {
|
|
557
|
+
test(`${name} - automated accessibility scan`, async ({ page }) => {
|
|
558
|
+
await page.goto(path);
|
|
559
|
+
await page.waitForTimeout(1000);
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
const accessibilityScanResults = await new AxeBuilder({ page })
|
|
563
|
+
.include('[data-testid="root"]')
|
|
564
|
+
.analyze();
|
|
565
|
+
|
|
566
|
+
// Check for violations
|
|
567
|
+
expect(accessibilityScanResults.violations).toEqual([]);
|
|
568
|
+
} catch (error) {
|
|
569
|
+
// If axe-core is not installed, do manual checks
|
|
570
|
+
const frame = await page.frame({ url: /iframe\.html/ });
|
|
571
|
+
if (frame) {
|
|
572
|
+
// At minimum, check for basic accessibility
|
|
573
|
+
const root = frame.locator('[data-testid="root"]').first();
|
|
574
|
+
if (await root.count() > 0) {
|
|
575
|
+
await expect(root).toBeVisible();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test.describe('Mobile Accessibility', () => {
|
|
584
|
+
test.beforeEach(async ({ page }) => {
|
|
585
|
+
// Set mobile viewport
|
|
586
|
+
await page.setViewportSize({ width: 375, height: 667 });
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test('Touch targets are adequately sized', async ({ page }) => {
|
|
590
|
+
frame = await navigateToStory(page, '/?path=/story/wl-button--default');
|
|
591
|
+
|
|
592
|
+
const button = frame.locator('button.v-btn').first();
|
|
593
|
+
|
|
594
|
+
// Get button dimensions
|
|
595
|
+
const dimensions = await button.boundingBox();
|
|
596
|
+
|
|
597
|
+
if (dimensions) {
|
|
598
|
+
// WCAG recommends minimum 44x44 pixels for touch targets
|
|
599
|
+
expect(dimensions.width).toBeGreaterThanOrEqual(44);
|
|
600
|
+
expect(dimensions.height).toBeGreaterThanOrEqual(44);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test('Form inputs are properly sized for mobile', async ({ page }) => {
|
|
605
|
+
frame = await navigateToStory(page, '/?path=/story/wl-input--default');
|
|
606
|
+
|
|
607
|
+
const input = frame.locator('input').first();
|
|
608
|
+
|
|
609
|
+
// Get input dimensions
|
|
610
|
+
const dimensions = await input.boundingBox();
|
|
611
|
+
|
|
612
|
+
if (dimensions) {
|
|
613
|
+
// Input should be tall enough for touch
|
|
614
|
+
expect(dimensions.height).toBeGreaterThanOrEqual(40);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test('Modals are accessible on mobile', async ({ page }) => {
|
|
619
|
+
frame = await navigateToStory(page, '/?path=/story/wl-dialog--default-dialog');
|
|
620
|
+
|
|
621
|
+
const trigger = frame.locator('button').filter({ hasText: 'Neuen Entwurf erstellen' }).first();
|
|
622
|
+
await trigger.click();
|
|
623
|
+
|
|
624
|
+
await page.waitForTimeout(500);
|
|
625
|
+
|
|
626
|
+
const dialog = frame.locator('[role="dialog"], .v-dialog').first();
|
|
627
|
+
if (await dialog.count() > 0) {
|
|
628
|
+
// Dialog should be visible and properly sized
|
|
629
|
+
await expect(dialog).toBeVisible();
|
|
630
|
+
|
|
631
|
+
const dimensions = await dialog.boundingBox();
|
|
632
|
+
if (dimensions) {
|
|
633
|
+
// Should not exceed viewport width
|
|
634
|
+
expect(dimensions.width).toBeLessThanOrEqual(375);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
639
|
});
|