@cnamts/synapse 0.0.9-alpha → 0.0.10-alpha

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 (55) hide show
  1. package/dist/design-system-v3.d.ts +631 -62
  2. package/dist/design-system-v3.js +3451 -2650
  3. package/dist/design-system-v3.umd.cjs +1 -1
  4. package/dist/style.css +1 -1
  5. package/package.json +1 -1
  6. package/src/components/DatePicker/Accessibilite.mdx +14 -0
  7. package/src/components/DatePicker/Accessibilite.stories.ts +191 -0
  8. package/src/components/DatePicker/AccessibiliteItems.ts +233 -0
  9. package/src/components/DatePicker/DatePicker.mdx +1 -6
  10. package/src/components/DatePicker/DatePicker.stories.ts +16 -16
  11. package/src/components/DatePicker/DatePicker.vue +20 -6
  12. package/src/components/DatePicker/constants/ExpertiseLevelEnum.ts +4 -0
  13. package/src/components/FileList/FileList.mdx +103 -0
  14. package/src/components/FileList/FileList.stories.ts +562 -0
  15. package/src/components/FileList/FileList.vue +78 -0
  16. package/src/components/FileList/UploadItem/UploadItem.vue +270 -0
  17. package/src/components/FileList/UploadItem/locales.ts +9 -0
  18. package/src/components/FileList/tests/FileList.spec.ts +176 -0
  19. package/src/components/FilePreview/FilePreview.mdx +82 -0
  20. package/src/components/FilePreview/FilePreview.stories.ts +242 -0
  21. package/src/components/FilePreview/FilePreview.vue +68 -0
  22. package/src/components/FilePreview/config.ts +10 -0
  23. package/src/components/FilePreview/locales.ts +4 -0
  24. package/src/components/FilePreview/tests/FilePreview.spec.ts +124 -0
  25. package/src/components/FilePreview/tests/__snapshots__/FilePreview.spec.ts.snap +11 -0
  26. package/src/components/PeriodField/PeriodField.mdx +32 -0
  27. package/src/components/PeriodField/PeriodField.stories.ts +807 -0
  28. package/src/components/PeriodField/PeriodField.vue +355 -0
  29. package/src/components/PeriodField/tests/PeriodField.spec.ts +348 -0
  30. package/src/components/RangeField/Accessibilite.mdx +14 -0
  31. package/src/components/RangeField/Accessibilite.stories.ts +191 -0
  32. package/src/components/RangeField/AccessibiliteItems.ts +179 -0
  33. package/src/components/RangeField/constants/ExpertiseLevelEnum.ts +4 -0
  34. package/src/components/RatingPicker/Accessibilite.mdx +14 -0
  35. package/src/components/RatingPicker/Accessibilite.stories.ts +191 -0
  36. package/src/components/RatingPicker/AccessibiliteItems.ts +208 -0
  37. package/src/components/RatingPicker/constants/ExpertiseLevelEnum.ts +4 -0
  38. package/src/components/SearchListField/Accessibilite.mdx +14 -0
  39. package/src/components/SearchListField/Accessibilite.stories.ts +191 -0
  40. package/src/components/SearchListField/AccessibiliteItems.ts +310 -0
  41. package/src/components/SearchListField/constants/ExpertiseLevelEnum.ts +4 -0
  42. package/src/components/SelectBtnField/Accessibilite.mdx +14 -0
  43. package/src/components/SelectBtnField/Accessibilite.stories.ts +191 -0
  44. package/src/components/SelectBtnField/AccessibiliteItems.ts +191 -0
  45. package/src/components/SelectBtnField/constants/ExpertiseLevelEnum.ts +4 -0
  46. package/src/components/SyAlert/SyAlert.vue +11 -9
  47. package/src/components/TableToolbar/TableToolbar.mdx +130 -0
  48. package/src/components/TableToolbar/TableToolbar.stories.ts +935 -0
  49. package/src/components/TableToolbar/TableToolbar.vue +168 -0
  50. package/src/components/TableToolbar/config.ts +24 -0
  51. package/src/components/TableToolbar/locales.ts +6 -0
  52. package/src/components/TableToolbar/tests/TableToolbar.spec.ts +166 -0
  53. package/src/components/TableToolbar/tests/__snapshots__/TableToolbar.spec.ts.snap +359 -0
  54. package/src/components/index.ts +3 -0
  55. package/src/composables/rules/useFieldValidation.ts +17 -15
@@ -0,0 +1,355 @@
1
+ <script lang="ts" setup>
2
+ import { ref, watch, computed } from 'vue'
3
+ import DatePicker from '@/components/DatePicker/DatePicker.vue'
4
+ import { type RuleOptions } from '@/composables'
5
+
6
+ type DateInput = string | null
7
+ type PeriodValue = { from: DateInput, to: DateInput }
8
+
9
+ const props = withDefaults(defineProps<{
10
+ modelValue?: PeriodValue
11
+ placeholderFrom?: string
12
+ placeholderTo?: string
13
+ format?: string
14
+ dateFormatReturn?: string
15
+ showWeekNumber?: boolean
16
+ required?: boolean
17
+ displayIcon?: boolean
18
+ displayAppendIcon?: boolean
19
+ isDisabled?: boolean
20
+ noIcon?: boolean
21
+ noCalendar?: boolean
22
+ isOutlined?: boolean
23
+ showSuccessMessages?: boolean
24
+ customRules?: { type: string, options: RuleOptions }[]
25
+ customWarningRules?: { type: string, options: RuleOptions }[]
26
+ }>(), {
27
+ modelValue: () => ({ from: null, to: null }),
28
+ placeholderFrom: 'Début',
29
+ placeholderTo: 'Fin',
30
+ format: 'DD/MM/YYYY',
31
+ dateFormatReturn: '',
32
+ showWeekNumber: false,
33
+ required: false,
34
+ displayIcon: true,
35
+ displayAppendIcon: false,
36
+ isDisabled: false,
37
+ noIcon: false,
38
+ noCalendar: false,
39
+ isOutlined: true,
40
+ showSuccessMessages: false,
41
+ customRules: () => [],
42
+ customWarningRules: () => [],
43
+ })
44
+
45
+ const emit = defineEmits(['update:modelValue'])
46
+
47
+ const internalFromDate = ref<string | null>(null)
48
+ const internalToDate = ref<string | null>(null)
49
+
50
+ // Règles de validation pour la date de début
51
+ const fromDateRules = [
52
+ {
53
+ type: 'custom',
54
+ options: {
55
+ validate: (value: Date | null) => {
56
+ if (value === null) return true
57
+ if (tempToDate.value === undefined) return true
58
+ return value <= tempToDate.value
59
+ },
60
+ message: 'La date de début ne peut pas être supérieure à la date de fin.',
61
+ successMessage: 'La date de début est valide.',
62
+ fieldIdentifier: 'fromDateRef',
63
+ },
64
+ },
65
+ ...(props.required
66
+ ? [{
67
+ type: 'required',
68
+ options: {
69
+ validate: (value: Date | null) => {
70
+ // Si les deux champs sont vides, on affiche l'erreur sur les deux
71
+ if (!value && !tempToDate.value) {
72
+ return false
73
+ }
74
+ // Si l'autre champ est rempli, on force la validation de celui-ci
75
+ if (!value && tempToDate.value) {
76
+ return false
77
+ }
78
+ return true
79
+ },
80
+ message: 'La date de début est requise.',
81
+ successMessage: 'La date de début est renseignée.',
82
+ fieldIdentifier: 'fromDateRef',
83
+ },
84
+ }]
85
+ : []),
86
+ ...props.customRules,
87
+ ]
88
+
89
+ // Règles de validation pour la date de fin
90
+ const toDateRules = [
91
+ {
92
+ type: 'custom',
93
+ options: {
94
+ validate: (value: Date | null) => {
95
+ if (value === null) return true
96
+ if (tempFromDate.value === undefined) return true
97
+ return value >= tempFromDate.value
98
+ },
99
+ message: 'La date de fin ne peut pas être inférieure à la date de début.',
100
+ successMessage: 'La date de fin est valide.',
101
+ fieldIdentifier: 'toDate',
102
+ },
103
+ },
104
+ ...(props.required
105
+ ? [{
106
+ type: 'required',
107
+ options: {
108
+ validate: (value: Date | null) => {
109
+ // Si les deux champs sont vides, on affiche l'erreur sur les deux
110
+ if (!value && !tempFromDate.value) {
111
+ return false
112
+ }
113
+ // Si l'autre champ est rempli, on force la validation de celui-ci
114
+ if (!value && tempFromDate.value) {
115
+ return false
116
+ }
117
+ return true
118
+ },
119
+ message: 'La date de fin est requise.',
120
+ successMessage: 'La date de fin est renseignée.',
121
+ fieldIdentifier: 'toDate',
122
+ },
123
+ }]
124
+ : []),
125
+ ...props.customRules,
126
+ ]
127
+
128
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a generic type
129
+ function formatDateValue(value: any): string | null {
130
+ if (!value) return null
131
+ if (typeof value === 'string') return value
132
+ if (value.selectedDates) {
133
+ const date = new Date(value.selectedDates)
134
+ const day = date.getDate().toString().padStart(2, '0')
135
+ const month = (date.getMonth() + 1).toString().padStart(2, '0')
136
+ const year = date.getFullYear()
137
+ return `${day}/${month}/${year}`
138
+ }
139
+ return null
140
+ }
141
+
142
+ // Computed properties pour les dates formatées
143
+ const formattedFromDate = computed(() => formatDateValue(internalFromDate.value))
144
+ const formattedToDate = computed(() => formatDateValue(internalToDate.value))
145
+
146
+ // Computed properties pour les dates temporaires
147
+ const tempFromDate = computed(() => formattedFromDate.value ? stringToDate(formattedFromDate.value) : undefined)
148
+ const tempToDate = computed(() => formattedToDate.value ? stringToDate(formattedToDate.value) : undefined)
149
+
150
+ // Sets pour optimiser la recherche des erreurs et succès
151
+ const fromDateErrorsSet = computed(() => new Set(errors.value.filter(error => error.includes('fromDate'))))
152
+ const toDateErrorsSet = computed(() => new Set(errors.value.filter(error => error.includes('toDate'))))
153
+ const fromDateSuccessesSet = computed(() => new Set(successes.value.filter(success => success.includes('fromDate'))))
154
+ const toDateSuccessesSet = computed(() => new Set(successes.value.filter(success => success.includes('toDate'))))
155
+
156
+ const hasFromDateErrors = computed(() => fromDateErrorsSet.value.size > 0)
157
+ const hasToDateErrors = computed(() => toDateErrorsSet.value.size > 0)
158
+ const hasFromDateSuccesses = computed(() => fromDateSuccessesSet.value.size > 0)
159
+ const hasToDateSuccesses = computed(() => toDateSuccessesSet.value.size > 0)
160
+
161
+ const errors = ref<string[]>([])
162
+ const successes = ref<string[]>([])
163
+
164
+ // Computed property pour vérifier si le formulaire est valide
165
+ const isValid = computed(() => {
166
+ // Si aucune date n'est renseignée et que ce n'est pas required, c'est valide
167
+ if (!props.required && !formattedFromDate.value && !formattedToDate.value) {
168
+ return true
169
+ }
170
+
171
+ // Si c'est required, les deux dates doivent être renseignées
172
+ if (props.required && (!formattedFromDate.value || !formattedToDate.value)) {
173
+ return false
174
+ }
175
+
176
+ // Si une seule date est renseignée et que ce n'est pas required
177
+ if (!props.required && (formattedFromDate.value || formattedToDate.value)) {
178
+ // Les deux dates doivent être renseignées ensemble
179
+ if ((formattedFromDate.value && !formattedToDate.value) || (!formattedFromDate.value && formattedToDate.value)) {
180
+ return false
181
+ }
182
+ }
183
+
184
+ // Si les deux dates sont renseignées, vérifier qu'elles sont cohérentes
185
+ if (formattedFromDate.value && formattedToDate.value) {
186
+ const fromDate = stringToDate(formattedFromDate.value)
187
+ const toDate = stringToDate(formattedToDate.value)
188
+ if (!fromDate || !toDate || fromDate > toDate) {
189
+ return false
190
+ }
191
+ }
192
+
193
+ // Vérifier qu'il n'y a pas d'erreurs
194
+ return errors.value.length === 0
195
+ })
196
+
197
+ // Watch pour les changements de la date de début
198
+ watch(formattedFromDate, () => {
199
+ // Si la date de fin existe, on revalide
200
+ if (formattedToDate.value && toDateRef.value) {
201
+ toDateRef.value.validateOnSubmit()
202
+ }
203
+ })
204
+
205
+ // Watch pour les changements de la date de fin
206
+ watch(formattedToDate, () => {
207
+ // Si la date de début existe, on revalide
208
+ if (formattedFromDate.value && fromDateRef.value) {
209
+ fromDateRef.value.validateOnSubmit()
210
+ }
211
+ })
212
+
213
+ // Watch pour les changements internes
214
+ watch([internalFromDate, internalToDate], () => {
215
+ emit('update:modelValue', {
216
+ from: formattedFromDate.value,
217
+ to: formattedToDate.value,
218
+ })
219
+ })
220
+
221
+ // Watch pour les changements externes avec immediate pour synchroniser l'état initial
222
+ watch(() => props.modelValue, (newValue) => {
223
+ if (!newValue) return
224
+
225
+ const newFromDate = formatDateValue(newValue.from)
226
+ const newToDate = formatDateValue(newValue.to)
227
+
228
+ if (internalFromDate.value !== newFromDate) {
229
+ internalFromDate.value = newFromDate
230
+ }
231
+ if (internalToDate.value !== newToDate) {
232
+ internalToDate.value = newToDate
233
+ }
234
+ }, { deep: true, immediate: true })
235
+
236
+ // Initialisation
237
+ internalFromDate.value = formatDateValue(props.modelValue?.from)
238
+ internalToDate.value = formatDateValue(props.modelValue?.to)
239
+
240
+ const fromDateRef = ref()
241
+ const toDateRef = ref()
242
+
243
+ // Gestionnaires d'événements closed
244
+ const handleFromDateClosed = () => {
245
+ if (fromDateRef.value) {
246
+ fromDateRef.value.validateOnSubmit()
247
+ }
248
+ }
249
+
250
+ const handleToDateClosed = () => {
251
+ if (toDateRef.value) {
252
+ toDateRef.value.validateOnSubmit()
253
+ }
254
+ }
255
+
256
+ const validateOnSubmit = (): boolean => {
257
+ // Valider les deux DatePicker
258
+ const fromDateValid = fromDateRef.value?.validateOnSubmit() ?? true
259
+ const toDateValid = toDateRef.value?.validateOnSubmit() ?? true
260
+
261
+ // Retourner true seulement si tout est valide
262
+ const result = fromDateValid && toDateValid && isValid.value
263
+
264
+ return result
265
+ }
266
+
267
+ function stringToDate(dateString: string | null): Date | undefined {
268
+ if (!dateString) return undefined
269
+
270
+ // Créer un mapping des positions des éléments de date selon le format
271
+ const format = props.format || 'DD/MM/YYYY'
272
+ const separator = format.includes('/') ? '/' : format.includes('-') ? '-' : '.'
273
+ const parts = format.split(separator)
274
+ const dateParts = dateString.split(separator)
275
+
276
+ if (parts.length !== dateParts.length) return undefined
277
+
278
+ let day = '', month = '', year = ''
279
+
280
+ // Extraire les valeurs selon leur position dans le format
281
+ parts.forEach((part, index) => {
282
+ const value = dateParts[index]
283
+ if (part.includes('DD')) day = value
284
+ else if (part.includes('MM')) month = value
285
+ else if (part.includes('YYYY')) year = value
286
+ else if (part.includes('YY')) year = '20' + value // Assumons que nous sommes au 21ème siècle
287
+ })
288
+
289
+ // Vérifier que nous avons toutes les parties nécessaires
290
+ if (!day || !month || !year) return undefined
291
+
292
+ const date = new Date(`${year}-${month}-${day}`)
293
+ return isNaN(date.getTime()) ? undefined : date
294
+ }
295
+
296
+ defineExpose({
297
+ validateOnSubmit,
298
+ errors,
299
+ successes,
300
+ isValid,
301
+ })
302
+ </script>
303
+
304
+ <template>
305
+ <div class="period-field">
306
+ <DatePicker
307
+ ref="fromDateRef"
308
+ v-model="internalFromDate"
309
+ :custom-rules="fromDateRules"
310
+ :custom-warning-rules="props.customWarningRules"
311
+ :date-format-return="props.dateFormatReturn"
312
+ :display-append-icon="props.displayAppendIcon"
313
+ :display-icon="props.displayIcon"
314
+ :error-message="hasFromDateErrors"
315
+ :format="props.format"
316
+ :is-disabled="props.isDisabled"
317
+ :is-outlined="props.isOutlined"
318
+ :no-calendar="props.noCalendar"
319
+ :no-icon="props.noIcon"
320
+ :placeholder="props.placeholderFrom"
321
+ :required="props.required"
322
+ :show-week-number="props.showWeekNumber"
323
+ :success-message="hasFromDateSuccesses"
324
+ class="mr-2"
325
+ @closed="handleFromDateClosed"
326
+ />
327
+ <DatePicker
328
+ ref="toDateRef"
329
+ v-model="internalToDate"
330
+ :custom-rules="toDateRules"
331
+ :custom-warning-rules="props.customWarningRules"
332
+ :date-format-return="props.dateFormatReturn"
333
+ :display-append-icon="props.displayAppendIcon"
334
+ :display-icon="props.displayIcon"
335
+ :error-message="hasToDateErrors"
336
+ :format="props.format"
337
+ :is-disabled="props.isDisabled"
338
+ :is-outlined="props.isOutlined"
339
+ :no-calendar="props.noCalendar"
340
+ :no-icon="props.noIcon"
341
+ :placeholder="props.placeholderTo"
342
+ :required="props.required"
343
+ :show-week-number="props.showWeekNumber"
344
+ :success-message="hasToDateSuccesses"
345
+ @closed="handleToDateClosed"
346
+ />
347
+ </div>
348
+ </template>
349
+
350
+ <style scoped>
351
+ .period-field {
352
+ display: flex;
353
+ gap: 10px;
354
+ }
355
+ </style>
@@ -0,0 +1,348 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { describe, it, expect, beforeEach } from 'vitest'
3
+ import { createVuetify } from 'vuetify'
4
+
5
+ import PeriodField from '../PeriodField.vue'
6
+
7
+ describe('PeriodField.vue', () => {
8
+ let vuetify
9
+
10
+ beforeEach(() => {
11
+ vuetify = createVuetify()
12
+ })
13
+
14
+ describe('Rendering', () => {
15
+ it('displays 2 fields with correct labels', () => {
16
+ const wrapper = mount(PeriodField, {
17
+ global: {
18
+ plugins: [vuetify],
19
+ },
20
+ props: {
21
+ placeholderFrom: 'From',
22
+ placeholderTo: 'To',
23
+ },
24
+ })
25
+
26
+ const inputs = wrapper.findAll('input')
27
+ expect(inputs).toHaveLength(2)
28
+ expect(inputs[0].attributes('aria-label')).toBe('From')
29
+ expect(inputs[1].attributes('aria-label')).toBe('To')
30
+ })
31
+
32
+ it('renders with initial values', async () => {
33
+ const wrapper = mount(PeriodField, {
34
+ global: {
35
+ plugins: [vuetify],
36
+ },
37
+ props: {
38
+ modelValue: {
39
+ from: '14/11/2005',
40
+ to: '23/12/2005',
41
+ },
42
+ },
43
+ })
44
+
45
+ const inputs = wrapper.findAll('input')
46
+ await wrapper.vm.$nextTick()
47
+
48
+ expect(inputs[0].element.value).toBe('14/11/2005')
49
+ expect(inputs[1].element.value).toBe('23/12/2005')
50
+ })
51
+ })
52
+
53
+ describe('Events', () => {
54
+ it('emits update events when fields are changed', async () => {
55
+ const wrapper = mount(PeriodField, {
56
+ global: {
57
+ plugins: [vuetify],
58
+ },
59
+ props: {
60
+ modelValue: {
61
+ from: null,
62
+ to: null,
63
+ },
64
+ },
65
+ })
66
+
67
+ const [startField, endField] = wrapper.findAll('input')
68
+
69
+ // Test start date change
70
+ await startField.trigger('focus')
71
+ await startField.setValue('12/12/1995')
72
+ await startField.trigger('blur')
73
+ await wrapper.vm.$nextTick()
74
+
75
+ const emittedEvents = wrapper.emitted('update:modelValue')
76
+ expect(emittedEvents?.[0]).toEqual([
77
+ {
78
+ from: '12/12/1995',
79
+ to: null,
80
+ },
81
+ ])
82
+
83
+ // Test end date change
84
+ await endField.trigger('focus')
85
+ await endField.setValue('20/12/1995')
86
+ await endField.trigger('blur')
87
+ await wrapper.vm.$nextTick()
88
+
89
+ expect(emittedEvents?.[1]).toEqual([
90
+ {
91
+ from: '12/12/1995',
92
+ to: '20/12/1995',
93
+ },
94
+ ])
95
+ })
96
+ })
97
+
98
+ describe('Validation', () => {
99
+ it('shows error when start date is after end date', async () => {
100
+ const wrapper = mount(PeriodField, {
101
+ global: {
102
+ plugins: [vuetify],
103
+ },
104
+ props: {
105
+ modelValue: {
106
+ from: '12/12/1995',
107
+ to: '20/12/1995',
108
+ },
109
+ },
110
+ })
111
+
112
+ const startField = wrapper.findAll('input')[0]
113
+ await startField.trigger('focus')
114
+ await startField.setValue('22/12/1995')
115
+ await startField.trigger('blur')
116
+ await wrapper.vm.$nextTick()
117
+
118
+ expect(wrapper.text()).toContain('La date de début ne peut pas être supérieure à la date de fin')
119
+ expect(wrapper.vm.isValid).toBe(false)
120
+ })
121
+
122
+ it('shows error when end date is before start date', async () => {
123
+ const wrapper = mount(PeriodField, {
124
+ global: {
125
+ plugins: [vuetify],
126
+ },
127
+ props: {
128
+ modelValue: {
129
+ from: '12/12/1995',
130
+ to: '20/12/1995',
131
+ },
132
+ },
133
+ })
134
+
135
+ const endField = wrapper.findAll('input')[1]
136
+ await endField.trigger('focus')
137
+ await endField.setValue('10/12/1995')
138
+ await endField.trigger('blur')
139
+ await wrapper.vm.$nextTick()
140
+
141
+ expect(wrapper.text()).toContain('La date de fin ne peut pas être inférieure à la date de début')
142
+ expect(wrapper.vm.isValid).toBe(false)
143
+ })
144
+
145
+ it('validates when required and both dates are missing', async () => {
146
+ const wrapper = mount(PeriodField, {
147
+ global: {
148
+ plugins: [vuetify],
149
+ },
150
+ props: {
151
+ required: true,
152
+ modelValue: {
153
+ from: null,
154
+ to: null,
155
+ },
156
+ },
157
+ })
158
+
159
+ // Trigger validation manually
160
+ await wrapper.vm.validateOnSubmit()
161
+ await wrapper.vm.$nextTick()
162
+
163
+ const datePickers = wrapper.findAllComponents({ name: 'DatePicker' })
164
+ expect(wrapper.vm.isValid).toBe(false)
165
+ expect(datePickers[0].props('customRules')).toContainEqual(expect.objectContaining({
166
+ type: 'required',
167
+ options: expect.objectContaining({
168
+ message: 'La date de début est requise.',
169
+ }),
170
+ }))
171
+ expect(datePickers[1].props('customRules')).toContainEqual(expect.objectContaining({
172
+ type: 'required',
173
+ options: expect.objectContaining({
174
+ message: 'La date de fin est requise.',
175
+ }),
176
+ }))
177
+ })
178
+
179
+ it('validates when required and only one date is provided', async () => {
180
+ const wrapper = mount(PeriodField, {
181
+ global: {
182
+ plugins: [vuetify],
183
+ },
184
+ props: {
185
+ required: true,
186
+ modelValue: {
187
+ from: '12/12/1995',
188
+ to: null,
189
+ },
190
+ },
191
+ })
192
+
193
+ // Trigger validation manually
194
+ await wrapper.vm.validateOnSubmit()
195
+ await wrapper.vm.$nextTick()
196
+
197
+ const datePickers = wrapper.findAllComponents({ name: 'DatePicker' })
198
+ expect(wrapper.vm.isValid).toBe(false)
199
+ expect(datePickers[1].props('customRules')).toContainEqual(expect.objectContaining({
200
+ type: 'required',
201
+ options: expect.objectContaining({
202
+ message: 'La date de fin est requise.',
203
+ }),
204
+ }))
205
+ })
206
+
207
+ it('validates when not required and no dates are provided', async () => {
208
+ const wrapper = mount(PeriodField, {
209
+ global: {
210
+ plugins: [vuetify],
211
+ },
212
+ props: {
213
+ required: false,
214
+ modelValue: {
215
+ from: null,
216
+ to: null,
217
+ },
218
+ },
219
+ })
220
+
221
+ // Trigger validation manually
222
+ await wrapper.vm.validateOnSubmit()
223
+ await wrapper.vm.$nextTick()
224
+ expect(wrapper.vm.isValid).toBe(true)
225
+ })
226
+
227
+ it('validates when not required and only one date is provided', async () => {
228
+ const wrapper = mount(PeriodField, {
229
+ global: {
230
+ plugins: [vuetify],
231
+ },
232
+ props: {
233
+ required: false,
234
+ modelValue: {
235
+ from: '12/12/1995',
236
+ to: null,
237
+ },
238
+ },
239
+ })
240
+
241
+ // Trigger validation manually
242
+ await wrapper.vm.validateOnSubmit()
243
+ await wrapper.vm.$nextTick()
244
+ expect(wrapper.vm.isValid).toBe(false)
245
+ })
246
+
247
+ it('validates when both dates are valid', async () => {
248
+ const wrapper = mount(PeriodField, {
249
+ global: {
250
+ plugins: [vuetify],
251
+ },
252
+ props: {
253
+ modelValue: {
254
+ from: '12/12/1995',
255
+ to: '20/12/1995',
256
+ },
257
+ },
258
+ })
259
+
260
+ // Trigger validation manually
261
+ await wrapper.vm.validateOnSubmit()
262
+ await wrapper.vm.$nextTick()
263
+ expect(wrapper.vm.isValid).toBe(true)
264
+ })
265
+ })
266
+
267
+ describe('Utils', () => {
268
+ it('formats date from selectedDates correctly', async () => {
269
+ const wrapper = mount(PeriodField, {
270
+ global: {
271
+ plugins: [vuetify],
272
+ },
273
+ })
274
+
275
+ const input = {
276
+ selectedDates: new Date('2025-02-07T15:42:00.000Z'),
277
+ }
278
+
279
+ // @ts-expect-error: accès à une méthode privée pour le test
280
+ const result = wrapper.vm.formatDateValue(input)
281
+ expect(result).toBe('07/02/2025')
282
+ })
283
+
284
+ it('returns null for invalid inputs', async () => {
285
+ const wrapper = mount(PeriodField, {
286
+ global: {
287
+ plugins: [vuetify],
288
+ },
289
+ })
290
+
291
+ // @ts-expect-error: accès à une méthode privée pour le test
292
+ expect(wrapper.vm.formatDateValue(null)).toBe(null)
293
+ // @ts-expect-error: accès à une méthode privée pour le test
294
+ expect(wrapper.vm.formatDateValue(undefined)).toBe(null)
295
+ // @ts-expect-error: accès à une méthode privée pour le test
296
+ expect(wrapper.vm.formatDateValue({ selectedDates: null })).toBe(null)
297
+ })
298
+
299
+ it('returns string value directly', async () => {
300
+ const wrapper = mount(PeriodField, {
301
+ global: {
302
+ plugins: [vuetify],
303
+ },
304
+ })
305
+
306
+ // @ts-expect-error: accès à une méthode privée pour le test
307
+ expect(wrapper.vm.formatDateValue('07/02/2025')).toBe('07/02/2025')
308
+ })
309
+ })
310
+
311
+ describe('Custom Rules', () => {
312
+ it('applies custom validation rules', async () => {
313
+ const wrapper = mount(PeriodField, {
314
+ global: {
315
+ plugins: [vuetify],
316
+ },
317
+ props: {
318
+ modelValue: {
319
+ from: '12/12/1995',
320
+ to: '20/12/1995',
321
+ },
322
+ customRules: [{
323
+ type: 'custom',
324
+ options: {
325
+ validate: () => false,
326
+ message: 'Custom validation failed',
327
+ fieldIdentifier: 'fromDate',
328
+ },
329
+ }],
330
+ },
331
+ })
332
+
333
+ // Trigger validation manually
334
+ await wrapper.vm.validateOnSubmit()
335
+ await wrapper.vm.$nextTick()
336
+
337
+ const datePickers = wrapper.findAllComponents({ name: 'DatePicker' })
338
+ const fromDatePicker = datePickers[0]
339
+ expect(fromDatePicker.vm.errorMessages).toContainEqual('Custom validation failed')
340
+ expect(datePickers[0].props('customRules')).toContainEqual(expect.objectContaining({
341
+ type: 'custom',
342
+ options: expect.objectContaining({
343
+ message: 'Custom validation failed',
344
+ }),
345
+ }))
346
+ })
347
+ })
348
+ })
@@ -0,0 +1,14 @@
1
+ import { Meta, Story } from '@storybook/addon-docs';
2
+ import * as AccessStories from './Accessibilite.stories.ts';
3
+
4
+ <Meta of={AccessStories} />
5
+
6
+ Accessibilité
7
+ =============
8
+ <Story of={AccessStories.Legende} />
9
+ <br />
10
+
11
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
12
+
13
+ <Story of={AccessStories.AccessibilitePanel} />
14
+ <br />