@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,532 +1,532 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { mount, VueWrapper } from '@vue/test-utils';
3
- import { format } from 'date-fns';
4
- import { de } from 'date-fns/locale';
5
- import Card from '../../../../../src/components/Appointment/Card/Card.vue';
6
- import type { AppointmentData, Dentist } from '../../../../../src/types';
7
-
8
- // Mock vue-i18n
9
- vi.mock('vue-i18n', () => ({
10
- useI18n: () => ({
11
- t: (key: string) => {
12
- const translations: Record<string, string> = {
13
- 'wl.appointment_card.unknown_patient': 'Unbekannter Patient'
14
- };
15
- return translations[key] || key;
16
- }
17
- })
18
- }));
19
-
20
- // Mock appointmentTypes
21
- vi.mock('../../../../../src/utils/index', () => ({
22
- appointmentTypes: [
23
- { id: 1, title: 'Regular Checkup', icon: 'checkup' },
24
- { id: 2, title: 'Treatment', icon: 'treatment' }
25
- ]
26
- }));
27
-
28
- describe('Appointment Card', () => {
29
- let wrapper: VueWrapper;
30
-
31
- const createMockDentist = (overrides: Partial<Dentist> = {}): Dentist => ({
32
- name: 'Dr. Test Dentist',
33
- gender: 'Male',
34
- imageSrc: 'https://example.com/dentist.jpg',
35
- ...overrides
36
- });
37
-
38
- const createMockAppointment = (overrides: Partial<AppointmentData> = {}): AppointmentData => ({
39
- id: '123',
40
- template_name: 'Test Treatment',
41
- description: 'Test Description',
42
- dentist: createMockDentist(),
43
- start: '2024-01-15T10:30:00.000Z',
44
- type: 1,
45
- status: 'upcoming',
46
- patientName: 'John Doe',
47
- address: 'Test Address 123',
48
- district: 'Test District',
49
- is_confirmed: false,
50
- ...overrides
51
- });
52
-
53
- const createGlobalConfig = () => ({
54
- stubs: {
55
- UserIcon: {
56
- template: '<svg class="header-icon user-icon"></svg>'
57
- },
58
- CalendarIcon: {
59
- template: '<svg class="header-icon calendar-icon"></svg>'
60
- },
61
- ClockIcon: {
62
- template: '<svg class="header-icon clock-icon"></svg>'
63
- },
64
- IconsAdvanceAppointments: {
65
- template: '<div class="advance-appointments-icon"></div>',
66
- props: ['width', 'height', 'iconType']
67
- },
68
- Details: {
69
- template: '<div class="details-stub">Details Component</div>',
70
- props: ['appointment', 'dentistImageSrc']
71
- },
72
- Actions: {
73
- template: '<div class="actions-stub" @confirmed="$emit(\'confirmed\', $event)" @cancelled="$emit(\'cancelled\', $event)" @rescheduled="$emit(\'rescheduled\', $event)">Actions Component</div>',
74
- props: ['appointment', 'disabled'],
75
- emits: ['confirmed', 'cancelled', 'rescheduled']
76
- },
77
- AnamneseNotification: {
78
- template: '<div class="anamnese-notification-stub"></div>'
79
- }
80
- }
81
- });
82
-
83
- beforeEach(() => {
84
- const appointment = createMockAppointment();
85
- wrapper = mount(Card, {
86
- props: { appointment, showActions: true },
87
- global: createGlobalConfig()
88
- });
89
- });
90
-
91
- describe('Component Rendering', () => {
92
- it('should render the component with correct test id', () => {
93
- expect(wrapper.find('[data-testid="root"]').exists()).toBe(true);
94
- });
95
-
96
- it('should render the appointment card structure', () => {
97
- expect(wrapper.find('.appointment-card').exists()).toBe(true);
98
- expect(wrapper.find('.card-header').exists()).toBe(true);
99
- expect(wrapper.find('.card-body').exists()).toBe(true);
100
- expect(wrapper.find('.card-footer').exists()).toBe(true);
101
- });
102
-
103
- it('should render Details and Actions components', () => {
104
- expect(wrapper.find('.details-stub').exists()).toBe(true);
105
- expect(wrapper.find('.actions-stub').exists()).toBe(true);
106
- });
107
- });
108
-
109
- describe('Header Information', () => {
110
- it('should display patient name', () => {
111
- expect(wrapper.text()).toContain('John Doe');
112
- });
113
-
114
- it('should display default patient name when not provided', async () => {
115
- const appointmentWithoutPatient = createMockAppointment({ patientName: undefined });
116
- await wrapper.setProps({ appointment: appointmentWithoutPatient });
117
-
118
- expect(wrapper.text()).toContain('Unbekannter Patient');
119
- });
120
-
121
- it('should display formatted date', () => {
122
- const expectedDate = format(new Date('2024-01-15T10:30:00Z'), 'EEE, d MMM yyyy', { locale: de });
123
- expect(wrapper.text()).toContain(expectedDate);
124
- });
125
-
126
- it('should display formatted time', () => {
127
- const expectedTime = format(new Date('2024-01-15T10:30:00Z'), 'HH:mm');
128
- expect(wrapper.text()).toContain(expectedTime);
129
- });
130
-
131
- it('should render header icons', () => {
132
- // Component renders 3 header items with icons
133
- const headerItems = wrapper.findAll('.header-item');
134
- expect(headerItems).toHaveLength(3);
135
-
136
- // Each header item should contain an icon element
137
- headerItems.forEach(item => {
138
- expect(item.find('svg').exists()).toBe(true);
139
- });
140
- });
141
-
142
- it('should have correct header items structure', () => {
143
- const headerItems = wrapper.findAll('.header-item');
144
- expect(headerItems).toHaveLength(3);
145
- });
146
- });
147
-
148
- describe('Appointment Status Styling', () => {
149
- it('should apply default header background for upcoming appointments', () => {
150
- const header = wrapper.find('.card-header');
151
- expect(header.classes()).toContain('card-header--default');
152
- expect(header.classes()).not.toContain('card-header--cancelled');
153
- });
154
-
155
- it('should apply cancelled header background for cancelled appointments', async () => {
156
- const cancelledAppointment = createMockAppointment({ status: 'cancelled' });
157
- await wrapper.setProps({ appointment: cancelledAppointment });
158
-
159
- const header = wrapper.find('.card-header');
160
- expect(header.classes()).toContain('card-header--cancelled');
161
- expect(header.classes()).not.toContain('card-header--default');
162
- });
163
-
164
- it('should apply card opacity for done appointments', async () => {
165
- const doneAppointment = createMockAppointment({ status: 'done' });
166
- await wrapper.setProps({ appointment: doneAppointment });
167
-
168
- expect(wrapper.find('.card-opacity').exists()).toBe(true);
169
- });
170
-
171
- it('should not apply card opacity for upcoming appointments', () => {
172
- expect(wrapper.find('.card-opacity').exists()).toBe(false);
173
- });
174
- });
175
-
176
- describe('Template Name Display', () => {
177
- it('should display single template name', () => {
178
- expect(wrapper.text()).toContain('Test Treatment');
179
- });
180
-
181
- it('should display multiple template names as comma-separated string', async () => {
182
- const appointmentWithMultipleTemplates = createMockAppointment({
183
- template_name: ['Treatment A', 'Treatment B', 'Treatment C']
184
- });
185
- await wrapper.setProps({ appointment: appointmentWithMultipleTemplates });
186
-
187
- expect(wrapper.text()).toContain('Treatment A, Treatment B, Treatment C');
188
- });
189
-
190
- it('should handle empty template name array', async () => {
191
- const appointmentWithEmptyTemplates = createMockAppointment({
192
- template_name: []
193
- });
194
- await wrapper.setProps({ appointment: appointmentWithEmptyTemplates });
195
-
196
- // When template_name is empty array, h3 should show empty or appointment type title only
197
- expect(wrapper.find('h3').exists()).toBe(true);
198
- });
199
- });
200
-
201
- describe('Link Functionality', () => {
202
- it('should render appointment link when provided', async () => {
203
- await wrapper.setProps({ appointmentLink: 'https://example.com/appointment/123' });
204
-
205
- const link = wrapper.find('.appointment-card');
206
- expect(link.attributes('href')).toBe('https://example.com/appointment/123');
207
- expect(link.attributes('rel')).toBe('noopener noreferrer');
208
- });
209
-
210
- it('should render default hash link when no appointment link provided', () => {
211
- const link = wrapper.find('.appointment-card');
212
- expect(link.attributes('href')).toBe('#');
213
- });
214
- });
215
-
216
- describe('Event Handling', () => {
217
- it('should emit confirm event when Actions emits confirmed', async () => {
218
- const actionsComponent = wrapper.find('.actions-stub');
219
- await actionsComponent.trigger('confirmed', '123');
220
-
221
- expect(wrapper.emitted('confirm')).toBeTruthy();
222
- expect(wrapper.emitted('confirm')?.[0]).toEqual(['123']);
223
- });
224
-
225
- it('should emit cancel event when Actions emits cancelled', async () => {
226
- const actionsComponent = wrapper.find('.actions-stub');
227
- await actionsComponent.trigger('cancelled', '123');
228
-
229
- expect(wrapper.emitted('cancel')).toBeTruthy();
230
- expect(wrapper.emitted('cancel')?.[0]).toEqual(['123']);
231
- });
232
-
233
- it('should emit reschedule event when Actions emits rescheduled', async () => {
234
- const actionsComponent = wrapper.find('.actions-stub');
235
- await actionsComponent.trigger('rescheduled', '123');
236
-
237
- expect(wrapper.emitted('reschedule')).toBeTruthy();
238
- expect(wrapper.emitted('reschedule')?.[0]).toEqual(['123']);
239
- });
240
- });
241
-
242
- describe('Props Passing', () => {
243
- it('should pass appointment prop to Details component', () => {
244
- const detailsStub = wrapper.find('.details-stub');
245
- expect(detailsStub.exists()).toBe(true);
246
- });
247
-
248
- it('should pass appointment prop to Actions component', () => {
249
- const actionsStub = wrapper.find('.actions-stub');
250
- expect(actionsStub.exists()).toBe(true);
251
- });
252
-
253
- it('should pass dentistImageSrc prop to Details component', async () => {
254
- await wrapper.setProps({ dentistImageSrc: 'https://example.com/custom-dentist.jpg' });
255
- expect(wrapper.props('dentistImageSrc')).toBe('https://example.com/custom-dentist.jpg');
256
- });
257
- });
258
-
259
- describe('Date Formatting Edge Cases', () => {
260
- it('should handle current date when appointment date is null', async () => {
261
- const appointmentWithNullDate = createMockAppointment({ start: null as any });
262
- await wrapper.setProps({ appointment: appointmentWithNullDate });
263
-
264
- // Should not throw an error and should display current date
265
- expect(wrapper.find('.card-header').exists()).toBe(true);
266
- });
267
-
268
- it('should format different date correctly', async () => {
269
- const appointmentWithDifferentDate = createMockAppointment({
270
- start: '2024-12-25T15:45:00Z'
271
- });
272
- await wrapper.setProps({ appointment: appointmentWithDifferentDate });
273
-
274
- const expectedDate = format(new Date('2024-12-25T15:45:00Z'), 'EEE, d MMM yyyy', { locale: de });
275
- const expectedTime = format(new Date('2024-12-25T15:45:00Z'), 'HH:mm');
276
-
277
- expect(wrapper.text()).toContain(expectedDate);
278
- expect(wrapper.text()).toContain(expectedTime);
279
- });
280
- });
281
-
282
- describe('Layout Structure', () => {
283
- it('should have proper layout structure', () => {
284
- const root = wrapper.find('[data-testid="root"]');
285
- expect(root.exists()).toBe(true);
286
-
287
- const appointmentCard = root.find('.appointment-card');
288
- expect(appointmentCard.exists()).toBe(true);
289
-
290
- const cardFooter = root.find('.card-footer');
291
- expect(cardFooter.exists()).toBe(true);
292
- });
293
-
294
- it('should contain horizontal rule separator', () => {
295
- expect(wrapper.find('hr').exists()).toBe(true);
296
- });
297
- });
298
-
299
- describe('Accessibility', () => {
300
- it('should have proper link attributes for accessibility', async () => {
301
- await wrapper.setProps({ appointmentLink: 'https://example.com/appointment/123' });
302
-
303
- const link = wrapper.find('.appointment-card');
304
- expect(link.attributes('rel')).toBe('noopener noreferrer');
305
- });
306
-
307
- it('should have descriptive data-testid', () => {
308
- expect(wrapper.find('[data-testid="root"]').exists()).toBe(true);
309
- });
310
- });
311
-
312
- describe('Component Props Validation', () => {
313
- it('should accept all required props', () => {
314
- expect(wrapper.props('appointment')).toBeDefined();
315
- });
316
-
317
- it('should accept optional props', async () => {
318
- await wrapper.setProps({
319
- appointmentLink: 'https://example.com/test',
320
- dentistImageSrc: 'https://example.com/dentist.jpg'
321
- });
322
-
323
- expect(wrapper.props('appointmentLink')).toBe('https://example.com/test');
324
- expect(wrapper.props('dentistImageSrc')).toBe('https://example.com/dentist.jpg');
325
- });
326
- });
327
-
328
- describe('Accessibility', () => {
329
- beforeEach(() => {
330
- const appointment = createMockAppointment();
331
- wrapper = mount(Card, {
332
- props: { appointment, showActions: true },
333
- global: {
334
- stubs: {
335
- UserIcon: {
336
- template: '<svg class="header-icon user-icon" aria-hidden="true"></svg>'
337
- },
338
- CalendarIcon: {
339
- template: '<svg class="header-icon calendar-icon" aria-hidden="true"></svg>'
340
- },
341
- ClockIcon: {
342
- template: '<svg class="header-icon clock-icon" aria-hidden="true"></svg>'
343
- },
344
- IconsAdvanceAppointments: {
345
- template: '<div class="advance-appointments-icon" aria-hidden="true"></div>',
346
- props: ['width', 'height', 'iconType']
347
- },
348
- Details: {
349
- template: '<div class="details-stub" role="region" aria-label="Appointment details">Details Component</div>',
350
- props: ['appointment', 'dentistImageSrc']
351
- },
352
- Actions: {
353
- template: '<div class="actions-stub" role="group" aria-label="Appointment actions">Actions Component</div>',
354
- props: ['appointment', 'disabled']
355
- },
356
- AnamneseNotification: {
357
- template: '<div class="anamnese-notification-stub"></div>'
358
- }
359
- }
360
- }
361
- });
362
- });
363
-
364
- it('should have proper semantic HTML structure', () => {
365
- const root = wrapper.find('[data-testid="root"]');
366
- expect(root.exists()).toBe(true);
367
-
368
- const appointmentCard = wrapper.find('.appointment-card');
369
- expect(appointmentCard.exists()).toBe(true);
370
-
371
- // Should have proper sectioning
372
- expect(wrapper.find('.card-header').exists()).toBe(true);
373
- expect(wrapper.find('.card-body').exists()).toBe(true);
374
- expect(wrapper.find('.card-footer').exists()).toBe(true);
375
- });
376
-
377
- it('should use proper heading hierarchy', () => {
378
- const heading = wrapper.find('h3');
379
- expect(heading.exists()).toBe(true);
380
- expect(heading.text()).toContain('Test Treatment');
381
- });
382
-
383
- it('should have accessible link attributes', async () => {
384
- await wrapper.setProps({ appointmentLink: 'https://example.com/appointment/123' });
385
-
386
- const link = wrapper.find('.appointment-card');
387
- expect(link.attributes('href')).toBe('https://example.com/appointment/123');
388
- expect(link.attributes('rel')).toBe('noopener noreferrer');
389
- });
390
-
391
- it('should provide meaningful content for screen readers', () => {
392
- // Patient name should be visible
393
- expect(wrapper.text()).toContain('John Doe');
394
-
395
- // Date and time should be accessible
396
- const appointmentDate = new Date('2024-01-15T10:30:00.000Z');
397
- const expectedDate = format(appointmentDate, 'EEE, d MMM yyyy', { locale: de });
398
-
399
- expect(wrapper.text()).toContain(expectedDate);
400
- // Time will be displayed based on local timezone, check for any valid time format
401
- expect(wrapper.text()).toMatch(/\d{1,2}:\d{2}/);
402
-
403
- // Treatment name should be in heading
404
- const h3 = wrapper.find('h3');
405
- expect(h3.text()).toContain('Test Treatment');
406
- });
407
-
408
- it('should have proper icon accessibility attributes', () => {
409
- const icons = wrapper.findAll('.header-icon');
410
- expect(icons.length).toBeGreaterThanOrEqual(3);
411
-
412
- icons.forEach(icon => {
413
- expect(icon.attributes('aria-hidden')).toBe('true');
414
- });
415
- });
416
-
417
- it('should provide semantic roles for sub-components', () => {
418
- const detailsStub = wrapper.find('.details-stub');
419
- expect(detailsStub.attributes('role')).toBe('region');
420
- expect(detailsStub.attributes('aria-label')).toBe('Appointment details');
421
-
422
- const actionsStub = wrapper.find('.actions-stub');
423
- expect(actionsStub.attributes('role')).toBe('group');
424
- expect(actionsStub.attributes('aria-label')).toBe('Appointment actions');
425
- });
426
-
427
- it('should handle different appointment statuses accessibly', async () => {
428
- // Test default status
429
- let header = wrapper.find('.card-header');
430
- expect(header.classes()).toContain('card-header--default');
431
-
432
- // Test cancelled status
433
- const cancelledAppointment = createMockAppointment({ status: 'cancelled' });
434
- await wrapper.setProps({ appointment: cancelledAppointment });
435
-
436
- header = wrapper.find('.card-header');
437
- expect(header.classes()).toContain('card-header--cancelled');
438
-
439
- // Test done status with opacity
440
- const doneAppointment = createMockAppointment({ status: 'done' });
441
- await wrapper.setProps({ appointment: doneAppointment });
442
-
443
- expect(wrapper.find('.card-opacity').exists()).toBe(true);
444
- });
445
-
446
- it('should provide informative default values', async () => {
447
- const appointmentWithoutPatient = createMockAppointment({ patientName: undefined });
448
- await wrapper.setProps({ appointment: appointmentWithoutPatient });
449
-
450
- expect(wrapper.text()).toContain('Unbekannter Patient');
451
- });
452
-
453
- it('should have logical reading order', () => {
454
- // Header items should be in logical order
455
- const headerItems = wrapper.findAll('.header-item');
456
- expect(headerItems).toHaveLength(3);
457
-
458
- // Patient, Date, Time order
459
- expect(headerItems[0].text()).toContain('John Doe');
460
- // Time will be displayed based on local timezone, check for any valid time format
461
- expect(headerItems[2].text()).toMatch(/\d{1,2}:\d{2}/);
462
- });
463
-
464
- it('should handle multiple template names accessibly', async () => {
465
- const appointmentWithMultipleTemplates = createMockAppointment({
466
- template_name: ['Treatment A', 'Treatment B', 'Treatment C']
467
- });
468
- await wrapper.setProps({ appointment: appointmentWithMultipleTemplates });
469
-
470
- const heading = wrapper.find('h3');
471
- expect(heading.text()).toContain('Treatment A, Treatment B, Treatment C');
472
- });
473
-
474
- it('should use proper HTML5 semantic elements', () => {
475
- // Should have proper sectioning
476
- const header = wrapper.find('.card-header');
477
- const body = wrapper.find('.card-body');
478
- const footer = wrapper.find('.card-footer');
479
-
480
- expect(header.exists()).toBe(true);
481
- expect(body.exists()).toBe(true);
482
- expect(footer.exists()).toBe(true);
483
-
484
- // Should have horizontal rule for separation
485
- expect(wrapper.find('hr').exists()).toBe(true);
486
- });
487
-
488
- it('should maintain focus management for interactive elements', async () => {
489
- await wrapper.setProps({ appointmentLink: 'https://example.com/appointment/123' });
490
-
491
- const link = wrapper.find('.appointment-card');
492
- expect(link.attributes('href')).toBeTruthy();
493
-
494
- // Link should be focusable
495
- expect(link.element.tagName.toLowerCase()).toBe('a');
496
- });
497
-
498
- it('should provide context for visual styling changes', () => {
499
- // Visual changes should have semantic meaning
500
- const header = wrapper.find('.card-header');
501
- expect(header.classes()).toContain('card-header--default');
502
-
503
- // Check that there are no accessibility violations in basic structure
504
- expect(wrapper.find('[data-testid="root"]').exists()).toBe(true);
505
- });
506
-
507
- it('should handle date formatting for screen readers', async () => {
508
- const appointmentWithDifferentDate = createMockAppointment({
509
- start: '2024-12-25T15:45:00Z'
510
- });
511
- await wrapper.setProps({ appointment: appointmentWithDifferentDate });
512
-
513
- const expectedDate = format(new Date('2024-12-25T15:45:00Z'), 'EEE, d MMM yyyy', { locale: de });
514
- const expectedTime = format(new Date('2024-12-25T15:45:00Z'), 'HH:mm');
515
-
516
- expect(wrapper.text()).toContain(expectedDate);
517
- expect(wrapper.text()).toContain(expectedTime);
518
- });
519
-
520
- it('should provide comprehensive information structure', () => {
521
- // Card should contain all necessary information
522
- expect(wrapper.text()).toContain('John Doe'); // Patient
523
- expect(wrapper.text()).toContain('Test Treatment'); // Treatment
524
- // Time will be displayed based on local timezone, check for any valid time format
525
- expect(wrapper.text()).toMatch(/\d{1,2}:\d{2}/); // Time
526
-
527
- // Should have proper component structure
528
- expect(wrapper.find('.details-stub').exists()).toBe(true);
529
- expect(wrapper.find('.actions-stub').exists()).toBe(true);
530
- });
531
- });
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { mount, VueWrapper } from '@vue/test-utils';
3
+ import { format } from 'date-fns';
4
+ import { de } from 'date-fns/locale';
5
+ import Card from '../../../../../src/components/Appointment/Card/Card.vue';
6
+ import type { AppointmentData, Dentist } from '../../../../../src/types';
7
+
8
+ // Mock vue-i18n
9
+ vi.mock('vue-i18n', () => ({
10
+ useI18n: () => ({
11
+ t: (key: string) => {
12
+ const translations: Record<string, string> = {
13
+ 'wl.appointment_card.unknown_patient': 'Unbekannter Patient'
14
+ };
15
+ return translations[key] || key;
16
+ }
17
+ })
18
+ }));
19
+
20
+ // Mock appointmentTypes
21
+ vi.mock('../../../../../src/utils/index', () => ({
22
+ appointmentTypes: [
23
+ { id: 1, title: 'Regular Checkup', icon: 'checkup' },
24
+ { id: 2, title: 'Treatment', icon: 'treatment' }
25
+ ]
26
+ }));
27
+
28
+ describe('Appointment Card', () => {
29
+ let wrapper: VueWrapper;
30
+
31
+ const createMockDentist = (overrides: Partial<Dentist> = {}): Dentist => ({
32
+ name: 'Dr. Test Dentist',
33
+ gender: 'Male',
34
+ imageSrc: 'https://example.com/dentist.jpg',
35
+ ...overrides
36
+ });
37
+
38
+ const createMockAppointment = (overrides: Partial<AppointmentData> = {}): AppointmentData => ({
39
+ id: '123',
40
+ template_name: 'Test Treatment',
41
+ description: 'Test Description',
42
+ dentist: createMockDentist(),
43
+ start: '2024-01-15T10:30:00.000Z',
44
+ type: 1,
45
+ status: 'upcoming',
46
+ patientName: 'John Doe',
47
+ address: 'Test Address 123',
48
+ district: 'Test District',
49
+ is_confirmed: false,
50
+ ...overrides
51
+ });
52
+
53
+ const createGlobalConfig = () => ({
54
+ stubs: {
55
+ UserIcon: {
56
+ template: '<svg class="header-icon user-icon"></svg>'
57
+ },
58
+ CalendarIcon: {
59
+ template: '<svg class="header-icon calendar-icon"></svg>'
60
+ },
61
+ ClockIcon: {
62
+ template: '<svg class="header-icon clock-icon"></svg>'
63
+ },
64
+ IconsAdvanceAppointments: {
65
+ template: '<div class="advance-appointments-icon"></div>',
66
+ props: ['width', 'height', 'iconType']
67
+ },
68
+ Details: {
69
+ template: '<div class="details-stub">Details Component</div>',
70
+ props: ['appointment', 'dentistImageSrc']
71
+ },
72
+ Actions: {
73
+ template: '<div class="actions-stub" @confirmed="$emit(\'confirmed\', $event)" @cancelled="$emit(\'cancelled\', $event)" @rescheduled="$emit(\'rescheduled\', $event)">Actions Component</div>',
74
+ props: ['appointment', 'disabled'],
75
+ emits: ['confirmed', 'cancelled', 'rescheduled']
76
+ },
77
+ AnamneseNotification: {
78
+ template: '<div class="anamnese-notification-stub"></div>'
79
+ }
80
+ }
81
+ });
82
+
83
+ beforeEach(() => {
84
+ const appointment = createMockAppointment();
85
+ wrapper = mount(Card, {
86
+ props: { appointment, showActions: true },
87
+ global: createGlobalConfig()
88
+ });
89
+ });
90
+
91
+ describe('Component Rendering', () => {
92
+ it('should render the component with correct test id', () => {
93
+ expect(wrapper.find('[data-testid="root"]').exists()).toBe(true);
94
+ });
95
+
96
+ it('should render the appointment card structure', () => {
97
+ expect(wrapper.find('.appointment-card').exists()).toBe(true);
98
+ expect(wrapper.find('.card-header').exists()).toBe(true);
99
+ expect(wrapper.find('.card-body').exists()).toBe(true);
100
+ expect(wrapper.find('.card-footer').exists()).toBe(true);
101
+ });
102
+
103
+ it('should render Details and Actions components', () => {
104
+ expect(wrapper.find('.details-stub').exists()).toBe(true);
105
+ expect(wrapper.find('.actions-stub').exists()).toBe(true);
106
+ });
107
+ });
108
+
109
+ describe('Header Information', () => {
110
+ it('should display patient name', () => {
111
+ expect(wrapper.text()).toContain('John Doe');
112
+ });
113
+
114
+ it('should display default patient name when not provided', async () => {
115
+ const appointmentWithoutPatient = createMockAppointment({ patientName: undefined });
116
+ await wrapper.setProps({ appointment: appointmentWithoutPatient });
117
+
118
+ expect(wrapper.text()).toContain('Unbekannter Patient');
119
+ });
120
+
121
+ it('should display formatted date', () => {
122
+ const expectedDate = format(new Date('2024-01-15T10:30:00Z'), 'EEE, d MMM yyyy', { locale: de });
123
+ expect(wrapper.text()).toContain(expectedDate);
124
+ });
125
+
126
+ it('should display formatted time', () => {
127
+ const expectedTime = format(new Date('2024-01-15T10:30:00Z'), 'HH:mm');
128
+ expect(wrapper.text()).toContain(expectedTime);
129
+ });
130
+
131
+ it('should render header icons', () => {
132
+ // Component renders 3 header items with icons
133
+ const headerItems = wrapper.findAll('.header-item');
134
+ expect(headerItems).toHaveLength(3);
135
+
136
+ // Each header item should contain an icon element
137
+ headerItems.forEach(item => {
138
+ expect(item.find('svg').exists()).toBe(true);
139
+ });
140
+ });
141
+
142
+ it('should have correct header items structure', () => {
143
+ const headerItems = wrapper.findAll('.header-item');
144
+ expect(headerItems).toHaveLength(3);
145
+ });
146
+ });
147
+
148
+ describe('Appointment Status Styling', () => {
149
+ it('should apply default header background for upcoming appointments', () => {
150
+ const header = wrapper.find('.card-header');
151
+ expect(header.classes()).toContain('card-header--default');
152
+ expect(header.classes()).not.toContain('card-header--cancelled');
153
+ });
154
+
155
+ it('should apply cancelled header background for cancelled appointments', async () => {
156
+ const cancelledAppointment = createMockAppointment({ status: 'cancelled' });
157
+ await wrapper.setProps({ appointment: cancelledAppointment });
158
+
159
+ const header = wrapper.find('.card-header');
160
+ expect(header.classes()).toContain('card-header--cancelled');
161
+ expect(header.classes()).not.toContain('card-header--default');
162
+ });
163
+
164
+ it('should apply card opacity for done appointments', async () => {
165
+ const doneAppointment = createMockAppointment({ status: 'done' });
166
+ await wrapper.setProps({ appointment: doneAppointment });
167
+
168
+ expect(wrapper.find('.card-opacity').exists()).toBe(true);
169
+ });
170
+
171
+ it('should not apply card opacity for upcoming appointments', () => {
172
+ expect(wrapper.find('.card-opacity').exists()).toBe(false);
173
+ });
174
+ });
175
+
176
+ describe('Template Name Display', () => {
177
+ it('should display single template name', () => {
178
+ expect(wrapper.text()).toContain('Test Treatment');
179
+ });
180
+
181
+ it('should display multiple template names as comma-separated string', async () => {
182
+ const appointmentWithMultipleTemplates = createMockAppointment({
183
+ template_name: ['Treatment A', 'Treatment B', 'Treatment C']
184
+ });
185
+ await wrapper.setProps({ appointment: appointmentWithMultipleTemplates });
186
+
187
+ expect(wrapper.text()).toContain('Treatment A, Treatment B, Treatment C');
188
+ });
189
+
190
+ it('should handle empty template name array', async () => {
191
+ const appointmentWithEmptyTemplates = createMockAppointment({
192
+ template_name: []
193
+ });
194
+ await wrapper.setProps({ appointment: appointmentWithEmptyTemplates });
195
+
196
+ // When template_name is empty array, h3 should show empty or appointment type title only
197
+ expect(wrapper.find('h3').exists()).toBe(true);
198
+ });
199
+ });
200
+
201
+ describe('Link Functionality', () => {
202
+ it('should render appointment link when provided', async () => {
203
+ await wrapper.setProps({ appointmentLink: 'https://example.com/appointment/123' });
204
+
205
+ const link = wrapper.find('.appointment-card');
206
+ expect(link.attributes('href')).toBe('https://example.com/appointment/123');
207
+ expect(link.attributes('rel')).toBe('noopener noreferrer');
208
+ });
209
+
210
+ it('should render default hash link when no appointment link provided', () => {
211
+ const link = wrapper.find('.appointment-card');
212
+ expect(link.attributes('href')).toBe('#');
213
+ });
214
+ });
215
+
216
+ describe('Event Handling', () => {
217
+ it('should emit confirm event when Actions emits confirmed', async () => {
218
+ const actionsComponent = wrapper.find('.actions-stub');
219
+ await actionsComponent.trigger('confirmed', '123');
220
+
221
+ expect(wrapper.emitted('confirm')).toBeTruthy();
222
+ expect(wrapper.emitted('confirm')?.[0]).toEqual(['123']);
223
+ });
224
+
225
+ it('should emit cancel event when Actions emits cancelled', async () => {
226
+ const actionsComponent = wrapper.find('.actions-stub');
227
+ await actionsComponent.trigger('cancelled', '123');
228
+
229
+ expect(wrapper.emitted('cancel')).toBeTruthy();
230
+ expect(wrapper.emitted('cancel')?.[0]).toEqual(['123']);
231
+ });
232
+
233
+ it('should emit reschedule event when Actions emits rescheduled', async () => {
234
+ const actionsComponent = wrapper.find('.actions-stub');
235
+ await actionsComponent.trigger('rescheduled', '123');
236
+
237
+ expect(wrapper.emitted('reschedule')).toBeTruthy();
238
+ expect(wrapper.emitted('reschedule')?.[0]).toEqual(['123']);
239
+ });
240
+ });
241
+
242
+ describe('Props Passing', () => {
243
+ it('should pass appointment prop to Details component', () => {
244
+ const detailsStub = wrapper.find('.details-stub');
245
+ expect(detailsStub.exists()).toBe(true);
246
+ });
247
+
248
+ it('should pass appointment prop to Actions component', () => {
249
+ const actionsStub = wrapper.find('.actions-stub');
250
+ expect(actionsStub.exists()).toBe(true);
251
+ });
252
+
253
+ it('should pass dentistImageSrc prop to Details component', async () => {
254
+ await wrapper.setProps({ dentistImageSrc: 'https://example.com/custom-dentist.jpg' });
255
+ expect(wrapper.props('dentistImageSrc')).toBe('https://example.com/custom-dentist.jpg');
256
+ });
257
+ });
258
+
259
+ describe('Date Formatting Edge Cases', () => {
260
+ it('should handle current date when appointment date is null', async () => {
261
+ const appointmentWithNullDate = createMockAppointment({ start: null as any });
262
+ await wrapper.setProps({ appointment: appointmentWithNullDate });
263
+
264
+ // Should not throw an error and should display current date
265
+ expect(wrapper.find('.card-header').exists()).toBe(true);
266
+ });
267
+
268
+ it('should format different date correctly', async () => {
269
+ const appointmentWithDifferentDate = createMockAppointment({
270
+ start: '2024-12-25T15:45:00Z'
271
+ });
272
+ await wrapper.setProps({ appointment: appointmentWithDifferentDate });
273
+
274
+ const expectedDate = format(new Date('2024-12-25T15:45:00Z'), 'EEE, d MMM yyyy', { locale: de });
275
+ const expectedTime = format(new Date('2024-12-25T15:45:00Z'), 'HH:mm');
276
+
277
+ expect(wrapper.text()).toContain(expectedDate);
278
+ expect(wrapper.text()).toContain(expectedTime);
279
+ });
280
+ });
281
+
282
+ describe('Layout Structure', () => {
283
+ it('should have proper layout structure', () => {
284
+ const root = wrapper.find('[data-testid="root"]');
285
+ expect(root.exists()).toBe(true);
286
+
287
+ const appointmentCard = root.find('.appointment-card');
288
+ expect(appointmentCard.exists()).toBe(true);
289
+
290
+ const cardFooter = root.find('.card-footer');
291
+ expect(cardFooter.exists()).toBe(true);
292
+ });
293
+
294
+ it('should contain horizontal rule separator', () => {
295
+ expect(wrapper.find('hr').exists()).toBe(true);
296
+ });
297
+ });
298
+
299
+ describe('Accessibility', () => {
300
+ it('should have proper link attributes for accessibility', async () => {
301
+ await wrapper.setProps({ appointmentLink: 'https://example.com/appointment/123' });
302
+
303
+ const link = wrapper.find('.appointment-card');
304
+ expect(link.attributes('rel')).toBe('noopener noreferrer');
305
+ });
306
+
307
+ it('should have descriptive data-testid', () => {
308
+ expect(wrapper.find('[data-testid="root"]').exists()).toBe(true);
309
+ });
310
+ });
311
+
312
+ describe('Component Props Validation', () => {
313
+ it('should accept all required props', () => {
314
+ expect(wrapper.props('appointment')).toBeDefined();
315
+ });
316
+
317
+ it('should accept optional props', async () => {
318
+ await wrapper.setProps({
319
+ appointmentLink: 'https://example.com/test',
320
+ dentistImageSrc: 'https://example.com/dentist.jpg'
321
+ });
322
+
323
+ expect(wrapper.props('appointmentLink')).toBe('https://example.com/test');
324
+ expect(wrapper.props('dentistImageSrc')).toBe('https://example.com/dentist.jpg');
325
+ });
326
+ });
327
+
328
+ describe('Accessibility', () => {
329
+ beforeEach(() => {
330
+ const appointment = createMockAppointment();
331
+ wrapper = mount(Card, {
332
+ props: { appointment, showActions: true },
333
+ global: {
334
+ stubs: {
335
+ UserIcon: {
336
+ template: '<svg class="header-icon user-icon" aria-hidden="true"></svg>'
337
+ },
338
+ CalendarIcon: {
339
+ template: '<svg class="header-icon calendar-icon" aria-hidden="true"></svg>'
340
+ },
341
+ ClockIcon: {
342
+ template: '<svg class="header-icon clock-icon" aria-hidden="true"></svg>'
343
+ },
344
+ IconsAdvanceAppointments: {
345
+ template: '<div class="advance-appointments-icon" aria-hidden="true"></div>',
346
+ props: ['width', 'height', 'iconType']
347
+ },
348
+ Details: {
349
+ template: '<div class="details-stub" role="region" aria-label="Appointment details">Details Component</div>',
350
+ props: ['appointment', 'dentistImageSrc']
351
+ },
352
+ Actions: {
353
+ template: '<div class="actions-stub" role="group" aria-label="Appointment actions">Actions Component</div>',
354
+ props: ['appointment', 'disabled']
355
+ },
356
+ AnamneseNotification: {
357
+ template: '<div class="anamnese-notification-stub"></div>'
358
+ }
359
+ }
360
+ }
361
+ });
362
+ });
363
+
364
+ it('should have proper semantic HTML structure', () => {
365
+ const root = wrapper.find('[data-testid="root"]');
366
+ expect(root.exists()).toBe(true);
367
+
368
+ const appointmentCard = wrapper.find('.appointment-card');
369
+ expect(appointmentCard.exists()).toBe(true);
370
+
371
+ // Should have proper sectioning
372
+ expect(wrapper.find('.card-header').exists()).toBe(true);
373
+ expect(wrapper.find('.card-body').exists()).toBe(true);
374
+ expect(wrapper.find('.card-footer').exists()).toBe(true);
375
+ });
376
+
377
+ it('should use proper heading hierarchy', () => {
378
+ const heading = wrapper.find('h3');
379
+ expect(heading.exists()).toBe(true);
380
+ expect(heading.text()).toContain('Test Treatment');
381
+ });
382
+
383
+ it('should have accessible link attributes', async () => {
384
+ await wrapper.setProps({ appointmentLink: 'https://example.com/appointment/123' });
385
+
386
+ const link = wrapper.find('.appointment-card');
387
+ expect(link.attributes('href')).toBe('https://example.com/appointment/123');
388
+ expect(link.attributes('rel')).toBe('noopener noreferrer');
389
+ });
390
+
391
+ it('should provide meaningful content for screen readers', () => {
392
+ // Patient name should be visible
393
+ expect(wrapper.text()).toContain('John Doe');
394
+
395
+ // Date and time should be accessible
396
+ const appointmentDate = new Date('2024-01-15T10:30:00.000Z');
397
+ const expectedDate = format(appointmentDate, 'EEE, d MMM yyyy', { locale: de });
398
+
399
+ expect(wrapper.text()).toContain(expectedDate);
400
+ // Time will be displayed based on local timezone, check for any valid time format
401
+ expect(wrapper.text()).toMatch(/\d{1,2}:\d{2}/);
402
+
403
+ // Treatment name should be in heading
404
+ const h3 = wrapper.find('h3');
405
+ expect(h3.text()).toContain('Test Treatment');
406
+ });
407
+
408
+ it('should have proper icon accessibility attributes', () => {
409
+ const icons = wrapper.findAll('.header-icon');
410
+ expect(icons.length).toBeGreaterThanOrEqual(3);
411
+
412
+ icons.forEach(icon => {
413
+ expect(icon.attributes('aria-hidden')).toBe('true');
414
+ });
415
+ });
416
+
417
+ it('should provide semantic roles for sub-components', () => {
418
+ const detailsStub = wrapper.find('.details-stub');
419
+ expect(detailsStub.attributes('role')).toBe('region');
420
+ expect(detailsStub.attributes('aria-label')).toBe('Appointment details');
421
+
422
+ const actionsStub = wrapper.find('.actions-stub');
423
+ expect(actionsStub.attributes('role')).toBe('group');
424
+ expect(actionsStub.attributes('aria-label')).toBe('Appointment actions');
425
+ });
426
+
427
+ it('should handle different appointment statuses accessibly', async () => {
428
+ // Test default status
429
+ let header = wrapper.find('.card-header');
430
+ expect(header.classes()).toContain('card-header--default');
431
+
432
+ // Test cancelled status
433
+ const cancelledAppointment = createMockAppointment({ status: 'cancelled' });
434
+ await wrapper.setProps({ appointment: cancelledAppointment });
435
+
436
+ header = wrapper.find('.card-header');
437
+ expect(header.classes()).toContain('card-header--cancelled');
438
+
439
+ // Test done status with opacity
440
+ const doneAppointment = createMockAppointment({ status: 'done' });
441
+ await wrapper.setProps({ appointment: doneAppointment });
442
+
443
+ expect(wrapper.find('.card-opacity').exists()).toBe(true);
444
+ });
445
+
446
+ it('should provide informative default values', async () => {
447
+ const appointmentWithoutPatient = createMockAppointment({ patientName: undefined });
448
+ await wrapper.setProps({ appointment: appointmentWithoutPatient });
449
+
450
+ expect(wrapper.text()).toContain('Unbekannter Patient');
451
+ });
452
+
453
+ it('should have logical reading order', () => {
454
+ // Header items should be in logical order
455
+ const headerItems = wrapper.findAll('.header-item');
456
+ expect(headerItems).toHaveLength(3);
457
+
458
+ // Patient, Date, Time order
459
+ expect(headerItems[0].text()).toContain('John Doe');
460
+ // Time will be displayed based on local timezone, check for any valid time format
461
+ expect(headerItems[2].text()).toMatch(/\d{1,2}:\d{2}/);
462
+ });
463
+
464
+ it('should handle multiple template names accessibly', async () => {
465
+ const appointmentWithMultipleTemplates = createMockAppointment({
466
+ template_name: ['Treatment A', 'Treatment B', 'Treatment C']
467
+ });
468
+ await wrapper.setProps({ appointment: appointmentWithMultipleTemplates });
469
+
470
+ const heading = wrapper.find('h3');
471
+ expect(heading.text()).toContain('Treatment A, Treatment B, Treatment C');
472
+ });
473
+
474
+ it('should use proper HTML5 semantic elements', () => {
475
+ // Should have proper sectioning
476
+ const header = wrapper.find('.card-header');
477
+ const body = wrapper.find('.card-body');
478
+ const footer = wrapper.find('.card-footer');
479
+
480
+ expect(header.exists()).toBe(true);
481
+ expect(body.exists()).toBe(true);
482
+ expect(footer.exists()).toBe(true);
483
+
484
+ // Should have horizontal rule for separation
485
+ expect(wrapper.find('hr').exists()).toBe(true);
486
+ });
487
+
488
+ it('should maintain focus management for interactive elements', async () => {
489
+ await wrapper.setProps({ appointmentLink: 'https://example.com/appointment/123' });
490
+
491
+ const link = wrapper.find('.appointment-card');
492
+ expect(link.attributes('href')).toBeTruthy();
493
+
494
+ // Link should be focusable
495
+ expect(link.element.tagName.toLowerCase()).toBe('a');
496
+ });
497
+
498
+ it('should provide context for visual styling changes', () => {
499
+ // Visual changes should have semantic meaning
500
+ const header = wrapper.find('.card-header');
501
+ expect(header.classes()).toContain('card-header--default');
502
+
503
+ // Check that there are no accessibility violations in basic structure
504
+ expect(wrapper.find('[data-testid="root"]').exists()).toBe(true);
505
+ });
506
+
507
+ it('should handle date formatting for screen readers', async () => {
508
+ const appointmentWithDifferentDate = createMockAppointment({
509
+ start: '2024-12-25T15:45:00Z'
510
+ });
511
+ await wrapper.setProps({ appointment: appointmentWithDifferentDate });
512
+
513
+ const expectedDate = format(new Date('2024-12-25T15:45:00Z'), 'EEE, d MMM yyyy', { locale: de });
514
+ const expectedTime = format(new Date('2024-12-25T15:45:00Z'), 'HH:mm');
515
+
516
+ expect(wrapper.text()).toContain(expectedDate);
517
+ expect(wrapper.text()).toContain(expectedTime);
518
+ });
519
+
520
+ it('should provide comprehensive information structure', () => {
521
+ // Card should contain all necessary information
522
+ expect(wrapper.text()).toContain('John Doe'); // Patient
523
+ expect(wrapper.text()).toContain('Test Treatment'); // Treatment
524
+ // Time will be displayed based on local timezone, check for any valid time format
525
+ expect(wrapper.text()).toMatch(/\d{1,2}:\d{2}/); // Time
526
+
527
+ // Should have proper component structure
528
+ expect(wrapper.find('.details-stub').exists()).toBe(true);
529
+ expect(wrapper.find('.actions-stub').exists()).toBe(true);
530
+ });
531
+ });
532
532
  });