@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.
- package/dist/design-system-v3.d.ts +631 -62
- package/dist/design-system-v3.js +3451 -2650
- package/dist/design-system-v3.umd.cjs +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/DatePicker/Accessibilite.mdx +14 -0
- package/src/components/DatePicker/Accessibilite.stories.ts +191 -0
- package/src/components/DatePicker/AccessibiliteItems.ts +233 -0
- package/src/components/DatePicker/DatePicker.mdx +1 -6
- package/src/components/DatePicker/DatePicker.stories.ts +16 -16
- package/src/components/DatePicker/DatePicker.vue +20 -6
- package/src/components/DatePicker/constants/ExpertiseLevelEnum.ts +4 -0
- package/src/components/FileList/FileList.mdx +103 -0
- package/src/components/FileList/FileList.stories.ts +562 -0
- package/src/components/FileList/FileList.vue +78 -0
- package/src/components/FileList/UploadItem/UploadItem.vue +270 -0
- package/src/components/FileList/UploadItem/locales.ts +9 -0
- package/src/components/FileList/tests/FileList.spec.ts +176 -0
- package/src/components/FilePreview/FilePreview.mdx +82 -0
- package/src/components/FilePreview/FilePreview.stories.ts +242 -0
- package/src/components/FilePreview/FilePreview.vue +68 -0
- package/src/components/FilePreview/config.ts +10 -0
- package/src/components/FilePreview/locales.ts +4 -0
- package/src/components/FilePreview/tests/FilePreview.spec.ts +124 -0
- package/src/components/FilePreview/tests/__snapshots__/FilePreview.spec.ts.snap +11 -0
- package/src/components/PeriodField/PeriodField.mdx +32 -0
- package/src/components/PeriodField/PeriodField.stories.ts +807 -0
- package/src/components/PeriodField/PeriodField.vue +355 -0
- package/src/components/PeriodField/tests/PeriodField.spec.ts +348 -0
- package/src/components/RangeField/Accessibilite.mdx +14 -0
- package/src/components/RangeField/Accessibilite.stories.ts +191 -0
- package/src/components/RangeField/AccessibiliteItems.ts +179 -0
- package/src/components/RangeField/constants/ExpertiseLevelEnum.ts +4 -0
- package/src/components/RatingPicker/Accessibilite.mdx +14 -0
- package/src/components/RatingPicker/Accessibilite.stories.ts +191 -0
- package/src/components/RatingPicker/AccessibiliteItems.ts +208 -0
- package/src/components/RatingPicker/constants/ExpertiseLevelEnum.ts +4 -0
- package/src/components/SearchListField/Accessibilite.mdx +14 -0
- package/src/components/SearchListField/Accessibilite.stories.ts +191 -0
- package/src/components/SearchListField/AccessibiliteItems.ts +310 -0
- package/src/components/SearchListField/constants/ExpertiseLevelEnum.ts +4 -0
- package/src/components/SelectBtnField/Accessibilite.mdx +14 -0
- package/src/components/SelectBtnField/Accessibilite.stories.ts +191 -0
- package/src/components/SelectBtnField/AccessibiliteItems.ts +191 -0
- package/src/components/SelectBtnField/constants/ExpertiseLevelEnum.ts +4 -0
- package/src/components/SyAlert/SyAlert.vue +11 -9
- package/src/components/TableToolbar/TableToolbar.mdx +130 -0
- package/src/components/TableToolbar/TableToolbar.stories.ts +935 -0
- package/src/components/TableToolbar/TableToolbar.vue +168 -0
- package/src/components/TableToolbar/config.ts +24 -0
- package/src/components/TableToolbar/locales.ts +6 -0
- package/src/components/TableToolbar/tests/TableToolbar.spec.ts +166 -0
- package/src/components/TableToolbar/tests/__snapshots__/TableToolbar.spec.ts.snap +359 -0
- package/src/components/index.ts +3 -0
- 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 />
|