@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.
- package/.github/workflows/playwright.yml +229 -229
- package/.github/workflows/static.yml +61 -61
- package/.github/workflows/update-snapshots.yml +37 -37
- package/.prettierrc.json +8 -8
- 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 +139 -139
- package/README.md +56 -56
- package/docs/assets.md +62 -62
- package/heroicons.ts +75 -75
- package/index.html +19 -19
- package/package.json +71 -71
- 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/scripts/check-translations.ts +352 -352
- package/src/assets/css/base.css +242 -242
- package/src/assets/css/variables.css +176 -176
- package/src/components/Accordion/Accordion.css +65 -65
- package/src/components/Accordion/AccordionGroup.vue +88 -88
- package/src/components/Accordion/AccordionItem.vue +272 -272
- package/src/components/Accordion/presets/default.css +4 -4
- package/src/components/Accordion/presets/elevated.css +25 -25
- package/src/components/Accordion/presets/filled.css +26 -26
- package/src/components/Accordion/presets/index.css +5 -5
- package/src/components/Accordion/presets/plain.css +34 -34
- 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 +20 -20
- package/src/components/Appointment/Card/AnamneseNotification.vue +23 -23
- package/src/components/Appointment/Card/Card.css +99 -99
- package/src/components/Appointment/Card/Card.vue +97 -97
- package/src/components/Appointment/Card/Details.css +62 -62
- package/src/components/Appointment/Card/Details.vue +44 -44
- package/src/components/Audio/Audio.vue +187 -187
- package/src/components/Audio/Waveform.vue +118 -118
- package/src/components/Banner/Banner.css +29 -29
- package/src/components/Banner/Banner.vue +89 -89
- package/src/components/Button/Button.vue +257 -257
- package/src/components/CheckBox/CheckBox.css +234 -234
- package/src/components/CheckBox/Checkbox.vue +184 -184
- package/src/components/DateInput/DateInput.css +2 -2
- package/src/components/DateInput/DateInput.vue +376 -370
- package/src/components/Dialog/Dialog.css +6 -6
- package/src/components/Dialog/Dialog.vue +46 -46
- package/src/components/EditField/EditField.css +19 -19
- package/src/components/EditField/EditField.vue +211 -211
- package/src/components/ErrorPage/ErrorPage.css +172 -172
- package/src/components/IconBullet/IconBullet.vue +104 -104
- package/src/components/IconBullet/IconBulletList.vue +55 -55
- package/src/components/Icons/AdvanceAppointments.vue +161 -161
- package/src/components/Icons/Audio/CloudFailed.vue +27 -27
- package/src/components/Icons/Audio/CloudSaved.vue +28 -28
- package/src/components/Icons/Audio/Delete.vue +22 -22
- package/src/components/Icons/Audio/Pause.vue +25 -25
- package/src/components/Icons/Audio/Play.vue +22 -22
- package/src/components/Icons/Calendar.vue +28 -28
- package/src/components/Icons/CalendarNotification.vue +137 -137
- package/src/components/Icons/Chair.vue +43 -43
- package/src/components/Icons/ChairNotification.vue +46 -46
- package/src/components/Icons/Circle.vue +66 -66
- package/src/components/Icons/FavIcon.vue +69 -69
- package/src/components/Icons/FilledCircle.vue +11 -11
- package/src/components/Icons/Group3.vue +57 -57
- package/src/components/Icons/Play.vue +16 -16
- package/src/components/Icons/RingNotification.vue +65 -65
- package/src/components/Icons/SolidArrowRight.vue +14 -14
- package/src/components/Icons/checkbox.vue +19 -19
- package/src/components/Icons/outlineChecked.vue +38 -38
- package/src/components/Input/Input.css +234 -234
- package/src/components/Input/Input.vue +281 -281
- 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 +78 -78
- package/src/components/MaintenanceBanner/MaintenanceBanner.css +353 -353
- package/src/components/MaintenanceBanner/MaintenanceBanner.vue +140 -140
- package/src/components/MaintenanceBanner/MaintenanceIllustration.vue +54 -54
- package/src/components/Modal/Modal.css +5 -5
- package/src/components/Modal/Modal.vue +29 -29
- package/src/components/NotificationBubble/NotificationBubble.css +4 -4
- package/src/components/NotificationBubble/NotificationBubble.vue +90 -90
- package/src/components/OtpInput/OtpInput.css +43 -43
- package/src/components/OtpInput/OtpInput.vue +181 -181
- package/src/components/PhoneInput/PhoneInput.css +151 -126
- package/src/components/PhoneInput/PhoneInput.vue +230 -139
- package/src/components/RadioGroup/RadioGroup.css +65 -0
- package/src/components/RadioGroup/RadioGroup.vue +134 -0
- package/src/components/Select/Select.css +172 -172
- package/src/components/Select/Select.vue +377 -377
- package/src/components/SelectAutocomplete/SelectAutocomplete.css +172 -172
- package/src/components/SelectAutocomplete/SelectAutocomplete.vue +414 -414
- package/src/components/TextArea/TextArea.css +269 -269
- package/src/components/TextArea/TextArea.vue +207 -207
- package/src/components/TickBox/TickBox.css +116 -116
- package/src/components/TickBox/TickBox.vue +172 -172
- package/src/components/Tile/Tile.css +106 -106
- package/src/components/Tile/Tile.vue +173 -173
- package/src/components/accessibility.css +218 -218
- package/src/components/index.ts +110 -109
- 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 +43 -43
- package/src/main.ts +11 -11
- package/src/pages/AccordionGroupPage.vue +873 -873
- package/src/pages/AllPage.vue +2483 -2365
- package/src/pages/SelectPage.vue +1302 -1302
- package/src/pages/TilePage.vue +902 -902
- package/src/plugins/vuetify.ts +54 -54
- package/src/shims-vue.d.ts +30 -30
- package/src/utils/index.ts +733 -733
- package/src/vite-env.d.ts +1 -1
- package/tests/unit/accessibility/component-a11y.spec.ts +657 -657
- package/tests/unit/components/Accordion/AccordionGroup.spec.ts +228 -228
- package/tests/unit/components/Accordion/AccordionItem.spec.ts +257 -257
- package/tests/unit/components/Appointment/AnamneseNotification.spec.ts +176 -176
- package/tests/unit/components/Appointment/Card/Actions.spec.ts +436 -436
- package/tests/unit/components/Appointment/Card/Card.spec.ts +531 -531
- package/tests/unit/components/Appointment/Card/Details.spec.ts +395 -395
- package/tests/unit/components/Audio/Audio.spec.ts +403 -403
- package/tests/unit/components/Audio/Waveform.spec.ts +483 -483
- package/tests/unit/components/Background/Background.spec.ts +177 -177
- package/tests/unit/components/Core/AnamneseAnswerDialog.spec.ts +344 -0
- package/tests/unit/components/Core/Banner.spec.ts +187 -0
- package/tests/unit/components/Core/Button.spec.ts +346 -346
- package/tests/unit/components/Core/Checkbox.spec.ts +544 -544
- package/tests/unit/components/Core/DateInput.spec.ts +702 -702
- package/tests/unit/components/Core/Dialog.spec.ts +448 -448
- package/tests/unit/components/Core/EditField.spec.ts +541 -541
- package/tests/unit/components/Core/Input.spec.ts +512 -512
- package/tests/unit/components/Core/List.spec.ts +163 -0
- package/tests/unit/components/Core/ListItem.spec.ts +205 -0
- 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 +757 -619
- package/tests/unit/components/Core/RadioGroup.spec.ts +318 -0
- package/tests/unit/components/Core/Select.spec.ts +712 -712
- package/tests/unit/components/Core/SelectAutocomplete.spec.ts +361 -0
- package/tests/unit/components/Core/TextArea.spec.ts +565 -565
- package/tests/unit/components/Core/TickBox.spec.ts +836 -836
- package/tests/unit/components/Core/Tile.spec.ts +286 -0
- package/tests/unit/components/DateInput/DateInput.spec.ts +128 -0
- package/tests/unit/components/ErrorPage/ErrorPage.spec.ts +313 -313
- package/tests/unit/components/ErrorPage/ErrorPageLogo.spec.ts +153 -153
- 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/AdvanceAppointments.spec.ts +186 -186
- 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 +193 -193
- package/tests/unit/components/Icons/Chair.spec.ts +241 -241
- package/tests/unit/components/Icons/ChairNotification.spec.ts +318 -318
- package/tests/unit/components/Icons/Circle.spec.ts +255 -255
- package/tests/unit/components/Icons/FavIcon.spec.ts +259 -259
- package/tests/unit/components/Icons/FilledCircle.spec.ts +274 -274
- package/tests/unit/components/Icons/Group3.spec.ts +362 -362
- package/tests/unit/components/Icons/Logo.spec.ts +229 -229
- package/tests/unit/components/Icons/MiniLogo.spec.ts +38 -38
- package/tests/unit/components/Icons/RingNotification.spec.ts +400 -400
- package/tests/unit/components/Icons/SolidArrowRight.spec.ts +49 -49
- package/tests/unit/components/Icons/calendar.spec.ts +293 -293
- package/tests/unit/components/Icons/checkbox.spec.ts +315 -315
- package/tests/unit/components/Icons/outlineChecked.spec.ts +441 -441
- package/tests/unit/components/Icons/play.spec.ts +315 -315
- 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/components/Loader/Loader.spec.ts +197 -197
- package/tests/unit/components/MaintenanceBanner/MaintenanceBanner.spec.ts +302 -302
- 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 +182 -182
- package/tests/unit/setup.ts +237 -237
- 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 +111 -111
- package/tests/unit/utils/accessibility.spec.ts +318 -318
- package/tests/unit/utils/anamnese.spec.ts +531 -0
- package/tsconfig.json +26 -26
- package/vite.config.ts +29 -29
- package/vitest.config.ts +91 -91
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
ANAMNESE_FORM_NAME,
|
|
4
|
+
flattenElements,
|
|
5
|
+
pickLang,
|
|
6
|
+
normalizeScalar,
|
|
7
|
+
buildAllChoices,
|
|
8
|
+
pageAnswerGroupsFor,
|
|
9
|
+
type FormElement,
|
|
10
|
+
type FormPage,
|
|
11
|
+
} from '@/utils/anamnese';
|
|
12
|
+
|
|
13
|
+
describe('anamnese utilities', () => {
|
|
14
|
+
// ─── ANAMNESE_FORM_NAME ───────────────────────────────────────────
|
|
15
|
+
describe('ANAMNESE_FORM_NAME', () => {
|
|
16
|
+
it('exports the correct constant', () => {
|
|
17
|
+
expect(ANAMNESE_FORM_NAME).toBe('Anamnese');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// ─── flattenElements ──────────────────────────────────────────────
|
|
22
|
+
describe('flattenElements', () => {
|
|
23
|
+
it('returns empty array for no input', () => {
|
|
24
|
+
expect(flattenElements()).toEqual([]);
|
|
25
|
+
expect(flattenElements([])).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns non-panel elements unchanged', () => {
|
|
29
|
+
const els: FormElement[] = [
|
|
30
|
+
{ type: 'text', name: 'q1' },
|
|
31
|
+
{ type: 'checkbox', name: 'q2' },
|
|
32
|
+
];
|
|
33
|
+
expect(flattenElements(els)).toEqual(els);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('flattens panel children one level deep', () => {
|
|
37
|
+
const els: FormElement[] = [
|
|
38
|
+
{
|
|
39
|
+
type: 'panel',
|
|
40
|
+
elements: [
|
|
41
|
+
{ type: 'text', name: 'inner1' },
|
|
42
|
+
{ type: 'text', name: 'inner2' },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{ type: 'text', name: 'outer' },
|
|
46
|
+
];
|
|
47
|
+
const flat = flattenElements(els);
|
|
48
|
+
expect(flat).toHaveLength(3);
|
|
49
|
+
expect(flat.map(e => e.name)).toEqual(['inner1', 'inner2', 'outer']);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('flattens nested panels recursively', () => {
|
|
53
|
+
const els: FormElement[] = [
|
|
54
|
+
{
|
|
55
|
+
type: 'panel',
|
|
56
|
+
elements: [
|
|
57
|
+
{
|
|
58
|
+
type: 'panel',
|
|
59
|
+
elements: [{ type: 'text', name: 'deep' }],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
const flat = flattenElements(els);
|
|
65
|
+
expect(flat).toHaveLength(1);
|
|
66
|
+
expect(flat[0].name).toBe('deep');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('keeps panel without elements as-is if elements missing', () => {
|
|
70
|
+
const els: FormElement[] = [{ type: 'panel' }];
|
|
71
|
+
expect(flattenElements(els)).toEqual([{ type: 'panel' }]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ─── pickLang ─────────────────────────────────────────────────────
|
|
76
|
+
describe('pickLang', () => {
|
|
77
|
+
it('returns fallback for undefined', () => {
|
|
78
|
+
expect(pickLang(undefined)).toBe('');
|
|
79
|
+
expect(pickLang(undefined, 'fb')).toBe('fb');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns string value directly', () => {
|
|
83
|
+
expect(pickLang('hello')).toBe('hello');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns the "default" key from a multilingual object', () => {
|
|
87
|
+
expect(pickLang({ default: 'Hallo', en: 'Hello' })).toBe('Hallo');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('returns first value if no "default" key', () => {
|
|
91
|
+
expect(pickLang({ en: 'Hello', de: 'Hallo' })).toBe('Hello');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns fallback if object values are empty', () => {
|
|
95
|
+
expect(pickLang({} as any, 'fb')).toBe('fb');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ─── normalizeScalar ──────────────────────────────────────────────
|
|
100
|
+
describe('normalizeScalar', () => {
|
|
101
|
+
it('returns null for null, empty string, empty array', () => {
|
|
102
|
+
expect(normalizeScalar(null)).toBeNull();
|
|
103
|
+
expect(normalizeScalar('')).toBeNull();
|
|
104
|
+
expect(normalizeScalar([])).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('converts boolean true to "Ja"', () => {
|
|
108
|
+
expect(normalizeScalar(true)).toBe('Ja');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('converts boolean false to "Nein"', () => {
|
|
112
|
+
expect(normalizeScalar(false)).toBe('Nein');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('detects data:image strings as signature', () => {
|
|
116
|
+
expect(normalizeScalar('data:image/png;base64,abc')).toBe('Unterschrift vorhanden');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('formats ISO date strings to German locale', () => {
|
|
120
|
+
const result = normalizeScalar('2024-03-15T10:30:00Z');
|
|
121
|
+
expect(result).toMatch(/15\.03\.2024/);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('returns plain strings as-is', () => {
|
|
125
|
+
expect(normalizeScalar('hello')).toBe('hello');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('converts numbers to string', () => {
|
|
129
|
+
expect(normalizeScalar(42)).toBe('42');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('handles non-ISO date strings as regular strings', () => {
|
|
133
|
+
expect(normalizeScalar('2024-03-15')).toBe('2024-03-15');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ─── buildAllChoices ──────────────────────────────────────────────
|
|
138
|
+
describe('buildAllChoices', () => {
|
|
139
|
+
it('builds choices from string array', () => {
|
|
140
|
+
const el: FormElement = {
|
|
141
|
+
choices: ['A', 'B', 'C'],
|
|
142
|
+
};
|
|
143
|
+
const result = buildAllChoices(el, ['B']);
|
|
144
|
+
expect(result).toHaveLength(3);
|
|
145
|
+
expect(result[0]).toEqual({ text: 'A', selected: false });
|
|
146
|
+
expect(result[1]).toEqual({ text: 'B', selected: true });
|
|
147
|
+
expect(result[2]).toEqual({ text: 'C', selected: false });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('builds choices from Choice objects with wlColor', () => {
|
|
151
|
+
const el: FormElement = {
|
|
152
|
+
choices: [
|
|
153
|
+
{ value: 'yes', text: 'Yes', wlColor: 'green' },
|
|
154
|
+
{ value: 'no', text: 'No', wlColor: 'red' },
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
const result = buildAllChoices(el, ['yes']);
|
|
158
|
+
expect(result[0]).toEqual({ text: 'Yes', color: 'green', selected: true });
|
|
159
|
+
expect(result[1]).toEqual({ text: 'No', color: 'red', selected: false });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('uses pickLang for multilingual choice text', () => {
|
|
163
|
+
const el: FormElement = {
|
|
164
|
+
choices: [
|
|
165
|
+
{ value: 'v1', text: { default: 'Default Text' } },
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
const result = buildAllChoices(el, []);
|
|
169
|
+
expect(result[0].text).toBe('Default Text');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('falls back to value when text is missing', () => {
|
|
173
|
+
const el: FormElement = {
|
|
174
|
+
choices: [{ value: 'myval' }],
|
|
175
|
+
};
|
|
176
|
+
const result = buildAllChoices(el, []);
|
|
177
|
+
expect(result[0].text).toBe('myval');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('adds "other" item when showOtherItem is true', () => {
|
|
181
|
+
const el: FormElement = {
|
|
182
|
+
choices: ['A'],
|
|
183
|
+
showOtherItem: true,
|
|
184
|
+
otherText: 'Sonstiges',
|
|
185
|
+
};
|
|
186
|
+
const result = buildAllChoices(el, ['other'], 'My custom text');
|
|
187
|
+
expect(result).toHaveLength(2);
|
|
188
|
+
expect(result[1]).toEqual({
|
|
189
|
+
text: 'My custom text',
|
|
190
|
+
selected: true,
|
|
191
|
+
isOther: true,
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('uses otherText default when other not selected', () => {
|
|
196
|
+
const el: FormElement = {
|
|
197
|
+
choices: [],
|
|
198
|
+
showOtherItem: true,
|
|
199
|
+
otherText: 'Sonstiges',
|
|
200
|
+
};
|
|
201
|
+
const result = buildAllChoices(el, []);
|
|
202
|
+
expect(result[0].text).toBe('Sonstiges');
|
|
203
|
+
expect(result[0].selected).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('falls back to "Andere" for other text when not provided', () => {
|
|
207
|
+
const el: FormElement = {
|
|
208
|
+
choices: [],
|
|
209
|
+
showOtherItem: true,
|
|
210
|
+
};
|
|
211
|
+
const result = buildAllChoices(el, []);
|
|
212
|
+
expect(result[0].text).toBe('Andere');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('adds "none" item when showNoneItem is true', () => {
|
|
216
|
+
const el: FormElement = {
|
|
217
|
+
choices: ['A'],
|
|
218
|
+
showNoneItem: true,
|
|
219
|
+
noneText: 'Nichts',
|
|
220
|
+
};
|
|
221
|
+
const result = buildAllChoices(el, ['none']);
|
|
222
|
+
expect(result).toHaveLength(2);
|
|
223
|
+
expect(result[1]).toEqual({
|
|
224
|
+
text: 'Nichts',
|
|
225
|
+
selected: true,
|
|
226
|
+
isNone: true,
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('falls back to "Keine" for none text when not provided', () => {
|
|
231
|
+
const el: FormElement = {
|
|
232
|
+
choices: [],
|
|
233
|
+
showNoneItem: true,
|
|
234
|
+
};
|
|
235
|
+
const result = buildAllChoices(el, []);
|
|
236
|
+
expect(result[0].text).toBe('Keine');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('returns empty when no choices and no special items', () => {
|
|
240
|
+
const result = buildAllChoices({}, []);
|
|
241
|
+
expect(result).toEqual([]);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ─── pageAnswerGroupsFor ──────────────────────────────────────────
|
|
246
|
+
describe('pageAnswerGroupsFor', () => {
|
|
247
|
+
const makeForm = (pages: FormPage[]) => ({
|
|
248
|
+
components: { pages },
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('returns empty array when item is falsy', () => {
|
|
252
|
+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
253
|
+
expect(pageAnswerGroupsFor(null, makeForm([]))).toEqual([]);
|
|
254
|
+
spy.mockRestore();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('returns empty array when form has no pages', () => {
|
|
258
|
+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
259
|
+
expect(pageAnswerGroupsFor({}, { components: {} })).toEqual([]);
|
|
260
|
+
spy.mockRestore();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('handles components as JSON string', () => {
|
|
264
|
+
const form = {
|
|
265
|
+
components: JSON.stringify({
|
|
266
|
+
pages: [
|
|
267
|
+
{
|
|
268
|
+
name: 'p1',
|
|
269
|
+
title: 'Page 1',
|
|
270
|
+
elements: [{ name: 'q1', title: 'Question 1' }],
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
}),
|
|
274
|
+
};
|
|
275
|
+
const item = { q1: 'answer' };
|
|
276
|
+
const result = pageAnswerGroupsFor(item, form);
|
|
277
|
+
expect(result).toHaveLength(1);
|
|
278
|
+
expect(result[0].title).toBe('Page 1');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('builds answer groups from simple text answers', () => {
|
|
282
|
+
const form = makeForm([
|
|
283
|
+
{
|
|
284
|
+
name: 'page1',
|
|
285
|
+
title: 'Seite 1',
|
|
286
|
+
elements: [
|
|
287
|
+
{ name: 'q1', title: 'Frage 1' },
|
|
288
|
+
{ name: 'q2', title: 'Frage 2' },
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
]);
|
|
292
|
+
const item = { q1: 'Antwort 1', q2: 'Antwort 2' };
|
|
293
|
+
const result = pageAnswerGroupsFor(item, form);
|
|
294
|
+
|
|
295
|
+
expect(result).toHaveLength(1);
|
|
296
|
+
expect(result[0].pageKey).toBe('page1');
|
|
297
|
+
expect(result[0].title).toBe('Seite 1');
|
|
298
|
+
expect(result[0].answers).toHaveLength(2);
|
|
299
|
+
expect(result[0].answers[0].value).toBe('Antwort 1');
|
|
300
|
+
expect(result[0].answers[1].value).toBe('Antwort 2');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('skips elements with no name', () => {
|
|
304
|
+
const form = makeForm([
|
|
305
|
+
{
|
|
306
|
+
name: 'p',
|
|
307
|
+
title: 'P',
|
|
308
|
+
elements: [
|
|
309
|
+
{ title: 'no name' },
|
|
310
|
+
{ name: 'q1', title: 'Q1' },
|
|
311
|
+
],
|
|
312
|
+
},
|
|
313
|
+
]);
|
|
314
|
+
const item = { q1: 'yes' };
|
|
315
|
+
const result = pageAnswerGroupsFor(item, form);
|
|
316
|
+
expect(result[0].answers).toHaveLength(1);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('skips elements with null/undefined/empty values', () => {
|
|
320
|
+
const form = makeForm([
|
|
321
|
+
{
|
|
322
|
+
name: 'p',
|
|
323
|
+
title: 'P',
|
|
324
|
+
elements: [
|
|
325
|
+
{ name: 'q1', title: 'Q1' },
|
|
326
|
+
{ name: 'q2', title: 'Q2' },
|
|
327
|
+
{ name: 'q3', title: 'Q3' },
|
|
328
|
+
],
|
|
329
|
+
},
|
|
330
|
+
]);
|
|
331
|
+
const item = { q1: null, q2: undefined, q3: '' };
|
|
332
|
+
const result = pageAnswerGroupsFor(item, form);
|
|
333
|
+
expect(result).toHaveLength(0); // page skipped because no answers
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('skips pages with no answers', () => {
|
|
337
|
+
const form = makeForm([
|
|
338
|
+
{
|
|
339
|
+
name: 'empty',
|
|
340
|
+
title: 'Empty',
|
|
341
|
+
elements: [{ name: 'q1', title: 'Q1' }],
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
name: 'filled',
|
|
345
|
+
title: 'Filled',
|
|
346
|
+
elements: [{ name: 'q2', title: 'Q2' }],
|
|
347
|
+
},
|
|
348
|
+
]);
|
|
349
|
+
const item = { q2: 'answer' };
|
|
350
|
+
const result = pageAnswerGroupsFor(item, form);
|
|
351
|
+
expect(result).toHaveLength(1);
|
|
352
|
+
expect(result[0].pageKey).toBe('filled');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('handles array values with choices', () => {
|
|
356
|
+
const form = makeForm([
|
|
357
|
+
{
|
|
358
|
+
name: 'p',
|
|
359
|
+
title: 'P',
|
|
360
|
+
elements: [
|
|
361
|
+
{
|
|
362
|
+
name: 'symptoms',
|
|
363
|
+
title: 'Symptome',
|
|
364
|
+
choices: [
|
|
365
|
+
{ value: 'headache', text: 'Kopfschmerzen', wlColor: 'red' },
|
|
366
|
+
{ value: 'fever', text: 'Fieber', wlColor: 'yellow' },
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
},
|
|
371
|
+
]);
|
|
372
|
+
const item = { symptoms: ['headache'] };
|
|
373
|
+
const result = pageAnswerGroupsFor(item, form);
|
|
374
|
+
|
|
375
|
+
const answer = result[0].answers[0];
|
|
376
|
+
expect(answer.value).toBe('Kopfschmerzen');
|
|
377
|
+
expect(answer.valueListAll).toHaveLength(2);
|
|
378
|
+
expect(answer.valueListSelected).toHaveLength(1);
|
|
379
|
+
expect(answer.hasColor).toBe(true);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('handles string values with choices (single select)', () => {
|
|
383
|
+
const form = makeForm([
|
|
384
|
+
{
|
|
385
|
+
name: 'p',
|
|
386
|
+
title: 'P',
|
|
387
|
+
elements: [
|
|
388
|
+
{
|
|
389
|
+
name: 'q1',
|
|
390
|
+
title: 'Q1',
|
|
391
|
+
choices: ['Yes', 'No'],
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
},
|
|
395
|
+
]);
|
|
396
|
+
const item = { q1: 'Yes' };
|
|
397
|
+
const result = pageAnswerGroupsFor(item, form);
|
|
398
|
+
expect(result[0].answers[0].value).toBe('Yes');
|
|
399
|
+
expect(result[0].answers[0].valueListSelected).toHaveLength(1);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('handles boolean values via normalizeScalar', () => {
|
|
403
|
+
const form = makeForm([
|
|
404
|
+
{
|
|
405
|
+
name: 'p',
|
|
406
|
+
title: 'P',
|
|
407
|
+
elements: [{ name: 'q1', title: 'Q1' }],
|
|
408
|
+
},
|
|
409
|
+
]);
|
|
410
|
+
const item = { q1: true };
|
|
411
|
+
const result = pageAnswerGroupsFor(item, form);
|
|
412
|
+
expect(result[0].answers[0].value).toBe('Ja');
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('handles signature pad elements', () => {
|
|
416
|
+
const getSignatureUrl = (id: string) => `https://example.com/sig/${id}`;
|
|
417
|
+
const form = makeForm([
|
|
418
|
+
{
|
|
419
|
+
name: 'p',
|
|
420
|
+
title: 'P',
|
|
421
|
+
elements: [{ name: 'sig', title: 'Unterschrift', type: 'signaturepad' }],
|
|
422
|
+
},
|
|
423
|
+
]);
|
|
424
|
+
const item = { sig: 'sig-123' };
|
|
425
|
+
const result = pageAnswerGroupsFor(item, form, { getSignatureUrl });
|
|
426
|
+
|
|
427
|
+
const answer = result[0].answers[0];
|
|
428
|
+
expect(answer.isSignature).toBe(true);
|
|
429
|
+
expect(answer.signatureId).toBe('sig-123');
|
|
430
|
+
expect(answer.signatureUrl).toBe('https://example.com/sig/sig-123');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('handles signature_pad type alias', () => {
|
|
434
|
+
const form = makeForm([
|
|
435
|
+
{
|
|
436
|
+
name: 'p',
|
|
437
|
+
title: 'P',
|
|
438
|
+
elements: [{ name: 'sig', title: 'Sig', type: 'signature_pad' }],
|
|
439
|
+
},
|
|
440
|
+
]);
|
|
441
|
+
const item = { sig: 'id-456' };
|
|
442
|
+
const result = pageAnswerGroupsFor(item, form);
|
|
443
|
+
expect(result[0].answers[0].isSignature).toBe(true);
|
|
444
|
+
expect(result[0].answers[0].signatureUrl).toBe('');
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('reads from answers_json when present', () => {
|
|
448
|
+
const form = makeForm([
|
|
449
|
+
{
|
|
450
|
+
name: 'p',
|
|
451
|
+
title: 'P',
|
|
452
|
+
elements: [{ name: 'q1', title: 'Q1' }],
|
|
453
|
+
},
|
|
454
|
+
]);
|
|
455
|
+
const item = { answers_json: { q1: 'from json' } };
|
|
456
|
+
const result = pageAnswerGroupsFor(item, form);
|
|
457
|
+
expect(result[0].answers[0].value).toBe('from json');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('flattens panel elements within pages', () => {
|
|
461
|
+
const form = makeForm([
|
|
462
|
+
{
|
|
463
|
+
name: 'p',
|
|
464
|
+
title: 'P',
|
|
465
|
+
elements: [
|
|
466
|
+
{
|
|
467
|
+
type: 'panel',
|
|
468
|
+
elements: [{ name: 'q1', title: 'Q1' }],
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
},
|
|
472
|
+
]);
|
|
473
|
+
const item = { q1: 'answer' };
|
|
474
|
+
const result = pageAnswerGroupsFor(item, form);
|
|
475
|
+
expect(result[0].answers).toHaveLength(1);
|
|
476
|
+
expect(result[0].answers[0].value).toBe('answer');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('generates fallback page title and key', () => {
|
|
480
|
+
const form = makeForm([{ elements: [{ name: 'q1', title: 'Q' }] }]);
|
|
481
|
+
const item = { q1: 'val' };
|
|
482
|
+
const result = pageAnswerGroupsFor(item, form);
|
|
483
|
+
expect(result[0].pageKey).toBe('page_0');
|
|
484
|
+
expect(result[0].title).toBe('Seite 1');
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('includes comment field for "other" choices', () => {
|
|
488
|
+
const form = makeForm([
|
|
489
|
+
{
|
|
490
|
+
name: 'p',
|
|
491
|
+
title: 'P',
|
|
492
|
+
elements: [
|
|
493
|
+
{
|
|
494
|
+
name: 'q1',
|
|
495
|
+
title: 'Q1',
|
|
496
|
+
choices: ['A'],
|
|
497
|
+
showOtherItem: true,
|
|
498
|
+
},
|
|
499
|
+
],
|
|
500
|
+
},
|
|
501
|
+
]);
|
|
502
|
+
const item = { q1: ['other'], 'q1-Comment': 'Custom answer' };
|
|
503
|
+
const result = pageAnswerGroupsFor(item, form);
|
|
504
|
+
const otherChoice = result[0].answers[0].valueListAll?.find(c => c.isOther);
|
|
505
|
+
expect(otherChoice?.text).toBe('Custom answer');
|
|
506
|
+
expect(otherChoice?.selected).toBe(true);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('valueListAllDisplay only includes selected items', () => {
|
|
510
|
+
const form = makeForm([
|
|
511
|
+
{
|
|
512
|
+
name: 'p',
|
|
513
|
+
title: 'P',
|
|
514
|
+
elements: [
|
|
515
|
+
{
|
|
516
|
+
name: 'q1',
|
|
517
|
+
title: 'Q1',
|
|
518
|
+
choices: ['A', 'B', 'C'],
|
|
519
|
+
},
|
|
520
|
+
],
|
|
521
|
+
},
|
|
522
|
+
]);
|
|
523
|
+
const item = { q1: ['A'] };
|
|
524
|
+
const result = pageAnswerGroupsFor(item, form);
|
|
525
|
+
const answer = result[0].answers[0];
|
|
526
|
+
expect(answer.valueListAll).toHaveLength(3);
|
|
527
|
+
expect(answer.valueListAllDisplay).toHaveLength(1);
|
|
528
|
+
expect(answer.valueListAllDisplay![0].text).toBe('A');
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
});
|
package/tsconfig.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
// tsconfig.json
|
|
2
|
-
{
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"target": "esnext",
|
|
5
|
-
"module": "esnext",
|
|
6
|
-
"moduleResolution": "node",
|
|
7
|
-
"lib": ["esnext", "dom"],
|
|
8
|
-
"jsx": "preserve",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"sourceMap": true,
|
|
11
|
-
"resolveJsonModule": true,
|
|
12
|
-
"esModuleInterop": true,
|
|
13
|
-
"allowSyntheticDefaultImports": true,
|
|
14
|
-
"skipLibCheck": true,
|
|
15
|
-
"declaration": true,
|
|
16
|
-
"emitDeclarationOnly": true,
|
|
17
|
-
"outDir": "dist",
|
|
18
|
-
"baseUrl": ".",
|
|
19
|
-
"paths": {
|
|
20
|
-
"@/*": ["src/*"],
|
|
21
|
-
"@components/*": ["src/components/*"]
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
"include": ["src/**/*", "src/**/*.vue"],
|
|
25
|
-
"exclude": ["node_modules", "dist", "**/*.stories.ts"]
|
|
26
|
-
}
|
|
1
|
+
// tsconfig.json
|
|
2
|
+
{
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "esnext",
|
|
5
|
+
"module": "esnext",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"lib": ["esnext", "dom"],
|
|
8
|
+
"jsx": "preserve",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"allowSyntheticDefaultImports": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"emitDeclarationOnly": true,
|
|
17
|
+
"outDir": "dist",
|
|
18
|
+
"baseUrl": ".",
|
|
19
|
+
"paths": {
|
|
20
|
+
"@/*": ["src/*"],
|
|
21
|
+
"@components/*": ["src/components/*"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"include": ["src/**/*", "src/**/*.vue"],
|
|
25
|
+
"exclude": ["node_modules", "dist", "**/*.stories.ts"]
|
|
26
|
+
}
|
package/vite.config.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import { defineConfig } from 'vite';
|
|
2
|
-
import vue from '@vitejs/plugin-vue';
|
|
3
|
-
import { resolve } from 'path';
|
|
4
|
-
|
|
5
|
-
export default defineConfig({
|
|
6
|
-
plugins: [vue()],
|
|
7
|
-
resolve: {
|
|
8
|
-
alias: {
|
|
9
|
-
'@': resolve(__dirname, './src'),
|
|
10
|
-
'@components': resolve(__dirname, './src/components'),
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
build: {
|
|
14
|
-
lib: {
|
|
15
|
-
entry: resolve(__dirname, 'src/index.ts'),
|
|
16
|
-
name: 'wl-shared-components',
|
|
17
|
-
fileName: (format) => `wl-shared-components.${format}.js`,
|
|
18
|
-
formats: ['es', 'umd'],
|
|
19
|
-
},
|
|
20
|
-
rollupOptions: {
|
|
21
|
-
external: ['vue'],
|
|
22
|
-
output: {
|
|
23
|
-
globals: {
|
|
24
|
-
vue: 'Vue',
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
});
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import vue from '@vitejs/plugin-vue';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [vue()],
|
|
7
|
+
resolve: {
|
|
8
|
+
alias: {
|
|
9
|
+
'@': resolve(__dirname, './src'),
|
|
10
|
+
'@components': resolve(__dirname, './src/components'),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
build: {
|
|
14
|
+
lib: {
|
|
15
|
+
entry: resolve(__dirname, 'src/index.ts'),
|
|
16
|
+
name: 'wl-shared-components',
|
|
17
|
+
fileName: (format) => `wl-shared-components.${format}.js`,
|
|
18
|
+
formats: ['es', 'umd'],
|
|
19
|
+
},
|
|
20
|
+
rollupOptions: {
|
|
21
|
+
external: ['vue'],
|
|
22
|
+
output: {
|
|
23
|
+
globals: {
|
|
24
|
+
vue: 'Vue',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|