@cnamts/synapse 1.0.23 → 1.0.24
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/AutocompleteFilter-BWLR3U7W.js +114 -0
- package/dist/AutocompleteFilter-D9jzRzAL.cjs +1 -0
- package/dist/{DateFilter-Dc-gSGwk.js → DateFilter-BpwFexzi.js} +1 -1
- package/dist/DateFilter-DTUl8hb1.cjs +1 -0
- package/dist/{NumberFilter-vP38Wp6j.js → NumberFilter-Bz_NTdX9.js} +3 -3
- package/dist/NumberFilter-MAEojdk0.cjs +1 -0
- package/dist/PeriodFilter-CC4WgIhl.cjs +1 -0
- package/dist/{PeriodFilter-Ba1uYUnT.js → PeriodFilter-DX_wy9g-.js} +1 -1
- package/dist/SelectFilter-BR3fvl-a.cjs +1 -0
- package/dist/SelectFilter-xqiPtPgX.js +135 -0
- package/dist/{TextFilter-B84dpnoq.js → TextFilter-BBl3JFqK.js} +7 -7
- package/dist/TextFilter-CCfYFl5F.cjs +1 -0
- package/dist/apLightTheme-CFSRrjv2.cjs +1 -0
- package/dist/apLightTheme-D1P4jcD0.js +1231 -0
- package/dist/components/Amelipro/AmeliproAutoCompleteField/AmeliproAutoCompleteField.d.ts +7022 -9616
- package/dist/components/Amelipro/AmeliproCarousel/AmeliproCarousel.d.ts +2 -2
- package/dist/components/Amelipro/AmeliproIconBtn/AmeliproIconBtn.d.ts +2 -2
- package/dist/components/Amelipro/AmeliproPostalAddressField/AmeliproPostalAddressCityRow/AmeliproPostalAddressCityRow.d.ts +40 -40
- package/dist/components/Amelipro/AmeliproPostalAddressField/AmeliproPostalAddressField.d.ts +60 -60
- package/dist/components/Amelipro/AmeliproSelect/AmeliproSelect.d.ts +7168 -9762
- package/dist/components/Amelipro/AmeliproStepper/AmeliproStepper.d.ts +2 -2
- package/dist/components/Amelipro/AmeliproTabs/AmeliproTabs.d.ts +7501 -10095
- package/dist/components/Amelipro/AmeliproTextArea/AmeliproTextArea.d.ts +21 -21
- package/dist/components/Amelipro/AmeliproTextField/AmeliproTextField.d.ts +41 -41
- package/dist/components/Amelipro/StructureMenu/StructureTabs/StructureTabs.d.ts +2 -2
- package/dist/components/CookiesSelection/CookiesInformation/CookiesInformation.d.ts +20 -498
- package/dist/components/Customs/Selects/SyAutocomplete/SyAutocomplete.d.ts +108 -146
- package/dist/components/Customs/Selects/SyInputSelect/SyInputSelect.d.ts +5 -5
- package/dist/components/Customs/Selects/SySelect/SySelect.d.ts +12 -16
- package/dist/components/Customs/SyCheckBoxGroup/SyCheckBoxGroup.d.ts +8 -8
- package/dist/components/Customs/SyCheckbox/SyCheckbox.d.ts +28 -506
- package/dist/components/Customs/SyRadioGroup/SyRadioGroup.d.ts +28 -506
- package/dist/components/Customs/SyTextField/SyTextField.d.ts +65 -85
- package/dist/components/DatePicker/CalendarMode/DatePicker.d.ts +627 -771
- package/dist/components/DatePicker/ComplexDatePicker/ComplexDatePicker.d.ts +315 -402
- package/dist/components/DatePicker/DateTextInput/DateTextInput.d.ts +112 -155
- package/dist/components/DatePicker/composables/index.d.ts +1 -0
- package/dist/components/DatePicker/composables/useDatePickerFocusTrap.d.ts +11 -0
- package/dist/components/DatePicker/composables/useDateTextField.d.ts +4 -4
- package/dist/components/DatePicker/composables/useDateValidation.d.ts +3 -3
- package/dist/components/DatePicker/composables/useInputBlurHandler.d.ts +2 -2
- package/dist/components/DatePicker/composables/useManualDateValidation.d.ts +2 -2
- package/dist/components/HeaderNavigationBar/HeaderNavigationBar.d.ts +4 -4
- package/dist/components/HeaderToolbar/HeaderToolbar.d.ts +20 -28
- package/dist/components/LunarCalendar/useLunarCalendarValidation.d.ts +3 -3
- package/dist/components/MonthPicker/MonthPicker.d.ts +86 -122
- package/dist/components/MonthPicker/MonthPickerText/MonthPickerInput.d.ts +85 -121
- package/dist/components/NirField/NirField.d.ts +206 -270
- package/dist/components/NirField/locales.d.ts +10 -10
- package/dist/components/NirField/useNirValidation.d.ts +64 -0
- package/dist/components/PasswordField/PasswordField.d.ts +8 -9
- package/dist/components/PeriodField/PeriodField.d.ts +1352 -1640
- package/dist/components/PhoneField/PhoneField.d.ts +88 -124
- package/dist/components/RangeField/RangeSlider/RangeSlider.d.ts +12 -12
- package/dist/components/SyTextArea/SyTextArea.d.ts +34 -14
- package/dist/components/SyTextArea/useDefaultValidationRules.d.ts +11 -0
- package/dist/components/Tables/SyServerTable/SyServerTable.d.ts +9 -6
- package/dist/components/Tables/SyTable/SyTable.d.ts +9 -6
- package/dist/components/Tables/common/SyTableFilter.d.ts +2 -3
- package/dist/components/Tables/common/SyTablePagination.d.ts +17 -19
- package/dist/components/Tables/common/filters/AutocompleteFilter.d.ts +120 -0
- package/dist/components/Tables/common/filters/locales.d.ts +0 -1
- package/dist/components/Tables/common/types.d.ts +19 -3
- package/dist/components/Tables/common/useClickableTableRow.d.ts +17 -0
- package/dist/components/Tables/common/usePagination.d.ts +3 -1
- package/dist/components/Tables/common/usePinnedColumns.d.ts +31 -0
- package/dist/components/Tables/common/useTableHeaders.d.ts +2 -0
- package/dist/components/Tables/common/useTableRowCheckboxAccessibility.d.ts +5 -0
- package/dist/components/UploadWorkflow/UploadWorkflow.d.ts +6 -6
- package/dist/composables/date/useDatePickerAccessibility.d.ts +1 -1
- package/dist/composables/rules/useFieldValidation.d.ts +4 -4
- package/dist/composables/unifyValidation/useCustomValidation.d.ts +8 -0
- package/dist/composables/unifyValidation/useValidation.d.ts +102 -0
- package/dist/composables/unifyValidation/useVuetifyValidation.d.ts +18 -0
- package/dist/composables/useFormFieldErrorHandling.d.ts +2 -2
- package/dist/composables/validation/useFormValidation.d.ts +11 -2
- package/dist/composables/validation/useValidation.d.ts +15 -9
- package/dist/design-system-v3.d.ts +2 -0
- package/dist/design-system-v3.js +186 -187
- package/dist/design-system-v3.umd.cjs +1 -1066
- package/dist/{main-aLKwdMi1.js → main-BtTqyn4z.js} +16434 -15672
- package/dist/main-C1e3eoxd.cjs +1067 -0
- package/dist/main.d.ts +0 -1
- package/dist/synapse.css +1 -0
- package/dist/tooth-11-D3sLWv2n.cjs +1 -0
- package/dist/tooth-12-CXrLuH03.cjs +1 -0
- package/dist/tooth-13-BSfo5fpT.cjs +1 -0
- package/dist/tooth-14-DMzulx0h.cjs +1 -0
- package/dist/tooth-15-BKRFVi-9.cjs +1 -0
- package/dist/tooth-16-CpuxAbuM.cjs +1 -0
- package/dist/tooth-17-BPoahUdg.cjs +1 -0
- package/dist/tooth-18-DhHJz8sy.cjs +1 -0
- package/dist/tooth-21-Dgd5hn_X.cjs +1 -0
- package/dist/tooth-22-C2Tn19sB.cjs +1 -0
- package/dist/tooth-23-C9uaaSGb.cjs +1 -0
- package/dist/tooth-24-BrK9UGpf.cjs +1 -0
- package/dist/tooth-25-CE_EfGNp.cjs +1 -0
- package/dist/tooth-26-Ctv4i9Fy.cjs +1 -0
- package/dist/tooth-27-C5J7JkWM.cjs +1 -0
- package/dist/tooth-28-Z9oWqjo0.cjs +1 -0
- package/dist/tooth-31-BrYqmkTi.cjs +1 -0
- package/dist/tooth-32-BNNR0oCZ.cjs +1 -0
- package/dist/tooth-33-DuxvqO2J.cjs +1 -0
- package/dist/tooth-34-BCSCXMB6.cjs +1 -0
- package/dist/tooth-35-BLUXkX88.cjs +1 -0
- package/dist/tooth-36-IrKHYqlA.cjs +1 -0
- package/dist/tooth-37-BYqpdMwo.cjs +1 -0
- package/dist/tooth-38-B_eNXXdu.cjs +1 -0
- package/dist/tooth-41-Ddva4Ot8.cjs +1 -0
- package/dist/tooth-42-szcDqlM0.cjs +1 -0
- package/dist/tooth-43-B3ka6rQm.cjs +1 -0
- package/dist/tooth-44-CazyQucj.cjs +1 -0
- package/dist/tooth-45-B4HQtc8n.cjs +1 -0
- package/dist/tooth-46-BPM40gbG.cjs +1 -0
- package/dist/tooth-47-Dvr20dlh.cjs +1 -0
- package/dist/tooth-48-Bd8ljGsF.cjs +1 -0
- package/dist/tooth-51-OBpwCOF3.cjs +1 -0
- package/dist/tooth-52-aKxyHcmq.cjs +1 -0
- package/dist/tooth-53-vCwJjTOc.cjs +1 -0
- package/dist/tooth-54-DsWu2iFy.cjs +1 -0
- package/dist/tooth-55-BxC1X2Dn.cjs +1 -0
- package/dist/tooth-61-BbLvxMQi.cjs +1 -0
- package/dist/tooth-62-CmTkWczP.cjs +1 -0
- package/dist/tooth-63-DI7l_2qI.cjs +1 -0
- package/dist/tooth-64-B21sOsJh.cjs +1 -0
- package/dist/tooth-65-D2ZC2VEr.cjs +1 -0
- package/dist/tooth-71-D473PPO5.cjs +1 -0
- package/dist/tooth-72-Drh1wnNu.cjs +1 -0
- package/dist/tooth-73-DzlwYI23.cjs +1 -0
- package/dist/tooth-74-8aGvcZPg.cjs +1 -0
- package/dist/tooth-75-BFK7At_r.cjs +1 -0
- package/dist/tooth-81-BZmR-I0M.cjs +1 -0
- package/dist/tooth-82-euVfUUZV.cjs +1 -0
- package/dist/tooth-83-KV010j64.cjs +1 -0
- package/dist/tooth-84-BBg1RjhZ.cjs +1 -0
- package/dist/tooth-85-Cr-kc1wM.cjs +1 -0
- package/dist/vuetifyConfig.js +561 -0
- package/dist/vuetifyConfig.umd.cjs +1 -0
- package/package.json +10 -4
- package/src/assets/overrides/_btns.scss +0 -6
- package/src/assets/overrides/_icons.scss +9 -1
- package/src/assets/overrides/_typography.scss +0 -10
- package/src/components/Amelipro/AmeliproAutoCompleteField/__tests__/__snapshots__/AmeliproAutoCompleteField.spec.ts.snap +2 -2
- package/src/components/Amelipro/AmeliproHeader/AmeliproHeaderBar/AmeliproHeaderBrandSection/__tests__/__snapshots__/AmeliproHeaderBrandSection.spec.ts.snap +1 -1
- package/src/components/Amelipro/AmeliproTextArea/__tests__/__snapshots__/AmeliproTextArea.spec.ts.snap +2 -2
- package/src/components/Captcha/accessibilite/Accessibility.mdx +86 -8
- package/src/components/Captcha/tests/__snapshots__/Captcha.spec.ts.snap +12 -12
- package/src/components/ChipList/ChipList.stories.ts +0 -15
- package/src/components/ChipList/ChipList.vue +5 -1
- package/src/components/ChipList/accessibilite/Accessibility.mdx +83 -10
- package/src/components/ChipList/tests/ChipList.a11y.spec.ts +41 -0
- package/src/components/Customs/Selects/SelectBtnField/accessibilite/Accessibility.mdx +0 -9
- package/src/components/Customs/Selects/SyAutocomplete/SyAutocomplete.vue +22 -5
- package/src/components/Customs/Selects/SyAutocomplete/tests/SyAutocomplete.spec.ts +143 -0
- package/src/components/Customs/Selects/SyAutocomplete/utils/ariaManager.ts +14 -10
- package/src/components/Customs/Selects/SyInputSelect/SyInputSelect.stories.ts +4 -4
- package/src/components/Customs/Selects/SyInputSelect/SyInputSelect.vue +8 -9
- package/src/components/Customs/Selects/SyInputSelect/tests/SyInputSelect.spec.ts +10 -10
- package/src/components/Customs/Selects/SySelect/SySelect.vue +14 -11
- package/src/components/Customs/Selects/SySelect/tests/SySelect.spec.ts +54 -0
- package/src/components/Customs/SyCheckBoxGroup/SyCheckBoxGroup.vue +6 -9
- package/src/components/Customs/SyCheckbox/SyCheckbox.stories.ts +10 -16
- package/src/components/Customs/SyCheckbox/SyCheckbox.vue +16 -11
- package/src/components/Customs/SyCheckbox/accessibilite/Accessibility.mdx +35 -0
- package/src/components/Customs/SyCheckbox/tests/SyCheckbox.a11y.spec.ts +134 -2
- package/src/components/Customs/SyForm/SyForm.stories.ts +31 -5
- package/src/components/Customs/SyRadioGroup/SyRadioGroup.vue +4 -7
- package/src/components/Customs/SyTextField/SyTextField.mdx +1 -1
- package/src/components/Customs/SyTextField/SyTextField.stories.ts +29 -27
- package/src/components/Customs/SyTextField/SyTextField.vue +154 -157
- package/src/components/Customs/SyTextField/tests/SyTextField.a11y.spec.ts +32 -0
- package/src/components/Customs/SyTextField/tests/SyTextField.spec.ts +120 -11
- package/src/components/DatePicker/CalendarMode/DatePicker.stories.ts +62 -58
- package/src/components/DatePicker/CalendarMode/DatePicker.vue +330 -223
- package/src/components/DatePicker/CalendarMode/accessibilite/Accessibility.mdx +82 -0
- package/src/components/DatePicker/CalendarMode/tests/DatePicker.a11y.spec.ts +141 -0
- package/src/components/DatePicker/ComplexDatePicker/ComplexDatePicker.stories.ts +2 -56
- package/src/components/DatePicker/ComplexDatePicker/ComplexDatePicker.vue +195 -159
- package/src/components/DatePicker/ComplexDatePicker/accessibilite/Accessibility.mdx +76 -0
- package/src/components/DatePicker/ComplexDatePicker/tests/ComplexDatePicker.spec.ts +10 -10
- package/src/components/DatePicker/DatePickerValidationExample/CalendarMode.stories.ts +8 -8
- package/src/components/DatePicker/DatePickerValidationExample/ComplexDatePicker.stories.ts +106 -8
- package/src/components/DatePicker/DatePickerValidationExample/DateTextInput.stories.ts +12 -11
- package/src/components/DatePicker/DatePickerValidationExample/MultiMode.stories.ts +12 -12
- package/src/components/DatePicker/DateTextInput/DateRange.stories.ts +0 -12
- package/src/components/DatePicker/DateTextInput/DateTextInput.vue +63 -57
- package/src/components/DatePicker/DateTextInput/NoCalendar.stories.ts +3 -0
- package/src/components/DatePicker/DateTextInput/accessibilite/Accessibility.mdx +66 -0
- package/src/components/DatePicker/DateTextInput/tests/DateTextInput.spec.ts +52 -1
- package/src/components/DatePicker/composables/index.ts +1 -0
- package/src/components/DatePicker/composables/tests/useCalendarKeyboardNavigation.spec.ts +109 -65
- package/src/components/DatePicker/composables/tests/useDatePickerFocusTrap.spec.ts +138 -0
- package/src/components/DatePicker/composables/tests/useDateValidation.spec.ts +74 -18
- package/src/components/DatePicker/composables/tests/useInputBlurHandler.spec.ts +39 -0
- package/src/components/DatePicker/composables/tests/useManualDateValidation.spec.ts +91 -0
- package/src/components/DatePicker/composables/useCalendarKeyboardNavigation.ts +442 -36
- package/src/components/DatePicker/composables/useDatePickerFocusTrap.ts +92 -0
- package/src/components/DatePicker/composables/useDateTextField.ts +7 -6
- package/src/components/DatePicker/composables/useDateValidation.ts +36 -35
- package/src/components/DatePicker/composables/useInputBlurHandler.ts +3 -3
- package/src/components/DatePicker/composables/useManualDateValidation.ts +6 -2
- package/src/components/DiacriticPicker/accessibilite/Accessibility.mdx +76 -8
- package/src/components/HeaderBar/HeaderBar.stories.ts +14 -2
- package/src/components/Logo/accessibilite/Accessibility.mdx +73 -11
- package/src/components/LogoBrandSection/accessibilite/Accessibility.mdx +85 -9
- package/src/components/LunarCalendar/tests/LunarCalendar.spec.ts +3 -1
- package/src/components/LunarCalendar/useLunarCalendarValidation.ts +4 -5
- package/src/components/MonthPicker/tests/MonthPicker.spec.ts +2 -1
- package/src/components/MonthPicker/tests/__snapshots__/MonthPicker.spec.ts.snap +7 -7
- package/src/components/NirField/NirField.stories.ts +4 -0
- package/src/components/NirField/NirField.vue +64 -260
- package/src/components/NirField/accessibilite/Accessibility.mdx +2 -2
- package/src/components/NirField/locales.ts +1 -1
- package/src/components/NirField/tests/NirField.spec.ts +6 -0
- package/src/components/NirField/useNirValidation.ts +271 -0
- package/src/components/PasswordField/PasswordField.stories.ts +4 -4
- package/src/components/PasswordField/PasswordField.vue +18 -24
- package/src/components/PasswordField/tests/PasswordField.spec.ts +6 -3
- package/src/components/PeriodField/PeriodField.stories.ts +4 -4
- package/src/components/PeriodField/PeriodField.vue +57 -57
- package/src/components/PeriodField/__tests__/PeriodField.async.spec.ts +32 -0
- package/src/components/PeriodField/accessibilite/Accessibility.mdx +68 -8
- package/src/components/PeriodField/tests/PeriodField.spec.ts +28 -2
- package/src/components/PhoneField/PhoneField.vue +5 -6
- package/src/components/PhoneField/tests/PhoneField.spec.ts +1 -0
- package/src/components/RangeField/RangeField.vue +6 -0
- package/src/components/SyTextArea/SyTextArea.stories.ts +138 -2
- package/src/components/SyTextArea/SyTextArea.vue +53 -23
- package/src/components/SyTextArea/tests/SyTextArea.spec.ts +126 -3
- package/src/components/SyTextArea/useDefaultValidationRules.ts +74 -0
- package/src/components/Tables/SyServerTable/SyServerTable.mdx +25 -0
- package/src/components/Tables/SyServerTable/SyServerTable.stories.ts +673 -1
- package/src/components/Tables/SyServerTable/SyServerTable.vue +148 -91
- package/src/components/Tables/SyServerTable/tests/SyServerTable.a11y.spec.ts +58 -0
- package/src/components/Tables/SyServerTable/tests/SyServerTable.spec.ts +122 -0
- package/src/components/Tables/SyTable/SyTable.mdx +25 -0
- package/src/components/Tables/SyTable/SyTable.stories.ts +452 -1
- package/src/components/Tables/SyTable/SyTable.vue +130 -56
- package/src/components/Tables/SyTable/tests/SyTable.a11y.spec.ts +57 -0
- package/src/components/Tables/SyTable/tests/SyTable.spec.ts +108 -0
- package/src/components/Tables/common/SyTableFilter.vue +22 -2
- package/src/components/Tables/common/TableHeader.vue +5 -1
- package/src/components/Tables/common/filters/AutocompleteFilter.vue +160 -0
- package/src/components/Tables/common/filters/NumberFilter.vue +1 -1
- package/src/components/Tables/common/filters/SelectFilter.vue +10 -11
- package/src/components/Tables/common/filters/TextFilter.vue +1 -1
- package/src/components/Tables/common/filters/getFilterComponent.ts +8 -1
- package/src/components/Tables/common/filters/locales.ts +0 -1
- package/src/components/Tables/common/filters/tests/AutocompleteFilter.a11y.spec.ts +110 -0
- package/src/components/Tables/common/filters/tests/AutocompleteFilter.spec.ts +203 -0
- package/src/components/Tables/common/filters/tests/SelectFilter.a11y.spec.ts +104 -0
- package/src/components/Tables/common/filters/tests/SelectFilter.spec.ts +152 -16
- package/src/components/Tables/common/tableFilterUtils.ts +3 -0
- package/src/components/Tables/common/tableStyles.scss +48 -4
- package/src/components/Tables/common/tests/filterByRange.spec.ts +2 -1
- package/src/components/Tables/common/types.ts +13 -4
- package/src/components/Tables/common/useClickableTableRow.ts +103 -0
- package/src/components/Tables/common/usePagination.ts +13 -0
- package/src/components/Tables/common/usePinnedColumns.ts +237 -0
- package/src/components/Tables/common/useTableHeaders.ts +3 -3
- package/src/components/Tables/common/useTableRowCheckboxAccessibility.ts +41 -0
- package/src/composables/date/tests/useDatePickerAccessibility.spec.ts +2 -6
- package/src/composables/date/useDatePickerAccessibility.ts +42 -207
- package/src/composables/rules/tests/useFieldValidation.spec.ts +120 -120
- package/src/composables/rules/useFieldValidation.ts +34 -17
- package/src/composables/unifyValidation/tests/useCustomValidation.spec.ts +601 -0
- package/src/composables/unifyValidation/tests/useValidation.spec.ts +2048 -0
- package/src/composables/unifyValidation/tests/useVuetifyValidation.spec.ts +184 -0
- package/src/composables/unifyValidation/useCustomValidation.ts +95 -0
- package/src/composables/unifyValidation/useValidation.ts +190 -0
- package/src/composables/unifyValidation/useVuetifyValidation.ts +54 -0
- package/src/composables/useFormFieldErrorHandling.ts +4 -7
- package/src/composables/validation/tests/useFormValidation.spec.ts +14 -0
- package/src/composables/validation/tests/useValidation.spec.ts +116 -21
- package/src/composables/validation/useFormValidation.ts +20 -13
- package/src/composables/validation/useValidatable.ts +8 -1
- package/src/composables/validation/useValidation.ts +135 -99
- package/src/composantsVuetify/Introduction.mdx +48 -0
- package/src/composantsVuetify/VBtn/VBtn.mdx +72 -0
- package/src/composantsVuetify/VBtn/v-btn.stories.ts +121 -0
- package/src/composantsVuetify/VTooltip/VTooltip.mdx +32 -0
- package/src/composantsVuetify/VTooltip/v-tooltip.stories.ts +95 -0
- package/src/designTokens/tokens/cnam/cnamSemantic.ts +2 -2
- package/src/main.ts +0 -2
- package/src/stories/Components/Components.stories.ts +74 -9
- package/src/stories/Demarrer/Accueil.stories.ts +3 -3
- package/src/stories/GuideDuDev/Amelipro.mdx +15 -0
- package/src/stories/GuideDuDev/Amelipro.stories.ts +209 -0
- package/src/stories/GuideDuDev/vuetifyOptions.mdx +3 -3
- package/dist/SelectFilter-BioGT6Nn.js +0 -136
- package/dist/style.css +0 -1
- package/src/components/DatePicker/Accessibilite.mdx +0 -14
|
@@ -0,0 +1,2048 @@
|
|
|
1
|
+
/* eslint-disable vue/one-component-per-file */
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import { defineComponent, nextTick, ref } from 'vue'
|
|
4
|
+
import { mount } from '@vue/test-utils'
|
|
5
|
+
import { useValidation } from '../useValidation'
|
|
6
|
+
import SyForm from '@/components/Customs/SyForm/SyForm.vue'
|
|
7
|
+
import type { ValidationRule } from '@/composables/validation/useValidation'
|
|
8
|
+
|
|
9
|
+
/** Run a composable inside a mounted Vue component to support lifecycle hooks. */
|
|
10
|
+
function withSetup<T>(setup: () => T): { result: T, wrapper: ReturnType<typeof mount> } {
|
|
11
|
+
let result: T
|
|
12
|
+
const TestComponent = defineComponent({
|
|
13
|
+
setup() {
|
|
14
|
+
result = setup()
|
|
15
|
+
return {}
|
|
16
|
+
},
|
|
17
|
+
render: () => null,
|
|
18
|
+
})
|
|
19
|
+
const wrapper = mount(TestComponent)
|
|
20
|
+
return { result: result!, wrapper }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('useValidation (unifyValidation)', () => {
|
|
24
|
+
const makeParams = (overrides = {}) => ({
|
|
25
|
+
modelValue: ref<unknown>(''),
|
|
26
|
+
readonly: ref(false),
|
|
27
|
+
disabled: ref(false),
|
|
28
|
+
required: ref(false),
|
|
29
|
+
isValidateOnBlur: ref(true),
|
|
30
|
+
showSuccessMessages: ref(true),
|
|
31
|
+
disableErrorHandling: ref(false),
|
|
32
|
+
label: ref('Mon champ'),
|
|
33
|
+
focused: ref(false),
|
|
34
|
+
useVuetifyValidation: false as const,
|
|
35
|
+
customRules: ref<ValidationRule[]>([]),
|
|
36
|
+
customWarningRules: ref<ValidationRule[]>([]),
|
|
37
|
+
customSuccessRules: ref<ValidationRule[]>([]),
|
|
38
|
+
...overrides,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('disableErrorHandling', () => {
|
|
42
|
+
it('returns a stub with empty refs and false computed values when disableErrorHandling is true', () => {
|
|
43
|
+
const params = makeParams({ disableErrorHandling: ref(true) })
|
|
44
|
+
const result = useValidation(params as Parameters<typeof useValidation>[0])
|
|
45
|
+
|
|
46
|
+
expect(result.errors.value).toEqual([])
|
|
47
|
+
expect(result.warnings.value).toEqual([])
|
|
48
|
+
expect(result.successes.value).toEqual([])
|
|
49
|
+
expect(result.hasError.value).toBe(false)
|
|
50
|
+
expect(result.hasWarning.value).toBe(false)
|
|
51
|
+
expect(result.hasSuccess.value).toBe(false)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('stub validate() always returns true when disableErrorHandling is true', async () => {
|
|
55
|
+
const params = makeParams({ disableErrorHandling: ref(true) })
|
|
56
|
+
const result = useValidation(params as Parameters<typeof useValidation>[0])
|
|
57
|
+
|
|
58
|
+
const valid = await result.validate()
|
|
59
|
+
expect(valid).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('errorMessages / warningMessages / successMessages props', () => {
|
|
64
|
+
it('syncs errorMessages to errors ref immediately via watch', async () => {
|
|
65
|
+
const errorMessages = ref<string[] | null>(['Une erreur externe'])
|
|
66
|
+
const params = makeParams({ errorMessages })
|
|
67
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
68
|
+
|
|
69
|
+
await nextTick()
|
|
70
|
+
expect(result.errors.value).toContain('Une erreur externe')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('syncs warningMessages to warnings ref immediately', async () => {
|
|
74
|
+
const warningMessages = ref<string[] | null>(['Un avertissement'])
|
|
75
|
+
const params = makeParams({ warningMessages })
|
|
76
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
77
|
+
|
|
78
|
+
await nextTick()
|
|
79
|
+
expect(result.warnings.value).toContain('Un avertissement')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('syncs successMessages to successes ref immediately', async () => {
|
|
83
|
+
const successMessages = ref<string[] | null>(['Succès !'])
|
|
84
|
+
const params = makeParams({ successMessages })
|
|
85
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
86
|
+
|
|
87
|
+
await nextTick()
|
|
88
|
+
expect(result.successes.value).toContain('Succès !')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('clears errors when errorMessages is set to null', async () => {
|
|
92
|
+
const errorMessages = ref<string[] | null>(['Erreur'])
|
|
93
|
+
const params = makeParams({ errorMessages })
|
|
94
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
95
|
+
|
|
96
|
+
await nextTick()
|
|
97
|
+
expect(result.errors.value).toContain('Erreur')
|
|
98
|
+
|
|
99
|
+
errorMessages.value = null
|
|
100
|
+
await nextTick()
|
|
101
|
+
expect(result.errors.value).toEqual([])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('updates errors when errorMessages change reactively', async () => {
|
|
105
|
+
const errorMessages = ref<string[] | null>(null)
|
|
106
|
+
const params = makeParams({ errorMessages })
|
|
107
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
108
|
+
|
|
109
|
+
await nextTick()
|
|
110
|
+
expect(result.errors.value).toEqual([])
|
|
111
|
+
|
|
112
|
+
errorMessages.value = ['Nouvelle erreur']
|
|
113
|
+
await nextTick()
|
|
114
|
+
expect(result.errors.value).toContain('Nouvelle erreur')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('combines rule validation errors with external errorMessages', async () => {
|
|
118
|
+
const errorMessages = ref<string[] | null>(['Erreur par défaut'])
|
|
119
|
+
const params = makeParams({
|
|
120
|
+
modelValue: ref(''),
|
|
121
|
+
errorMessages,
|
|
122
|
+
customRules: ref([{ type: 'required', options: { message: 'Erreur règle' } }]),
|
|
123
|
+
})
|
|
124
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
125
|
+
|
|
126
|
+
const valid = await result.validate()
|
|
127
|
+
expect(valid).toBe(false)
|
|
128
|
+
expect(result.errors.value).toEqual(['Erreur règle', 'Erreur par défaut'])
|
|
129
|
+
expect(result.hasError.value).toBe(true)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('combines Vuetify rule errors with external errorMessages when useVuetifyValidation is true', async () => {
|
|
133
|
+
const params = {
|
|
134
|
+
modelValue: ref<unknown>(''),
|
|
135
|
+
readonly: ref(false),
|
|
136
|
+
disabled: ref(false),
|
|
137
|
+
required: ref(false),
|
|
138
|
+
isValidateOnBlur: ref(true),
|
|
139
|
+
showSuccessMessages: ref(true),
|
|
140
|
+
disableErrorHandling: ref(false),
|
|
141
|
+
label: ref('Mon champ'),
|
|
142
|
+
focused: ref(false),
|
|
143
|
+
useVuetifyValidation: true as const,
|
|
144
|
+
rules: ref([(v: unknown) => !!v || 'Erreur règle vuetify']),
|
|
145
|
+
errorMessages: ref<string[] | null>(['Erreur par défaut']),
|
|
146
|
+
maxErrors: ref(2),
|
|
147
|
+
}
|
|
148
|
+
const { result } = withSetup(() => useValidation(params))
|
|
149
|
+
|
|
150
|
+
const valid = await result.validate()
|
|
151
|
+
expect(valid).toBe(false)
|
|
152
|
+
expect(result.errors.value).toContain('Erreur règle vuetify')
|
|
153
|
+
expect(result.errors.value).toContain('Erreur par défaut')
|
|
154
|
+
expect(result.hasError.value).toBe(true)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('validate()', () => {
|
|
159
|
+
describe('when useVuetifyValidation = false', () => {
|
|
160
|
+
it('returns true and clears state when readonly is true', async () => {
|
|
161
|
+
const params = makeParams({
|
|
162
|
+
readonly: ref(false),
|
|
163
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis readonly' } }]),
|
|
164
|
+
modelValue: ref(''),
|
|
165
|
+
})
|
|
166
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
167
|
+
|
|
168
|
+
const invalid = await result.validate()
|
|
169
|
+
expect(invalid).toBe(false)
|
|
170
|
+
expect(result.errors.value).toContain('Requis readonly')
|
|
171
|
+
|
|
172
|
+
params.readonly.value = true
|
|
173
|
+
const valid = await result.validate()
|
|
174
|
+
|
|
175
|
+
expect(valid).toBe(true)
|
|
176
|
+
expect(result.errors.value).toEqual([])
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('returns true and clears state when disabled is true', async () => {
|
|
180
|
+
const params = makeParams({
|
|
181
|
+
disabled: ref(false),
|
|
182
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis disabled' } }]),
|
|
183
|
+
modelValue: ref(''),
|
|
184
|
+
})
|
|
185
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
186
|
+
|
|
187
|
+
const invalid = await result.validate()
|
|
188
|
+
expect(invalid).toBe(false)
|
|
189
|
+
expect(result.errors.value).toContain('Requis disabled')
|
|
190
|
+
|
|
191
|
+
params.disabled.value = true
|
|
192
|
+
const valid = await result.validate()
|
|
193
|
+
|
|
194
|
+
expect(valid).toBe(true)
|
|
195
|
+
expect(result.errors.value).toEqual([])
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('returns true when custom required rule passes (non-empty value)', async () => {
|
|
199
|
+
const params = makeParams({
|
|
200
|
+
useVuetifyValidation: false as const,
|
|
201
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis' } }]),
|
|
202
|
+
modelValue: ref('bonjour'),
|
|
203
|
+
})
|
|
204
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
205
|
+
|
|
206
|
+
const valid = await result.validate()
|
|
207
|
+
expect(valid).toBe(true)
|
|
208
|
+
expect(result.errors.value).toEqual([])
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('returns false and populates errors when custom required rule fails (empty value)', async () => {
|
|
212
|
+
const params = makeParams({
|
|
213
|
+
useVuetifyValidation: false as const,
|
|
214
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis' } }]),
|
|
215
|
+
modelValue: ref(''),
|
|
216
|
+
})
|
|
217
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
218
|
+
|
|
219
|
+
const valid = await result.validate()
|
|
220
|
+
expect(valid).toBe(false)
|
|
221
|
+
expect(result.errors.value).toContain('Requis')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('supports custom rules with validate function in options (sync)', async () => {
|
|
225
|
+
const params = makeParams({
|
|
226
|
+
useVuetifyValidation: false as const,
|
|
227
|
+
customRules: ref([
|
|
228
|
+
{
|
|
229
|
+
type: 'custom',
|
|
230
|
+
options: {
|
|
231
|
+
validate: (value: unknown) => value === 'ok',
|
|
232
|
+
message: 'Valeur personnalisée invalide',
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
]),
|
|
236
|
+
modelValue: ref('ko'),
|
|
237
|
+
})
|
|
238
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
239
|
+
|
|
240
|
+
const valid = await result.validate()
|
|
241
|
+
expect(valid).toBe(false)
|
|
242
|
+
expect(result.errors.value).toContain('Valeur personnalisée invalide')
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('supports custom rules with validate function in options (async)', async () => {
|
|
246
|
+
const params = makeParams({
|
|
247
|
+
useVuetifyValidation: false as const,
|
|
248
|
+
customRules: ref([
|
|
249
|
+
{
|
|
250
|
+
type: 'custom',
|
|
251
|
+
options: {
|
|
252
|
+
validate: async (value: unknown) => value === 'ok',
|
|
253
|
+
message: 'Erreur async',
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
]),
|
|
257
|
+
modelValue: ref('ko'),
|
|
258
|
+
})
|
|
259
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
260
|
+
|
|
261
|
+
const valid = await result.validate()
|
|
262
|
+
expect(valid).toBe(false)
|
|
263
|
+
expect(result.errors.value).toContain('Erreur async')
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('populates warnings from custom warning rules', async () => {
|
|
267
|
+
const params = makeParams({
|
|
268
|
+
useVuetifyValidation: false as const,
|
|
269
|
+
customRules: ref([]),
|
|
270
|
+
customWarningRules: ref([
|
|
271
|
+
{
|
|
272
|
+
type: 'custom',
|
|
273
|
+
options: {
|
|
274
|
+
validate: (value: unknown) => value === 'ok',
|
|
275
|
+
warningMessage: 'Warning custom',
|
|
276
|
+
isWarning: true,
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
]),
|
|
280
|
+
modelValue: ref('ko'),
|
|
281
|
+
})
|
|
282
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
283
|
+
|
|
284
|
+
const valid = await result.validate()
|
|
285
|
+
expect(valid).toBe(true)
|
|
286
|
+
expect(result.warnings.value).toContain('Warning custom')
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('populates successes from custom success rules', async () => {
|
|
290
|
+
const params = makeParams({
|
|
291
|
+
useVuetifyValidation: false as const,
|
|
292
|
+
customRules: ref([]),
|
|
293
|
+
customSuccessRules: ref([
|
|
294
|
+
{
|
|
295
|
+
type: 'custom',
|
|
296
|
+
options: {
|
|
297
|
+
validate: (value: unknown) => value === 'ok',
|
|
298
|
+
successMessage: 'Succès custom',
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
]),
|
|
302
|
+
modelValue: ref('ok'),
|
|
303
|
+
})
|
|
304
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
305
|
+
|
|
306
|
+
const valid = await result.validate()
|
|
307
|
+
expect(valid).toBe(true)
|
|
308
|
+
expect(result.successes.value).toContain('Succès custom')
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
describe('blur vs input validation triggers', () => {
|
|
312
|
+
it('with isValidateOnBlur=true, does not validate while focused then validates on blur', async () => {
|
|
313
|
+
const params = makeParams({
|
|
314
|
+
useVuetifyValidation: false as const,
|
|
315
|
+
isValidateOnBlur: ref(true),
|
|
316
|
+
focused: ref(true),
|
|
317
|
+
modelValue: ref(''),
|
|
318
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis blur' } }]),
|
|
319
|
+
})
|
|
320
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
321
|
+
|
|
322
|
+
await nextTick()
|
|
323
|
+
expect(result.errors.value).toEqual([])
|
|
324
|
+
|
|
325
|
+
params.focused.value = false
|
|
326
|
+
await nextTick()
|
|
327
|
+
expect(result.errors.value).toContain('Requis blur')
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('with isValidateOnBlur=false, does not validate while focused then validates on blur', async () => {
|
|
331
|
+
const params = makeParams({
|
|
332
|
+
useVuetifyValidation: false as const,
|
|
333
|
+
isValidateOnBlur: ref(false),
|
|
334
|
+
focused: ref(true),
|
|
335
|
+
modelValue: ref(''),
|
|
336
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis blur input-mode' } }]),
|
|
337
|
+
})
|
|
338
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
339
|
+
|
|
340
|
+
await nextTick()
|
|
341
|
+
expect(result.errors.value).toEqual([])
|
|
342
|
+
|
|
343
|
+
params.focused.value = false
|
|
344
|
+
await nextTick()
|
|
345
|
+
expect(result.errors.value).toContain('Requis blur input-mode')
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
describe('input events', () => {
|
|
350
|
+
it('with isValidateOnBlur=false, validates automatically on input change', async () => {
|
|
351
|
+
const params = makeParams({
|
|
352
|
+
useVuetifyValidation: false as const,
|
|
353
|
+
isValidateOnBlur: ref(false),
|
|
354
|
+
modelValue: ref('valeur initiale'),
|
|
355
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis input event' } }]),
|
|
356
|
+
})
|
|
357
|
+
let result!: ReturnType<typeof useValidation>
|
|
358
|
+
const wrapper = mount(defineComponent({
|
|
359
|
+
setup() {
|
|
360
|
+
result = useValidation(params as Parameters<typeof useValidation>[0])
|
|
361
|
+
const onInput = (event: Event) => {
|
|
362
|
+
params.modelValue.value = (event.target as HTMLInputElement).value
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
modelValue: params.modelValue,
|
|
367
|
+
onInput,
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
template: `<input data-test="field" :value="modelValue" @input="onInput">`,
|
|
371
|
+
}))
|
|
372
|
+
|
|
373
|
+
await nextTick()
|
|
374
|
+
expect(result.errors.value).toEqual([])
|
|
375
|
+
|
|
376
|
+
await wrapper.get('[data-test="field"]').setValue('')
|
|
377
|
+
await nextTick()
|
|
378
|
+
expect(result.errors.value).toContain('Requis input event')
|
|
379
|
+
|
|
380
|
+
wrapper.unmount()
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('with isValidateOnBlur=true, does not validate automatically on input change', async () => {
|
|
384
|
+
const params = makeParams({
|
|
385
|
+
useVuetifyValidation: false as const,
|
|
386
|
+
isValidateOnBlur: ref(true),
|
|
387
|
+
modelValue: ref('valeur initiale'),
|
|
388
|
+
focused: ref(false),
|
|
389
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis on blur only' } }]),
|
|
390
|
+
})
|
|
391
|
+
let result!: ReturnType<typeof useValidation>
|
|
392
|
+
const wrapper = mount(defineComponent({
|
|
393
|
+
setup() {
|
|
394
|
+
result = useValidation(params as Parameters<typeof useValidation>[0])
|
|
395
|
+
const onInput = (event: Event) => {
|
|
396
|
+
params.modelValue.value = (event.target as HTMLInputElement).value
|
|
397
|
+
}
|
|
398
|
+
const onFocus = () => {
|
|
399
|
+
params.focused.value = true
|
|
400
|
+
}
|
|
401
|
+
const onBlur = () => {
|
|
402
|
+
params.focused.value = false
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
modelValue: params.modelValue,
|
|
407
|
+
onInput,
|
|
408
|
+
onFocus,
|
|
409
|
+
onBlur,
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
template: `<input data-test="field" :value="modelValue" @input="onInput" @focus="onFocus" @blur="onBlur">`,
|
|
413
|
+
}))
|
|
414
|
+
|
|
415
|
+
await wrapper.get('[data-test="field"]').trigger('focus')
|
|
416
|
+
await wrapper.get('[data-test="field"]').setValue('')
|
|
417
|
+
await nextTick()
|
|
418
|
+
expect(result.errors.value).toEqual([])
|
|
419
|
+
|
|
420
|
+
await wrapper.get('[data-test="field"]').trigger('blur')
|
|
421
|
+
await nextTick()
|
|
422
|
+
expect(result.errors.value).toContain('Requis on blur only')
|
|
423
|
+
|
|
424
|
+
wrapper.unmount()
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
describe('when useVuetifyValidation = true', () => {
|
|
430
|
+
it('returns true when Vuetify required rule passes (non-empty value)', async () => {
|
|
431
|
+
const params = {
|
|
432
|
+
modelValue: ref<unknown>('bonjour'),
|
|
433
|
+
readonly: ref(false),
|
|
434
|
+
disabled: ref(false),
|
|
435
|
+
required: ref(false),
|
|
436
|
+
isValidateOnBlur: ref(true),
|
|
437
|
+
showSuccessMessages: ref(true),
|
|
438
|
+
disableErrorHandling: ref(false),
|
|
439
|
+
label: ref('Mon champ'),
|
|
440
|
+
focused: ref(false),
|
|
441
|
+
useVuetifyValidation: true as const,
|
|
442
|
+
rules: ref([(v: unknown) => !!v || 'Requis Vuetify']),
|
|
443
|
+
maxErrors: ref(1),
|
|
444
|
+
}
|
|
445
|
+
const { result } = withSetup(() => useValidation(params))
|
|
446
|
+
|
|
447
|
+
const valid = await result.validate()
|
|
448
|
+
expect(valid).toBe(true)
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('returns false when Vuetify required rule fails (empty value)', async () => {
|
|
452
|
+
const params = {
|
|
453
|
+
modelValue: ref<unknown>(''),
|
|
454
|
+
readonly: ref(false),
|
|
455
|
+
disabled: ref(false),
|
|
456
|
+
required: ref(false),
|
|
457
|
+
isValidateOnBlur: ref(true),
|
|
458
|
+
showSuccessMessages: ref(true),
|
|
459
|
+
disableErrorHandling: ref(false),
|
|
460
|
+
label: ref('Mon champ'),
|
|
461
|
+
focused: ref(false),
|
|
462
|
+
useVuetifyValidation: true as const,
|
|
463
|
+
rules: ref([(v: unknown) => !!v || 'Requis Vuetify']),
|
|
464
|
+
maxErrors: ref(1),
|
|
465
|
+
}
|
|
466
|
+
const { result } = withSetup(() => useValidation(params))
|
|
467
|
+
|
|
468
|
+
const valid = await result.validate()
|
|
469
|
+
expect(valid).toBe(false)
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it('returns false when Vuetify custom sync rule fails', async () => {
|
|
473
|
+
const params = {
|
|
474
|
+
modelValue: ref<unknown>('ko'),
|
|
475
|
+
readonly: ref(false),
|
|
476
|
+
disabled: ref(false),
|
|
477
|
+
required: ref(false),
|
|
478
|
+
isValidateOnBlur: ref(true),
|
|
479
|
+
showSuccessMessages: ref(true),
|
|
480
|
+
disableErrorHandling: ref(false),
|
|
481
|
+
label: ref('Mon champ'),
|
|
482
|
+
focused: ref(false),
|
|
483
|
+
useVuetifyValidation: true as const,
|
|
484
|
+
rules: ref([(v: unknown) => v === 'ok' || 'Erreur sync Vuetify']),
|
|
485
|
+
maxErrors: ref(1),
|
|
486
|
+
}
|
|
487
|
+
const { result } = withSetup(() => useValidation(params))
|
|
488
|
+
|
|
489
|
+
const valid = await result.validate()
|
|
490
|
+
expect(valid).toBe(false)
|
|
491
|
+
expect(result.errors.value).toContain('Erreur sync Vuetify')
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
it('returns true when Vuetify custom sync rule passes', async () => {
|
|
495
|
+
const params = {
|
|
496
|
+
modelValue: ref<unknown>('ok'),
|
|
497
|
+
readonly: ref(false),
|
|
498
|
+
disabled: ref(false),
|
|
499
|
+
required: ref(false),
|
|
500
|
+
isValidateOnBlur: ref(true),
|
|
501
|
+
showSuccessMessages: ref(true),
|
|
502
|
+
disableErrorHandling: ref(false),
|
|
503
|
+
label: ref('Mon champ'),
|
|
504
|
+
focused: ref(false),
|
|
505
|
+
useVuetifyValidation: true as const,
|
|
506
|
+
rules: ref([(v: unknown) => v === 'ok' || 'Erreur sync Vuetify']),
|
|
507
|
+
maxErrors: ref(1),
|
|
508
|
+
}
|
|
509
|
+
const { result } = withSetup(() => useValidation(params))
|
|
510
|
+
|
|
511
|
+
const valid = await result.validate()
|
|
512
|
+
expect(valid).toBe(true)
|
|
513
|
+
expect(result.errors.value).toEqual([])
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
it('returns false when Vuetify custom async rule fails', async () => {
|
|
517
|
+
const params = {
|
|
518
|
+
modelValue: ref<unknown>('ko'),
|
|
519
|
+
readonly: ref(false),
|
|
520
|
+
disabled: ref(false),
|
|
521
|
+
required: ref(false),
|
|
522
|
+
isValidateOnBlur: ref(true),
|
|
523
|
+
showSuccessMessages: ref(true),
|
|
524
|
+
disableErrorHandling: ref(false),
|
|
525
|
+
label: ref('Mon champ'),
|
|
526
|
+
focused: ref(false),
|
|
527
|
+
useVuetifyValidation: true as const,
|
|
528
|
+
rules: ref([async (v: unknown) => v === 'ok' || 'Erreur async Vuetify']),
|
|
529
|
+
maxErrors: ref(1),
|
|
530
|
+
}
|
|
531
|
+
const { result } = withSetup(() => useValidation(params))
|
|
532
|
+
|
|
533
|
+
const valid = await result.validate()
|
|
534
|
+
expect(valid).toBe(false)
|
|
535
|
+
expect(result.errors.value).toContain('Erreur async Vuetify')
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
it('returns true when Vuetify custom async rule passes', async () => {
|
|
539
|
+
const params = {
|
|
540
|
+
modelValue: ref<unknown>('ok'),
|
|
541
|
+
readonly: ref(false),
|
|
542
|
+
disabled: ref(false),
|
|
543
|
+
required: ref(false),
|
|
544
|
+
isValidateOnBlur: ref(true),
|
|
545
|
+
showSuccessMessages: ref(true),
|
|
546
|
+
disableErrorHandling: ref(false),
|
|
547
|
+
label: ref('Mon champ'),
|
|
548
|
+
focused: ref(false),
|
|
549
|
+
useVuetifyValidation: true as const,
|
|
550
|
+
rules: ref([async (v: unknown) => v === 'ok' || 'Erreur async Vuetify']),
|
|
551
|
+
maxErrors: ref(1),
|
|
552
|
+
}
|
|
553
|
+
const { result } = withSetup(() => useValidation(params))
|
|
554
|
+
|
|
555
|
+
const valid = await result.validate()
|
|
556
|
+
expect(valid).toBe(true)
|
|
557
|
+
expect(result.errors.value).toEqual([])
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
describe('input events', () => {
|
|
561
|
+
it('with isValidateOnBlur=false, validates automatically on input change', async () => {
|
|
562
|
+
const params = {
|
|
563
|
+
modelValue: ref<unknown>('valeur initiale'),
|
|
564
|
+
readonly: ref(false),
|
|
565
|
+
disabled: ref(false),
|
|
566
|
+
required: ref(false),
|
|
567
|
+
isValidateOnBlur: ref(false),
|
|
568
|
+
showSuccessMessages: ref(true),
|
|
569
|
+
disableErrorHandling: ref(false),
|
|
570
|
+
label: ref('Mon champ'),
|
|
571
|
+
focused: ref(false),
|
|
572
|
+
useVuetifyValidation: true as const,
|
|
573
|
+
rules: ref([(v: unknown) => !!v || 'Requis Vuetify input event']),
|
|
574
|
+
maxErrors: ref(1),
|
|
575
|
+
}
|
|
576
|
+
let result!: ReturnType<typeof useValidation>
|
|
577
|
+
const wrapper = mount(defineComponent({
|
|
578
|
+
setup() {
|
|
579
|
+
result = useValidation(params)
|
|
580
|
+
const onInput = (event: Event) => {
|
|
581
|
+
params.modelValue.value = (event.target as HTMLInputElement).value
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
modelValue: params.modelValue,
|
|
586
|
+
onInput,
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
template: `<input data-test="vuetify-field" :value="modelValue" @input="onInput">`,
|
|
590
|
+
}))
|
|
591
|
+
|
|
592
|
+
await nextTick()
|
|
593
|
+
expect(result.errors.value).toEqual([])
|
|
594
|
+
|
|
595
|
+
await wrapper.get('[data-test="vuetify-field"]').setValue('')
|
|
596
|
+
await nextTick()
|
|
597
|
+
expect(result.errors.value).toContain('Requis Vuetify input event')
|
|
598
|
+
|
|
599
|
+
wrapper.unmount()
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
it('with isValidateOnBlur=true, does not validate automatically on input change but validates on blur', async () => {
|
|
603
|
+
const params = {
|
|
604
|
+
modelValue: ref<unknown>('valeur initiale'),
|
|
605
|
+
readonly: ref(false),
|
|
606
|
+
disabled: ref(false),
|
|
607
|
+
required: ref(false),
|
|
608
|
+
isValidateOnBlur: ref(true),
|
|
609
|
+
showSuccessMessages: ref(true),
|
|
610
|
+
disableErrorHandling: ref(false),
|
|
611
|
+
label: ref('Mon champ'),
|
|
612
|
+
focused: ref(false),
|
|
613
|
+
useVuetifyValidation: true as const,
|
|
614
|
+
rules: ref([(v: unknown) => !!v || 'Requis Vuetify blur event']),
|
|
615
|
+
maxErrors: ref(1),
|
|
616
|
+
}
|
|
617
|
+
let result!: ReturnType<typeof useValidation>
|
|
618
|
+
const wrapper = mount(defineComponent({
|
|
619
|
+
setup() {
|
|
620
|
+
result = useValidation(params)
|
|
621
|
+
const onInput = (event: Event) => {
|
|
622
|
+
params.modelValue.value = (event.target as HTMLInputElement).value
|
|
623
|
+
}
|
|
624
|
+
const onFocus = () => {
|
|
625
|
+
params.focused.value = true
|
|
626
|
+
}
|
|
627
|
+
const onBlur = () => {
|
|
628
|
+
params.focused.value = false
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
modelValue: params.modelValue,
|
|
633
|
+
onInput,
|
|
634
|
+
onFocus,
|
|
635
|
+
onBlur,
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
template: `<input data-test="vuetify-field" :value="modelValue" @input="onInput" @focus="onFocus" @blur="onBlur">`,
|
|
639
|
+
}))
|
|
640
|
+
|
|
641
|
+
await wrapper.get('[data-test="vuetify-field"]').trigger('focus')
|
|
642
|
+
await wrapper.get('[data-test="vuetify-field"]').setValue('')
|
|
643
|
+
await nextTick()
|
|
644
|
+
expect(result.errors.value).toEqual([])
|
|
645
|
+
|
|
646
|
+
await wrapper.get('[data-test="vuetify-field"]').trigger('blur')
|
|
647
|
+
await nextTick()
|
|
648
|
+
expect(result.errors.value).toContain('Requis Vuetify blur event')
|
|
649
|
+
|
|
650
|
+
wrapper.unmount()
|
|
651
|
+
})
|
|
652
|
+
})
|
|
653
|
+
})
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
describe('additional coverage', () => {
|
|
657
|
+
it('clears warnings and successes when validate() short-circuits on readonly', async () => {
|
|
658
|
+
const params = makeParams({
|
|
659
|
+
readonly: ref(false),
|
|
660
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis readonly short-circuit' } }]),
|
|
661
|
+
modelValue: ref(''),
|
|
662
|
+
})
|
|
663
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
664
|
+
|
|
665
|
+
const invalid = await result.validate()
|
|
666
|
+
expect(invalid).toBe(false)
|
|
667
|
+
expect(result.errors.value).toContain('Requis readonly short-circuit')
|
|
668
|
+
|
|
669
|
+
params.readonly.value = true
|
|
670
|
+
|
|
671
|
+
const valid = await result.validate()
|
|
672
|
+
expect(valid).toBe(true)
|
|
673
|
+
expect(result.errors.value).toEqual([])
|
|
674
|
+
expect(result.warnings.value).toEqual([])
|
|
675
|
+
expect(result.successes.value).toEqual([])
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
it('hasSuccess is false when successes exist but warnings also exist', async () => {
|
|
679
|
+
const params = makeParams({
|
|
680
|
+
modelValue: ref('ok'),
|
|
681
|
+
hasWarningProp: ref(true),
|
|
682
|
+
customRules: ref([]),
|
|
683
|
+
customSuccessRules: ref([
|
|
684
|
+
{
|
|
685
|
+
type: 'custom',
|
|
686
|
+
options: {
|
|
687
|
+
validate: (value: unknown) => value === 'ok',
|
|
688
|
+
successMessage: 'ok',
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
]),
|
|
692
|
+
})
|
|
693
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
694
|
+
|
|
695
|
+
await result.validate()
|
|
696
|
+
expect(result.successes.value).toContain('ok')
|
|
697
|
+
expect(result.hasWarning.value).toBe(true)
|
|
698
|
+
expect(result.hasSuccess.value).toBeFalsy()
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
it('supports reactive switch between custom and Vuetify validation modes', async () => {
|
|
702
|
+
const params = {
|
|
703
|
+
modelValue: ref<unknown>(''),
|
|
704
|
+
readonly: ref(false),
|
|
705
|
+
disabled: ref(false),
|
|
706
|
+
required: ref(false),
|
|
707
|
+
isValidateOnBlur: ref(true),
|
|
708
|
+
showSuccessMessages: ref(true),
|
|
709
|
+
disableErrorHandling: ref(false),
|
|
710
|
+
label: ref('Champ hybride'),
|
|
711
|
+
focused: ref(false),
|
|
712
|
+
useVuetifyValidation: ref(false),
|
|
713
|
+
customRules: ref([{ type: 'required', options: { message: 'Erreur custom' } }]),
|
|
714
|
+
customWarningRules: ref([]),
|
|
715
|
+
customSuccessRules: ref([]),
|
|
716
|
+
rules: ref([(v: unknown) => !!v || 'Erreur vuetify']),
|
|
717
|
+
maxErrors: ref(1),
|
|
718
|
+
}
|
|
719
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
720
|
+
|
|
721
|
+
let valid = await result.validate()
|
|
722
|
+
expect(valid).toBe(false)
|
|
723
|
+
expect(result.errors.value).toContain('Erreur custom')
|
|
724
|
+
|
|
725
|
+
params.useVuetifyValidation.value = true
|
|
726
|
+
valid = await result.validate()
|
|
727
|
+
expect(valid).toBe(false)
|
|
728
|
+
expect(result.errors.value).toContain('Erreur vuetify')
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
it('respects maxErrors in Vuetify mode', async () => {
|
|
732
|
+
const params = {
|
|
733
|
+
modelValue: ref<unknown>('x'),
|
|
734
|
+
readonly: ref(false),
|
|
735
|
+
disabled: ref(false),
|
|
736
|
+
required: ref(false),
|
|
737
|
+
isValidateOnBlur: ref(true),
|
|
738
|
+
showSuccessMessages: ref(true),
|
|
739
|
+
disableErrorHandling: ref(false),
|
|
740
|
+
label: ref('Mon champ'),
|
|
741
|
+
focused: ref(false),
|
|
742
|
+
useVuetifyValidation: true as const,
|
|
743
|
+
rules: ref([
|
|
744
|
+
() => 'E1',
|
|
745
|
+
() => 'E2',
|
|
746
|
+
() => 'E3',
|
|
747
|
+
]),
|
|
748
|
+
maxErrors: ref(2),
|
|
749
|
+
}
|
|
750
|
+
const { result } = withSetup(() => useValidation(params))
|
|
751
|
+
|
|
752
|
+
const valid = await result.validate()
|
|
753
|
+
expect(valid).toBe(false)
|
|
754
|
+
expect(result.errors.value).toHaveLength(2)
|
|
755
|
+
expect(result.errors.value).toEqual(['E1', 'E2'])
|
|
756
|
+
})
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
describe('vform integration', () => {
|
|
760
|
+
const VForm = defineComponent({
|
|
761
|
+
emits: ['submit'],
|
|
762
|
+
template: `<form data-test="vform" @submit="$emit('submit', $event)"><slot /></form>`,
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
it('validates custom rules on VForm submit and blocks invalid state', async () => {
|
|
766
|
+
const params = makeParams({
|
|
767
|
+
useVuetifyValidation: false as const,
|
|
768
|
+
modelValue: ref(''),
|
|
769
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis VForm custom' } }]),
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
let isValid = true
|
|
773
|
+
let result!: ReturnType<typeof useValidation>
|
|
774
|
+
|
|
775
|
+
const wrapper = mount(defineComponent({
|
|
776
|
+
components: { VForm },
|
|
777
|
+
setup() {
|
|
778
|
+
result = useValidation(params as Parameters<typeof useValidation>[0])
|
|
779
|
+
const onSubmit = async (e: Event) => {
|
|
780
|
+
e.preventDefault()
|
|
781
|
+
isValid = await result.validate()
|
|
782
|
+
}
|
|
783
|
+
const onInput = (event: Event) => {
|
|
784
|
+
params.modelValue.value = (event.target as HTMLInputElement).value
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return { onSubmit, modelValue: params.modelValue, onInput }
|
|
788
|
+
},
|
|
789
|
+
template: `
|
|
790
|
+
<VForm @submit="onSubmit">
|
|
791
|
+
<input data-test="field" :value="modelValue" @input="onInput">
|
|
792
|
+
</VForm>
|
|
793
|
+
`,
|
|
794
|
+
}))
|
|
795
|
+
|
|
796
|
+
await wrapper.get('[data-test="vform"]').trigger('submit')
|
|
797
|
+
await nextTick()
|
|
798
|
+
|
|
799
|
+
expect(isValid).toBe(false)
|
|
800
|
+
expect(result.errors.value).toContain('Requis VForm custom')
|
|
801
|
+
|
|
802
|
+
await wrapper.get('[data-test="field"]').setValue('ok')
|
|
803
|
+
await wrapper.get('[data-test="vform"]').trigger('submit')
|
|
804
|
+
await nextTick()
|
|
805
|
+
|
|
806
|
+
expect(isValid).toBe(true)
|
|
807
|
+
expect(result.errors.value).toEqual([])
|
|
808
|
+
|
|
809
|
+
wrapper.unmount()
|
|
810
|
+
})
|
|
811
|
+
|
|
812
|
+
it('validates Vuetify rules on VForm submit', async () => {
|
|
813
|
+
const params = {
|
|
814
|
+
modelValue: ref<unknown>(''),
|
|
815
|
+
readonly: ref(false),
|
|
816
|
+
disabled: ref(false),
|
|
817
|
+
required: ref(false),
|
|
818
|
+
isValidateOnBlur: ref(true),
|
|
819
|
+
showSuccessMessages: ref(true),
|
|
820
|
+
disableErrorHandling: ref(false),
|
|
821
|
+
label: ref('Mon champ'),
|
|
822
|
+
focused: ref(false),
|
|
823
|
+
useVuetifyValidation: true as const,
|
|
824
|
+
rules: ref([(v: unknown) => !!v || 'Requis VForm Vuetify']),
|
|
825
|
+
maxErrors: ref(1),
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
let isValid = true
|
|
829
|
+
let result!: ReturnType<typeof useValidation>
|
|
830
|
+
|
|
831
|
+
const wrapper = mount(defineComponent({
|
|
832
|
+
components: { VForm },
|
|
833
|
+
setup() {
|
|
834
|
+
result = useValidation(params)
|
|
835
|
+
const onSubmit = async (e: Event) => {
|
|
836
|
+
e.preventDefault()
|
|
837
|
+
isValid = await result.validate()
|
|
838
|
+
}
|
|
839
|
+
const onInput = (event: Event) => {
|
|
840
|
+
params.modelValue.value = (event.target as HTMLInputElement).value
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return { onSubmit, modelValue: params.modelValue, onInput }
|
|
844
|
+
},
|
|
845
|
+
template: `
|
|
846
|
+
<VForm @submit="onSubmit">
|
|
847
|
+
<input data-test="field" :value="modelValue" @input="onInput">
|
|
848
|
+
</VForm>
|
|
849
|
+
`,
|
|
850
|
+
}))
|
|
851
|
+
|
|
852
|
+
await wrapper.get('[data-test="vform"]').trigger('submit')
|
|
853
|
+
await nextTick()
|
|
854
|
+
expect(isValid).toBe(false)
|
|
855
|
+
expect(result.errors.value).toContain('Requis VForm Vuetify')
|
|
856
|
+
|
|
857
|
+
await wrapper.get('[data-test="field"]').setValue('ok')
|
|
858
|
+
await wrapper.get('[data-test="vform"]').trigger('submit')
|
|
859
|
+
await nextTick()
|
|
860
|
+
expect(isValid).toBe(true)
|
|
861
|
+
|
|
862
|
+
wrapper.unmount()
|
|
863
|
+
})
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
describe('syform integration', () => {
|
|
867
|
+
it('uses SyForm validate() with a registered validatable child', async () => {
|
|
868
|
+
const params = makeParams({
|
|
869
|
+
useVuetifyValidation: false as const,
|
|
870
|
+
modelValue: ref(''),
|
|
871
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis SyForm fnc' } }]),
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
let result!: ReturnType<typeof useValidation>
|
|
875
|
+
|
|
876
|
+
const FieldUnderTest = defineComponent({
|
|
877
|
+
setup() {
|
|
878
|
+
result = useValidation(params as Parameters<typeof useValidation>[0])
|
|
879
|
+
const onInput = (event: Event) => {
|
|
880
|
+
params.modelValue.value = (event.target as HTMLInputElement).value
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return { modelValue: params.modelValue, onInput }
|
|
884
|
+
},
|
|
885
|
+
template: `<input data-test="field" :value="modelValue" @input="onInput">`,
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
const wrapper = mount(defineComponent({
|
|
889
|
+
components: { SyForm, FieldUnderTest },
|
|
890
|
+
template: `
|
|
891
|
+
<SyForm data-test="syform">
|
|
892
|
+
<FieldUnderTest />
|
|
893
|
+
</SyForm>
|
|
894
|
+
`,
|
|
895
|
+
}))
|
|
896
|
+
|
|
897
|
+
await nextTick()
|
|
898
|
+
expect(result.errors.value).toEqual([])
|
|
899
|
+
|
|
900
|
+
const syFormVm = wrapper.getComponent(SyForm).vm as {
|
|
901
|
+
validate: () => Promise<boolean>
|
|
902
|
+
clearValidation: () => void
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const invalid = await syFormVm.validate()
|
|
906
|
+
expect(invalid).toBe(false)
|
|
907
|
+
expect(result.errors.value).toContain('Requis SyForm fnc')
|
|
908
|
+
|
|
909
|
+
await wrapper.get('[data-test="field"]').setValue('ok')
|
|
910
|
+
const valid = await syFormVm.validate()
|
|
911
|
+
expect(valid).toBe(true)
|
|
912
|
+
|
|
913
|
+
wrapper.unmount()
|
|
914
|
+
})
|
|
915
|
+
|
|
916
|
+
it('uses SyForm clearValidation() with a registered validatable child', async () => {
|
|
917
|
+
const params = makeParams({
|
|
918
|
+
useVuetifyValidation: false as const,
|
|
919
|
+
modelValue: ref(''),
|
|
920
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis SyForm clear' } }]),
|
|
921
|
+
})
|
|
922
|
+
|
|
923
|
+
let result!: ReturnType<typeof useValidation>
|
|
924
|
+
|
|
925
|
+
const FieldUnderTest = defineComponent({
|
|
926
|
+
setup() {
|
|
927
|
+
result = useValidation(params as Parameters<typeof useValidation>[0])
|
|
928
|
+
const onInput = (event: Event) => {
|
|
929
|
+
params.modelValue.value = (event.target as HTMLInputElement).value
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return { modelValue: params.modelValue, onInput }
|
|
933
|
+
},
|
|
934
|
+
template: `<input data-test="field" :value="modelValue" @input="onInput">`,
|
|
935
|
+
})
|
|
936
|
+
|
|
937
|
+
const wrapper = mount(defineComponent({
|
|
938
|
+
components: { SyForm, FieldUnderTest },
|
|
939
|
+
template: `
|
|
940
|
+
<SyForm data-test="syform">
|
|
941
|
+
<FieldUnderTest />
|
|
942
|
+
</SyForm>
|
|
943
|
+
`,
|
|
944
|
+
}))
|
|
945
|
+
|
|
946
|
+
await nextTick()
|
|
947
|
+
|
|
948
|
+
const syFormVm = wrapper.getComponent(SyForm).vm as {
|
|
949
|
+
validate: () => Promise<boolean>
|
|
950
|
+
clearValidation: () => void
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
await syFormVm.validate()
|
|
954
|
+
expect(result.errors.value).toContain('Requis SyForm clear')
|
|
955
|
+
|
|
956
|
+
syFormVm.clearValidation()
|
|
957
|
+
await nextTick()
|
|
958
|
+
expect(result.errors.value).toEqual([])
|
|
959
|
+
|
|
960
|
+
wrapper.unmount()
|
|
961
|
+
})
|
|
962
|
+
|
|
963
|
+
it('uses SyForm reset() with a registered validatable child — clears errors, resets modelValue and emits reset', async () => {
|
|
964
|
+
const params = makeParams({
|
|
965
|
+
useVuetifyValidation: false as const,
|
|
966
|
+
modelValue: ref(''),
|
|
967
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis SyForm reset' } }]),
|
|
968
|
+
})
|
|
969
|
+
|
|
970
|
+
let result!: ReturnType<typeof useValidation>
|
|
971
|
+
|
|
972
|
+
const FieldUnderTest = defineComponent({
|
|
973
|
+
setup() {
|
|
974
|
+
result = useValidation(params as Parameters<typeof useValidation>[0])
|
|
975
|
+
const onInput = (event: Event) => {
|
|
976
|
+
params.modelValue.value = (event.target as HTMLInputElement).value
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return { modelValue: params.modelValue, onInput }
|
|
980
|
+
},
|
|
981
|
+
template: `<input data-test="field" :value="modelValue" @input="onInput">`,
|
|
982
|
+
})
|
|
983
|
+
|
|
984
|
+
const wrapper = mount(defineComponent({
|
|
985
|
+
components: { SyForm, FieldUnderTest },
|
|
986
|
+
setup() {
|
|
987
|
+
const onReset = () => {
|
|
988
|
+
params.modelValue.value = ''
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return { onReset }
|
|
992
|
+
},
|
|
993
|
+
template: `
|
|
994
|
+
<SyForm data-test="syform" @reset="onReset">
|
|
995
|
+
<FieldUnderTest />
|
|
996
|
+
</SyForm>
|
|
997
|
+
`,
|
|
998
|
+
}))
|
|
999
|
+
|
|
1000
|
+
await nextTick()
|
|
1001
|
+
|
|
1002
|
+
const syFormVm = wrapper.getComponent(SyForm).vm as {
|
|
1003
|
+
validate: () => Promise<boolean>
|
|
1004
|
+
reset: () => void
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Fill the field then validate so errors are empty
|
|
1008
|
+
await wrapper.get('[data-test="field"]').setValue('filled')
|
|
1009
|
+
await syFormVm.validate()
|
|
1010
|
+
expect(result.errors.value).toEqual([])
|
|
1011
|
+
expect(params.modelValue.value).toBe('filled')
|
|
1012
|
+
|
|
1013
|
+
// Reset: clears errors, resets modelValue via @reset handler, emits reset
|
|
1014
|
+
syFormVm.reset()
|
|
1015
|
+
await nextTick()
|
|
1016
|
+
|
|
1017
|
+
expect(result.errors.value).toEqual([])
|
|
1018
|
+
expect(params.modelValue.value).toBe('')
|
|
1019
|
+
expect(wrapper.getComponent(SyForm).emitted('reset')).toHaveLength(1)
|
|
1020
|
+
|
|
1021
|
+
wrapper.unmount()
|
|
1022
|
+
})
|
|
1023
|
+
|
|
1024
|
+
it('uses SyForm reset() with useVuetifyValidation — clears errors, resets modelValue and emits reset', async () => {
|
|
1025
|
+
const params = {
|
|
1026
|
+
modelValue: ref<unknown>(''),
|
|
1027
|
+
readonly: ref(false),
|
|
1028
|
+
disabled: ref(false),
|
|
1029
|
+
required: ref(false),
|
|
1030
|
+
isValidateOnBlur: ref(true),
|
|
1031
|
+
showSuccessMessages: ref(true),
|
|
1032
|
+
disableErrorHandling: ref(false),
|
|
1033
|
+
label: ref('Mon champ'),
|
|
1034
|
+
focused: ref(false),
|
|
1035
|
+
useVuetifyValidation: true as const,
|
|
1036
|
+
rules: ref([(v: unknown) => !!v || 'Requis SyForm Vuetify reset']),
|
|
1037
|
+
maxErrors: ref(1),
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
let result!: ReturnType<typeof useValidation>
|
|
1041
|
+
|
|
1042
|
+
const FieldUnderTest = defineComponent({
|
|
1043
|
+
setup() {
|
|
1044
|
+
result = useValidation(params)
|
|
1045
|
+
const onInput = (event: Event) => {
|
|
1046
|
+
params.modelValue.value = (event.target as HTMLInputElement).value
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
return { modelValue: params.modelValue, onInput }
|
|
1050
|
+
},
|
|
1051
|
+
template: `<input data-test="field" :value="modelValue" @input="onInput">`,
|
|
1052
|
+
})
|
|
1053
|
+
|
|
1054
|
+
const wrapper = mount(defineComponent({
|
|
1055
|
+
components: { SyForm, FieldUnderTest },
|
|
1056
|
+
setup() {
|
|
1057
|
+
const onReset = () => {
|
|
1058
|
+
params.modelValue.value = ''
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return { onReset }
|
|
1062
|
+
},
|
|
1063
|
+
template: `
|
|
1064
|
+
<SyForm data-test="syform" @reset="onReset">
|
|
1065
|
+
<FieldUnderTest />
|
|
1066
|
+
</SyForm>
|
|
1067
|
+
`,
|
|
1068
|
+
}))
|
|
1069
|
+
|
|
1070
|
+
await nextTick()
|
|
1071
|
+
|
|
1072
|
+
const syFormVm = wrapper.getComponent(SyForm).vm as {
|
|
1073
|
+
validate: () => Promise<boolean>
|
|
1074
|
+
reset: () => void
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Fill the field then validate so errors are empty
|
|
1078
|
+
await wrapper.get('[data-test="field"]').setValue('filled')
|
|
1079
|
+
await syFormVm.validate()
|
|
1080
|
+
expect(result.errors.value).toEqual([])
|
|
1081
|
+
expect(params.modelValue.value).toBe('filled')
|
|
1082
|
+
|
|
1083
|
+
// Reset: clears errors, resets modelValue via @reset handler, emits reset
|
|
1084
|
+
syFormVm.reset()
|
|
1085
|
+
await nextTick()
|
|
1086
|
+
|
|
1087
|
+
expect(result.errors.value).toEqual([])
|
|
1088
|
+
expect(params.modelValue.value).toBe('')
|
|
1089
|
+
expect(wrapper.getComponent(SyForm).emitted('reset')).toHaveLength(1)
|
|
1090
|
+
|
|
1091
|
+
wrapper.unmount()
|
|
1092
|
+
})
|
|
1093
|
+
|
|
1094
|
+
it('validates custom rules when SyForm emits submit', async () => {
|
|
1095
|
+
const params = makeParams({
|
|
1096
|
+
useVuetifyValidation: false as const,
|
|
1097
|
+
modelValue: ref(''),
|
|
1098
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis SyForm custom' } }]),
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1101
|
+
let isValid = true
|
|
1102
|
+
let result!: ReturnType<typeof useValidation>
|
|
1103
|
+
|
|
1104
|
+
const wrapper = mount(defineComponent({
|
|
1105
|
+
components: { SyForm },
|
|
1106
|
+
setup() {
|
|
1107
|
+
result = useValidation(params as Parameters<typeof useValidation>[0])
|
|
1108
|
+
const onSubmit = async () => {
|
|
1109
|
+
isValid = await result.validate()
|
|
1110
|
+
}
|
|
1111
|
+
const onInput = (event: Event) => {
|
|
1112
|
+
params.modelValue.value = (event.target as HTMLInputElement).value
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
return { onSubmit, modelValue: params.modelValue, onInput }
|
|
1116
|
+
},
|
|
1117
|
+
template: `
|
|
1118
|
+
<SyForm data-test="syform" :validate-on-submit="false" @submit="onSubmit">
|
|
1119
|
+
<input data-test="field" :value="modelValue" @input="onInput">
|
|
1120
|
+
</SyForm>
|
|
1121
|
+
`,
|
|
1122
|
+
}))
|
|
1123
|
+
|
|
1124
|
+
await wrapper.get('[data-test="syform"]').trigger('submit')
|
|
1125
|
+
await nextTick()
|
|
1126
|
+
|
|
1127
|
+
expect(isValid).toBe(false)
|
|
1128
|
+
expect(result.errors.value).toContain('Requis SyForm custom')
|
|
1129
|
+
|
|
1130
|
+
await wrapper.get('[data-test="field"]').setValue('ok')
|
|
1131
|
+
await wrapper.get('[data-test="syform"]').trigger('submit')
|
|
1132
|
+
await nextTick()
|
|
1133
|
+
|
|
1134
|
+
expect(isValid).toBe(true)
|
|
1135
|
+
expect(result.errors.value).toEqual([])
|
|
1136
|
+
|
|
1137
|
+
wrapper.unmount()
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
it('validates Vuetify rules when SyForm emits submit', async () => {
|
|
1141
|
+
const params = {
|
|
1142
|
+
modelValue: ref<unknown>(''),
|
|
1143
|
+
readonly: ref(false),
|
|
1144
|
+
disabled: ref(false),
|
|
1145
|
+
required: ref(false),
|
|
1146
|
+
isValidateOnBlur: ref(true),
|
|
1147
|
+
showSuccessMessages: ref(true),
|
|
1148
|
+
disableErrorHandling: ref(false),
|
|
1149
|
+
label: ref('Mon champ'),
|
|
1150
|
+
focused: ref(false),
|
|
1151
|
+
useVuetifyValidation: true as const,
|
|
1152
|
+
rules: ref([(v: unknown) => !!v || 'Requis SyForm Vuetify']),
|
|
1153
|
+
maxErrors: ref(1),
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
let isValid = true
|
|
1157
|
+
let result!: ReturnType<typeof useValidation>
|
|
1158
|
+
|
|
1159
|
+
const wrapper = mount(defineComponent({
|
|
1160
|
+
components: { SyForm },
|
|
1161
|
+
setup() {
|
|
1162
|
+
result = useValidation(params)
|
|
1163
|
+
const onSubmit = async () => {
|
|
1164
|
+
isValid = await result.validate()
|
|
1165
|
+
}
|
|
1166
|
+
const onInput = (event: Event) => {
|
|
1167
|
+
params.modelValue.value = (event.target as HTMLInputElement).value
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
return { onSubmit, modelValue: params.modelValue, onInput }
|
|
1171
|
+
},
|
|
1172
|
+
template: `
|
|
1173
|
+
<SyForm data-test="syform" :validate-on-submit="false" @submit="onSubmit">
|
|
1174
|
+
<input data-test="field" :value="modelValue" @input="onInput">
|
|
1175
|
+
</SyForm>
|
|
1176
|
+
`,
|
|
1177
|
+
}))
|
|
1178
|
+
|
|
1179
|
+
await wrapper.get('[data-test="syform"]').trigger('submit')
|
|
1180
|
+
await nextTick()
|
|
1181
|
+
expect(isValid).toBe(false)
|
|
1182
|
+
expect(result.errors.value).toContain('Requis SyForm Vuetify')
|
|
1183
|
+
|
|
1184
|
+
await wrapper.get('[data-test="field"]').setValue('ok')
|
|
1185
|
+
await wrapper.get('[data-test="syform"]').trigger('submit')
|
|
1186
|
+
await nextTick()
|
|
1187
|
+
expect(isValid).toBe(true)
|
|
1188
|
+
|
|
1189
|
+
wrapper.unmount()
|
|
1190
|
+
})
|
|
1191
|
+
|
|
1192
|
+
it('emits reset and allows parent side effects from reset handler', async () => {
|
|
1193
|
+
const params = makeParams({
|
|
1194
|
+
useVuetifyValidation: false as const,
|
|
1195
|
+
modelValue: ref(''),
|
|
1196
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis SyForm reset' } }]),
|
|
1197
|
+
})
|
|
1198
|
+
|
|
1199
|
+
let result!: ReturnType<typeof useValidation>
|
|
1200
|
+
const resetCount = ref(0)
|
|
1201
|
+
|
|
1202
|
+
const wrapper = mount(defineComponent({
|
|
1203
|
+
components: { SyForm },
|
|
1204
|
+
setup() {
|
|
1205
|
+
result = useValidation(params as Parameters<typeof useValidation>[0])
|
|
1206
|
+
const onSubmit = async () => {
|
|
1207
|
+
await result.validate()
|
|
1208
|
+
}
|
|
1209
|
+
const onInput = (event: Event) => {
|
|
1210
|
+
params.modelValue.value = (event.target as HTMLInputElement).value
|
|
1211
|
+
}
|
|
1212
|
+
const onReset = () => {
|
|
1213
|
+
resetCount.value += 1
|
|
1214
|
+
params.modelValue.value = ''
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
return { onSubmit, onReset, modelValue: params.modelValue, onInput }
|
|
1218
|
+
},
|
|
1219
|
+
template: `
|
|
1220
|
+
<SyForm data-test="syform" :validate-on-submit="false" @submit="onSubmit" @reset="onReset">
|
|
1221
|
+
<input data-test="field" :value="modelValue" @input="onInput">
|
|
1222
|
+
</SyForm>
|
|
1223
|
+
`,
|
|
1224
|
+
}))
|
|
1225
|
+
|
|
1226
|
+
await wrapper.get('[data-test="syform"]').trigger('submit')
|
|
1227
|
+
await nextTick()
|
|
1228
|
+
expect(result.errors.value).toContain('Requis SyForm reset')
|
|
1229
|
+
|
|
1230
|
+
await wrapper.get('[data-test="syform"]').trigger('reset')
|
|
1231
|
+
await nextTick()
|
|
1232
|
+
|
|
1233
|
+
expect(resetCount.value).toBe(1)
|
|
1234
|
+
expect(params.modelValue.value).toBe('')
|
|
1235
|
+
expect(result.errors.value).toContain('Requis SyForm reset')
|
|
1236
|
+
|
|
1237
|
+
wrapper.unmount()
|
|
1238
|
+
})
|
|
1239
|
+
})
|
|
1240
|
+
|
|
1241
|
+
describe('computed hasError / hasWarning / hasSuccess', () => {
|
|
1242
|
+
it('hasError is true when errors array is non-empty', async () => {
|
|
1243
|
+
const params = makeParams({
|
|
1244
|
+
modelValue: ref(''),
|
|
1245
|
+
customRules: ref([{ type: 'required', options: { message: 'Une erreur' } }]),
|
|
1246
|
+
})
|
|
1247
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1248
|
+
|
|
1249
|
+
await result.validate()
|
|
1250
|
+
expect(result.errors.value).toContain('Une erreur')
|
|
1251
|
+
expect(result.hasError.value).toBe(true)
|
|
1252
|
+
})
|
|
1253
|
+
|
|
1254
|
+
it('hasError is true when hasErrorProp is true', async () => {
|
|
1255
|
+
const params = makeParams({ hasErrorProp: ref(true) })
|
|
1256
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1257
|
+
|
|
1258
|
+
expect(result.hasError.value).toBe(true)
|
|
1259
|
+
})
|
|
1260
|
+
|
|
1261
|
+
it('hasWarning is true when warnings array is non-empty', async () => {
|
|
1262
|
+
const params = makeParams({
|
|
1263
|
+
modelValue: ref('ko'),
|
|
1264
|
+
customRules: ref([]),
|
|
1265
|
+
customWarningRules: ref([
|
|
1266
|
+
{
|
|
1267
|
+
type: 'custom',
|
|
1268
|
+
options: {
|
|
1269
|
+
validate: (value: unknown) => value === 'ok',
|
|
1270
|
+
warningMessage: 'Un avertissement',
|
|
1271
|
+
isWarning: true,
|
|
1272
|
+
},
|
|
1273
|
+
},
|
|
1274
|
+
]),
|
|
1275
|
+
})
|
|
1276
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1277
|
+
|
|
1278
|
+
await result.validate()
|
|
1279
|
+
expect(result.warnings.value).toContain('Un avertissement')
|
|
1280
|
+
expect(result.hasWarning.value).toBe(true)
|
|
1281
|
+
})
|
|
1282
|
+
|
|
1283
|
+
it('hasWarning is true when hasWarningProp is true', async () => {
|
|
1284
|
+
const params = makeParams({ hasWarningProp: ref(true) })
|
|
1285
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1286
|
+
|
|
1287
|
+
expect(result.hasWarning.value).toBe(true)
|
|
1288
|
+
})
|
|
1289
|
+
|
|
1290
|
+
it('hasSuccess is true when successes are non-empty and no errors or warnings', async () => {
|
|
1291
|
+
const params = makeParams({
|
|
1292
|
+
modelValue: ref('ok'),
|
|
1293
|
+
customRules: ref([]),
|
|
1294
|
+
customSuccessRules: ref([
|
|
1295
|
+
{
|
|
1296
|
+
type: 'custom',
|
|
1297
|
+
options: {
|
|
1298
|
+
validate: (value: unknown) => value === 'ok',
|
|
1299
|
+
successMessage: 'Succès',
|
|
1300
|
+
},
|
|
1301
|
+
},
|
|
1302
|
+
]),
|
|
1303
|
+
})
|
|
1304
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1305
|
+
|
|
1306
|
+
await result.validate()
|
|
1307
|
+
expect(result.successes.value).toContain('Succès')
|
|
1308
|
+
expect(result.hasSuccess.value).toBe(true)
|
|
1309
|
+
})
|
|
1310
|
+
|
|
1311
|
+
it('hasSuccess is falsy when there are both successes and errors', async () => {
|
|
1312
|
+
const params = makeParams({
|
|
1313
|
+
modelValue: ref('ok'),
|
|
1314
|
+
hasErrorProp: ref(true),
|
|
1315
|
+
customRules: ref([]),
|
|
1316
|
+
customSuccessRules: ref([
|
|
1317
|
+
{
|
|
1318
|
+
type: 'custom',
|
|
1319
|
+
options: {
|
|
1320
|
+
validate: (value: unknown) => value === 'ok',
|
|
1321
|
+
successMessage: 'Succès',
|
|
1322
|
+
},
|
|
1323
|
+
},
|
|
1324
|
+
]),
|
|
1325
|
+
})
|
|
1326
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1327
|
+
|
|
1328
|
+
await result.validate()
|
|
1329
|
+
expect(result.successes.value).toContain('Succès')
|
|
1330
|
+
expect(result.hasError.value).toBe(true)
|
|
1331
|
+
expect(result.hasSuccess.value).toBeFalsy()
|
|
1332
|
+
})
|
|
1333
|
+
|
|
1334
|
+
it('hasSuccess is true when hasSuccessProp is true and no errors/warnings', async () => {
|
|
1335
|
+
const params = makeParams({ hasSuccessProp: ref(true) })
|
|
1336
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1337
|
+
|
|
1338
|
+
expect(result.hasSuccess.value).toBe(true)
|
|
1339
|
+
})
|
|
1340
|
+
})
|
|
1341
|
+
|
|
1342
|
+
describe('auto-revalidation on config change', () => {
|
|
1343
|
+
it('auto-revalidates when customRules change and field is dirty', async () => {
|
|
1344
|
+
const params = makeParams({
|
|
1345
|
+
modelValue: ref('hello'),
|
|
1346
|
+
customRules: ref<ValidationRule[]>([{ type: 'required', options: { message: 'Requis' } }]),
|
|
1347
|
+
})
|
|
1348
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1349
|
+
|
|
1350
|
+
// Make the field dirty
|
|
1351
|
+
await result.validate()
|
|
1352
|
+
expect(result.errors.value).toEqual([])
|
|
1353
|
+
|
|
1354
|
+
// Change rules to one the value doesn't satisfy
|
|
1355
|
+
params.customRules.value = [{
|
|
1356
|
+
type: 'minLength',
|
|
1357
|
+
options: { length: 20, message: 'Trop court' },
|
|
1358
|
+
}]
|
|
1359
|
+
await nextTick()
|
|
1360
|
+
|
|
1361
|
+
// Should auto-validate since the field was dirty
|
|
1362
|
+
expect(result.errors.value.length).toBeGreaterThan(0)
|
|
1363
|
+
})
|
|
1364
|
+
|
|
1365
|
+
it('auto-revalidates when disableErrorHandling changes and field is dirty', async () => {
|
|
1366
|
+
const params = makeParams({
|
|
1367
|
+
modelValue: ref(''),
|
|
1368
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis' } }]),
|
|
1369
|
+
})
|
|
1370
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1371
|
+
|
|
1372
|
+
await result.validate()
|
|
1373
|
+
expect(result.errors.value).toContain('Requis')
|
|
1374
|
+
|
|
1375
|
+
params.disableErrorHandling.value = true
|
|
1376
|
+
await nextTick()
|
|
1377
|
+
|
|
1378
|
+
// Should auto-clear errors
|
|
1379
|
+
expect(result.errors.value).toEqual([])
|
|
1380
|
+
})
|
|
1381
|
+
|
|
1382
|
+
it('auto-revalidates when showSuccessMessages changes and field is dirty', async () => {
|
|
1383
|
+
const params = makeParams({
|
|
1384
|
+
modelValue: ref('valid'),
|
|
1385
|
+
customRules: ref([]),
|
|
1386
|
+
})
|
|
1387
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1388
|
+
|
|
1389
|
+
await result.validate()
|
|
1390
|
+
expect(result.successes.value.length).toBeGreaterThan(0)
|
|
1391
|
+
|
|
1392
|
+
params.showSuccessMessages.value = false
|
|
1393
|
+
await nextTick()
|
|
1394
|
+
|
|
1395
|
+
expect(result.successes.value).toEqual([])
|
|
1396
|
+
})
|
|
1397
|
+
|
|
1398
|
+
it('does not auto-revalidate when field is not dirty', async () => {
|
|
1399
|
+
const params = makeParams({
|
|
1400
|
+
modelValue: ref(''),
|
|
1401
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis' } }]),
|
|
1402
|
+
})
|
|
1403
|
+
withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1404
|
+
|
|
1405
|
+
// Don't validate — field is pristine
|
|
1406
|
+
expect(result => result).toBeDefined()
|
|
1407
|
+
|
|
1408
|
+
params.customRules.value = [{
|
|
1409
|
+
type: 'minLength',
|
|
1410
|
+
options: { length: 20, message: 'Trop court' },
|
|
1411
|
+
}]
|
|
1412
|
+
await nextTick()
|
|
1413
|
+
|
|
1414
|
+
// Should NOT auto-validate
|
|
1415
|
+
expect(params.modelValue.value).toBe('')
|
|
1416
|
+
})
|
|
1417
|
+
|
|
1418
|
+
it('auto-revalidates when label changes and field is dirty', async () => {
|
|
1419
|
+
const params = makeParams({
|
|
1420
|
+
modelValue: ref('valid'),
|
|
1421
|
+
customRules: ref([]),
|
|
1422
|
+
})
|
|
1423
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1424
|
+
|
|
1425
|
+
await result.validate()
|
|
1426
|
+
expect(result.successes.value.some((s: string) => s.includes('Mon champ'))).toBe(true)
|
|
1427
|
+
|
|
1428
|
+
params.label.value = 'Nouveau label'
|
|
1429
|
+
await nextTick()
|
|
1430
|
+
|
|
1431
|
+
expect(result.successes.value.some((s: string) => s.includes('Nouveau label'))).toBe(true)
|
|
1432
|
+
})
|
|
1433
|
+
})
|
|
1434
|
+
|
|
1435
|
+
describe('deduplication and message merging', () => {
|
|
1436
|
+
it('deduplicates errors when same message comes from rules and errorMessages', async () => {
|
|
1437
|
+
const params = makeParams({
|
|
1438
|
+
modelValue: ref(''),
|
|
1439
|
+
customRules: ref([{ type: 'required', options: { message: 'Requis' } }]),
|
|
1440
|
+
errorMessages: ref<string[] | null>(['Requis']),
|
|
1441
|
+
})
|
|
1442
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1443
|
+
|
|
1444
|
+
await result.validate()
|
|
1445
|
+
const requis = result.errors.value.filter((e: string) => e === 'Requis')
|
|
1446
|
+
expect(requis).toHaveLength(1)
|
|
1447
|
+
})
|
|
1448
|
+
|
|
1449
|
+
it('merges distinct errors from rules and errorMessages', async () => {
|
|
1450
|
+
const params = makeParams({
|
|
1451
|
+
modelValue: ref(''),
|
|
1452
|
+
customRules: ref([{ type: 'required', options: { message: 'Champ requis' } }]),
|
|
1453
|
+
errorMessages: ref<string[] | null>(['Erreur externe']),
|
|
1454
|
+
})
|
|
1455
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1456
|
+
|
|
1457
|
+
await result.validate()
|
|
1458
|
+
expect(result.errors.value).toContain('Champ requis')
|
|
1459
|
+
expect(result.errors.value).toContain('Erreur externe')
|
|
1460
|
+
})
|
|
1461
|
+
|
|
1462
|
+
it('merges distinct warnings from rules and warningMessages', async () => {
|
|
1463
|
+
const params = makeParams({
|
|
1464
|
+
modelValue: ref('ko'),
|
|
1465
|
+
customRules: ref([]),
|
|
1466
|
+
customWarningRules: ref([{
|
|
1467
|
+
type: 'custom',
|
|
1468
|
+
options: {
|
|
1469
|
+
validate: (value: unknown) => value === 'ok',
|
|
1470
|
+
warningMessage: 'Warning interne',
|
|
1471
|
+
isWarning: true,
|
|
1472
|
+
},
|
|
1473
|
+
}]),
|
|
1474
|
+
warningMessages: ref<string[] | null>(['Warning externe']),
|
|
1475
|
+
})
|
|
1476
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1477
|
+
|
|
1478
|
+
await result.validate()
|
|
1479
|
+
expect(result.warnings.value).toContain('Warning interne')
|
|
1480
|
+
expect(result.warnings.value).toContain('Warning externe')
|
|
1481
|
+
})
|
|
1482
|
+
|
|
1483
|
+
it('keeps external successMessages even when showSuccessMessages is false', async () => {
|
|
1484
|
+
const params = makeParams({
|
|
1485
|
+
modelValue: ref('valid'),
|
|
1486
|
+
showSuccessMessages: ref(false),
|
|
1487
|
+
customRules: ref([]),
|
|
1488
|
+
successMessages: ref<string[] | null>(['Succès externe']),
|
|
1489
|
+
})
|
|
1490
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1491
|
+
|
|
1492
|
+
await result.validate()
|
|
1493
|
+
expect(result.successes.value).toContain('Succès externe')
|
|
1494
|
+
})
|
|
1495
|
+
|
|
1496
|
+
it('hides inner successes but keeps external successMessages when showSuccessMessages is false', async () => {
|
|
1497
|
+
const params = makeParams({
|
|
1498
|
+
modelValue: ref('ok'),
|
|
1499
|
+
showSuccessMessages: ref(false),
|
|
1500
|
+
customRules: ref([]),
|
|
1501
|
+
customSuccessRules: ref([{
|
|
1502
|
+
type: 'custom',
|
|
1503
|
+
options: {
|
|
1504
|
+
validate: (value: unknown) => value === 'ok',
|
|
1505
|
+
successMessage: 'Succès interne',
|
|
1506
|
+
},
|
|
1507
|
+
}]),
|
|
1508
|
+
successMessages: ref<string[] | null>(['Succès externe']),
|
|
1509
|
+
})
|
|
1510
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1511
|
+
|
|
1512
|
+
await result.validate()
|
|
1513
|
+
expect(result.successes.value).not.toContain('Succès interne')
|
|
1514
|
+
expect(result.successes.value).toContain('Succès externe')
|
|
1515
|
+
})
|
|
1516
|
+
})
|
|
1517
|
+
|
|
1518
|
+
describe('rules concurrences', () => {
|
|
1519
|
+
it('last validate() call wins when multiple calls are made rapidly', async () => {
|
|
1520
|
+
let resolveFirst!: (v: boolean) => void
|
|
1521
|
+
let resolveSecond!: (v: boolean) => void
|
|
1522
|
+
|
|
1523
|
+
const firstRule = {
|
|
1524
|
+
type: 'custom',
|
|
1525
|
+
options: {
|
|
1526
|
+
validate: () => new Promise<boolean>((resolve) => { resolveFirst = resolve }),
|
|
1527
|
+
message: 'Erreur première',
|
|
1528
|
+
},
|
|
1529
|
+
}
|
|
1530
|
+
const secondRule = {
|
|
1531
|
+
type: 'custom',
|
|
1532
|
+
options: {
|
|
1533
|
+
validate: () => new Promise<boolean>((resolve) => { resolveSecond = resolve }),
|
|
1534
|
+
message: 'Erreur seconde',
|
|
1535
|
+
},
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
const customRules = ref<ValidationRule[]>([firstRule])
|
|
1539
|
+
const params = makeParams({
|
|
1540
|
+
modelValue: ref('test'),
|
|
1541
|
+
customRules,
|
|
1542
|
+
})
|
|
1543
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1544
|
+
|
|
1545
|
+
// First validation call (with slow-resolving rule)
|
|
1546
|
+
const firstPromise = result.validate()
|
|
1547
|
+
|
|
1548
|
+
// Swap to a different rule and trigger second validation before first resolves
|
|
1549
|
+
customRules.value = [secondRule]
|
|
1550
|
+
const secondPromise = result.validate()
|
|
1551
|
+
|
|
1552
|
+
// Resolve second first with failure (it should be the one that matters)
|
|
1553
|
+
resolveSecond(false)
|
|
1554
|
+
await secondPromise
|
|
1555
|
+
|
|
1556
|
+
expect(result.errors.value).toContain('Erreur seconde')
|
|
1557
|
+
|
|
1558
|
+
// Now resolve first with failure (stale — should be discarded)
|
|
1559
|
+
resolveFirst(false)
|
|
1560
|
+
await firstPromise
|
|
1561
|
+
|
|
1562
|
+
// The stale first result should NOT overwrite the second
|
|
1563
|
+
expect(result.errors.value).toContain('Erreur seconde')
|
|
1564
|
+
expect(result.errors.value).not.toContain('Erreur première')
|
|
1565
|
+
})
|
|
1566
|
+
|
|
1567
|
+
it('concurrent validate() calls with different model values keep only the latest result', async () => {
|
|
1568
|
+
const resolvers: Array<(v: boolean) => void> = []
|
|
1569
|
+
let callIndex = 0
|
|
1570
|
+
|
|
1571
|
+
const params = makeParams({
|
|
1572
|
+
modelValue: ref<unknown>('a'),
|
|
1573
|
+
customRules: ref<ValidationRule[]>([{
|
|
1574
|
+
type: 'custom',
|
|
1575
|
+
options: {
|
|
1576
|
+
validate: () => {
|
|
1577
|
+
const idx = callIndex++
|
|
1578
|
+
return new Promise<boolean>((resolve) => {
|
|
1579
|
+
resolvers[idx] = resolve
|
|
1580
|
+
})
|
|
1581
|
+
},
|
|
1582
|
+
message: 'Erreur validation',
|
|
1583
|
+
},
|
|
1584
|
+
}]),
|
|
1585
|
+
})
|
|
1586
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1587
|
+
|
|
1588
|
+
// Trigger first validation with modelValue 'a'
|
|
1589
|
+
const p1 = result.validate()
|
|
1590
|
+
|
|
1591
|
+
// Change model value and trigger second validation
|
|
1592
|
+
params.modelValue.value = 'b'
|
|
1593
|
+
const p2 = result.validate()
|
|
1594
|
+
|
|
1595
|
+
// Change model value again and trigger third validation
|
|
1596
|
+
params.modelValue.value = 'c'
|
|
1597
|
+
const p3 = result.validate()
|
|
1598
|
+
|
|
1599
|
+
// Resolve out of order: third fails, first fails, second fails
|
|
1600
|
+
resolvers[2]!(false)
|
|
1601
|
+
resolvers[0]!(false)
|
|
1602
|
+
resolvers[1]!(false)
|
|
1603
|
+
|
|
1604
|
+
await Promise.all([p1, p2, p3])
|
|
1605
|
+
|
|
1606
|
+
// Only the last validation (third) should populate errors
|
|
1607
|
+
expect(result.errors.value).toContain('Erreur validation')
|
|
1608
|
+
// Stale results should have been discarded (only 1 error from the latest call)
|
|
1609
|
+
expect(result.errors.value).toHaveLength(1)
|
|
1610
|
+
})
|
|
1611
|
+
|
|
1612
|
+
it('multiple async error rules on the same validation collect all errors', async () => {
|
|
1613
|
+
const params = makeParams({
|
|
1614
|
+
modelValue: ref('bad'),
|
|
1615
|
+
customRules: ref<ValidationRule[]>([
|
|
1616
|
+
{
|
|
1617
|
+
type: 'custom',
|
|
1618
|
+
options: {
|
|
1619
|
+
validate: async () => false,
|
|
1620
|
+
message: 'Erreur async 1',
|
|
1621
|
+
},
|
|
1622
|
+
},
|
|
1623
|
+
{
|
|
1624
|
+
type: 'custom',
|
|
1625
|
+
options: {
|
|
1626
|
+
validate: async () => false,
|
|
1627
|
+
message: 'Erreur async 2',
|
|
1628
|
+
},
|
|
1629
|
+
},
|
|
1630
|
+
{
|
|
1631
|
+
type: 'custom',
|
|
1632
|
+
options: {
|
|
1633
|
+
validate: async () => false,
|
|
1634
|
+
message: 'Erreur async 3',
|
|
1635
|
+
},
|
|
1636
|
+
},
|
|
1637
|
+
]),
|
|
1638
|
+
})
|
|
1639
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1640
|
+
|
|
1641
|
+
const valid = await result.validate()
|
|
1642
|
+
expect(valid).toBe(false)
|
|
1643
|
+
expect(result.errors.value).toContain('Erreur async 1')
|
|
1644
|
+
expect(result.errors.value).toContain('Erreur async 2')
|
|
1645
|
+
expect(result.errors.value).toContain('Erreur async 3')
|
|
1646
|
+
})
|
|
1647
|
+
|
|
1648
|
+
it('concurrent error, warning, and success rules resolve without interference', async () => {
|
|
1649
|
+
const params = makeParams({
|
|
1650
|
+
modelValue: ref('ok'),
|
|
1651
|
+
customRules: ref<ValidationRule[]>([
|
|
1652
|
+
{
|
|
1653
|
+
type: 'custom',
|
|
1654
|
+
options: {
|
|
1655
|
+
validate: async (v: unknown) => v === 'ok',
|
|
1656
|
+
message: 'Erreur concurrente',
|
|
1657
|
+
},
|
|
1658
|
+
},
|
|
1659
|
+
]),
|
|
1660
|
+
customWarningRules: ref<ValidationRule[]>([
|
|
1661
|
+
{
|
|
1662
|
+
type: 'custom',
|
|
1663
|
+
options: {
|
|
1664
|
+
validate: async () => false,
|
|
1665
|
+
warningMessage: 'Warning concurrent',
|
|
1666
|
+
isWarning: true,
|
|
1667
|
+
},
|
|
1668
|
+
},
|
|
1669
|
+
]),
|
|
1670
|
+
customSuccessRules: ref<ValidationRule[]>([
|
|
1671
|
+
{
|
|
1672
|
+
type: 'custom',
|
|
1673
|
+
options: {
|
|
1674
|
+
validate: async (v: unknown) => v === 'ok',
|
|
1675
|
+
successMessage: 'Succès concurrent',
|
|
1676
|
+
},
|
|
1677
|
+
},
|
|
1678
|
+
]),
|
|
1679
|
+
})
|
|
1680
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1681
|
+
|
|
1682
|
+
const valid = await result.validate()
|
|
1683
|
+
|
|
1684
|
+
// No error since value is 'ok'
|
|
1685
|
+
expect(valid).toBe(true)
|
|
1686
|
+
expect(result.errors.value).toEqual([])
|
|
1687
|
+
|
|
1688
|
+
// Warnings are populated independently
|
|
1689
|
+
expect(result.warnings.value).toContain('Warning concurrent')
|
|
1690
|
+
|
|
1691
|
+
// Success is suppressed because warnings exist
|
|
1692
|
+
expect(result.hasSuccess.value).toBeFalsy()
|
|
1693
|
+
})
|
|
1694
|
+
|
|
1695
|
+
it('mixed sync and async rules on the same field work correctly together', async () => {
|
|
1696
|
+
const params = makeParams({
|
|
1697
|
+
modelValue: ref('ab'),
|
|
1698
|
+
customRules: ref<ValidationRule[]>([
|
|
1699
|
+
{
|
|
1700
|
+
type: 'minLength',
|
|
1701
|
+
options: { length: 5, message: 'Trop court sync' },
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
type: 'custom',
|
|
1705
|
+
options: {
|
|
1706
|
+
validate: async (v: unknown) => String(v).length >= 10,
|
|
1707
|
+
message: 'Trop court async',
|
|
1708
|
+
},
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
type: 'custom',
|
|
1712
|
+
options: {
|
|
1713
|
+
validate: async (v: unknown) => String(v).length >= 1,
|
|
1714
|
+
message: 'Pas d\'erreur async',
|
|
1715
|
+
},
|
|
1716
|
+
},
|
|
1717
|
+
]),
|
|
1718
|
+
})
|
|
1719
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1720
|
+
|
|
1721
|
+
const valid = await result.validate()
|
|
1722
|
+
expect(valid).toBe(false)
|
|
1723
|
+
expect(result.errors.value).toContain('Trop court sync')
|
|
1724
|
+
expect(result.errors.value).toContain('Trop court async')
|
|
1725
|
+
})
|
|
1726
|
+
|
|
1727
|
+
it('rapid re-validations on input change discard intermediate stale results', async () => {
|
|
1728
|
+
const resolverQueue: Array<(v: boolean) => void> = []
|
|
1729
|
+
|
|
1730
|
+
const params = makeParams({
|
|
1731
|
+
modelValue: ref<unknown>(''),
|
|
1732
|
+
isValidateOnBlur: ref(false),
|
|
1733
|
+
customRules: ref<ValidationRule[]>([{
|
|
1734
|
+
type: 'custom',
|
|
1735
|
+
options: {
|
|
1736
|
+
validate: () => new Promise<boolean>((resolve) => { resolverQueue.push(resolve) }),
|
|
1737
|
+
message: 'Erreur stale',
|
|
1738
|
+
},
|
|
1739
|
+
}]),
|
|
1740
|
+
})
|
|
1741
|
+
|
|
1742
|
+
let result!: ReturnType<typeof useValidation>
|
|
1743
|
+
const wrapper = mount(defineComponent({
|
|
1744
|
+
setup() {
|
|
1745
|
+
result = useValidation(params as Parameters<typeof useValidation>[0])
|
|
1746
|
+
const onInput = (event: Event) => {
|
|
1747
|
+
params.modelValue.value = (event.target as HTMLInputElement).value
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
return { modelValue: params.modelValue, onInput }
|
|
1751
|
+
},
|
|
1752
|
+
template: `<input data-test="field" :value="modelValue" @input="onInput">`,
|
|
1753
|
+
}))
|
|
1754
|
+
|
|
1755
|
+
// Rapid sequential input changes
|
|
1756
|
+
await wrapper.get('[data-test="field"]').setValue('a')
|
|
1757
|
+
await nextTick()
|
|
1758
|
+
await wrapper.get('[data-test="field"]').setValue('ab')
|
|
1759
|
+
await nextTick()
|
|
1760
|
+
await wrapper.get('[data-test="field"]').setValue('abc')
|
|
1761
|
+
await nextTick()
|
|
1762
|
+
|
|
1763
|
+
// Resolve all pending validators
|
|
1764
|
+
resolverQueue.forEach((resolve, i) => {
|
|
1765
|
+
if (i < resolverQueue.length - 1) {
|
|
1766
|
+
resolve(false) // Stale intermediate results fail
|
|
1767
|
+
}
|
|
1768
|
+
else {
|
|
1769
|
+
resolve(true) // Last one passes
|
|
1770
|
+
}
|
|
1771
|
+
})
|
|
1772
|
+
|
|
1773
|
+
await nextTick()
|
|
1774
|
+
await nextTick()
|
|
1775
|
+
|
|
1776
|
+
// Only the latest validation result should matter
|
|
1777
|
+
expect(result.errors.value).toEqual([])
|
|
1778
|
+
|
|
1779
|
+
wrapper.unmount()
|
|
1780
|
+
})
|
|
1781
|
+
|
|
1782
|
+
it('validate() called while previous async validation is pending replaces its results', async () => {
|
|
1783
|
+
let slowResolve!: (v: boolean) => void
|
|
1784
|
+
|
|
1785
|
+
const params = makeParams({
|
|
1786
|
+
modelValue: ref('test'),
|
|
1787
|
+
customRules: ref<ValidationRule[]>([{
|
|
1788
|
+
type: 'custom',
|
|
1789
|
+
options: {
|
|
1790
|
+
validate: () => new Promise<boolean>((resolve) => { slowResolve = resolve }),
|
|
1791
|
+
message: 'Erreur lente obsolète',
|
|
1792
|
+
},
|
|
1793
|
+
}]),
|
|
1794
|
+
})
|
|
1795
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1796
|
+
|
|
1797
|
+
// Start slow async validation
|
|
1798
|
+
const slowPromise = result.validate()
|
|
1799
|
+
|
|
1800
|
+
// Immediately swap to a sync rule and re-validate
|
|
1801
|
+
params.customRules.value = [{
|
|
1802
|
+
type: 'custom',
|
|
1803
|
+
options: {
|
|
1804
|
+
validate: () => false,
|
|
1805
|
+
message: 'Erreur sync immédiate',
|
|
1806
|
+
},
|
|
1807
|
+
}]
|
|
1808
|
+
const fastResult = await result.validate()
|
|
1809
|
+
|
|
1810
|
+
expect(fastResult).toBe(false)
|
|
1811
|
+
expect(result.errors.value).toContain('Erreur sync immédiate')
|
|
1812
|
+
|
|
1813
|
+
// Resolve slow async — should be discarded
|
|
1814
|
+
slowResolve(false)
|
|
1815
|
+
await slowPromise
|
|
1816
|
+
|
|
1817
|
+
expect(result.errors.value).toContain('Erreur sync immédiate')
|
|
1818
|
+
expect(result.errors.value).not.toContain('Erreur lente obsolète')
|
|
1819
|
+
})
|
|
1820
|
+
|
|
1821
|
+
it('concurrent Vuetify validations with rapid calls keep only the latest model state', async () => {
|
|
1822
|
+
const params = {
|
|
1823
|
+
modelValue: ref<unknown>(''),
|
|
1824
|
+
readonly: ref(false),
|
|
1825
|
+
disabled: ref(false),
|
|
1826
|
+
required: ref(false),
|
|
1827
|
+
isValidateOnBlur: ref(true),
|
|
1828
|
+
showSuccessMessages: ref(true),
|
|
1829
|
+
disableErrorHandling: ref(false),
|
|
1830
|
+
label: ref('Mon champ'),
|
|
1831
|
+
focused: ref(false),
|
|
1832
|
+
useVuetifyValidation: true as const,
|
|
1833
|
+
rules: ref([(v: unknown) => !!v || 'Requis Vuetify concurrent']),
|
|
1834
|
+
maxErrors: ref(1),
|
|
1835
|
+
}
|
|
1836
|
+
const { result } = withSetup(() => useValidation(params))
|
|
1837
|
+
|
|
1838
|
+
// First call — modelValue is empty, should fail
|
|
1839
|
+
const p1 = result.validate()
|
|
1840
|
+
await p1
|
|
1841
|
+
expect(result.errors.value).toContain('Requis Vuetify concurrent')
|
|
1842
|
+
|
|
1843
|
+
// Update model value and validate again
|
|
1844
|
+
params.modelValue.value = 'ok'
|
|
1845
|
+
const p2 = result.validate()
|
|
1846
|
+
await p2
|
|
1847
|
+
|
|
1848
|
+
// Latest result should reflect the valid state
|
|
1849
|
+
expect(result.errors.value).toEqual([])
|
|
1850
|
+
expect(result.hasError.value).toBeFalsy()
|
|
1851
|
+
})
|
|
1852
|
+
|
|
1853
|
+
it('async error rules with varying delays all contribute to the same validation', async () => {
|
|
1854
|
+
const params = makeParams({
|
|
1855
|
+
modelValue: ref('bad'),
|
|
1856
|
+
customRules: ref<ValidationRule[]>([
|
|
1857
|
+
{
|
|
1858
|
+
type: 'custom',
|
|
1859
|
+
options: {
|
|
1860
|
+
validate: () => new Promise<boolean>(resolve => setTimeout(() => resolve(false), 10)),
|
|
1861
|
+
message: 'Erreur rapide',
|
|
1862
|
+
},
|
|
1863
|
+
},
|
|
1864
|
+
{
|
|
1865
|
+
type: 'custom',
|
|
1866
|
+
options: {
|
|
1867
|
+
validate: () => new Promise<boolean>(resolve => setTimeout(() => resolve(false), 50)),
|
|
1868
|
+
message: 'Erreur lente',
|
|
1869
|
+
},
|
|
1870
|
+
},
|
|
1871
|
+
]),
|
|
1872
|
+
})
|
|
1873
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1874
|
+
|
|
1875
|
+
const valid = await result.validate()
|
|
1876
|
+
expect(valid).toBe(false)
|
|
1877
|
+
expect(result.errors.value).toContain('Erreur rapide')
|
|
1878
|
+
expect(result.errors.value).toContain('Erreur lente')
|
|
1879
|
+
})
|
|
1880
|
+
|
|
1881
|
+
it('concurrent async warning and success rules resolve correctly when no errors', async () => {
|
|
1882
|
+
const params = makeParams({
|
|
1883
|
+
modelValue: ref('ok'),
|
|
1884
|
+
customRules: ref<ValidationRule[]>([]),
|
|
1885
|
+
customWarningRules: ref<ValidationRule[]>([
|
|
1886
|
+
{
|
|
1887
|
+
type: 'custom',
|
|
1888
|
+
options: {
|
|
1889
|
+
validate: async (v: unknown) => v === 'perfect',
|
|
1890
|
+
warningMessage: 'Attention',
|
|
1891
|
+
isWarning: true,
|
|
1892
|
+
},
|
|
1893
|
+
},
|
|
1894
|
+
]),
|
|
1895
|
+
customSuccessRules: ref<ValidationRule[]>([
|
|
1896
|
+
{
|
|
1897
|
+
type: 'custom',
|
|
1898
|
+
options: {
|
|
1899
|
+
validate: async (v: unknown) => v === 'ok',
|
|
1900
|
+
successMessage: 'Bravo',
|
|
1901
|
+
},
|
|
1902
|
+
},
|
|
1903
|
+
]),
|
|
1904
|
+
})
|
|
1905
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1906
|
+
|
|
1907
|
+
await result.validate()
|
|
1908
|
+
|
|
1909
|
+
expect(result.errors.value).toEqual([])
|
|
1910
|
+
expect(result.warnings.value).toContain('Attention')
|
|
1911
|
+
// Success suppressed by warning presence
|
|
1912
|
+
expect(result.hasSuccess.value).toBeFalsy()
|
|
1913
|
+
})
|
|
1914
|
+
|
|
1915
|
+
it('validate() returns true immediately when readonly is toggled, even with a pending async rule', async () => {
|
|
1916
|
+
let resolve!: (v: boolean) => void
|
|
1917
|
+
|
|
1918
|
+
const params = makeParams({
|
|
1919
|
+
modelValue: ref('some value'),
|
|
1920
|
+
customRules: ref<ValidationRule[]>([{
|
|
1921
|
+
type: 'custom',
|
|
1922
|
+
options: {
|
|
1923
|
+
validate: () => new Promise<boolean>((r) => { resolve = r }),
|
|
1924
|
+
message: 'Erreur obsolète readonly',
|
|
1925
|
+
},
|
|
1926
|
+
}]),
|
|
1927
|
+
})
|
|
1928
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1929
|
+
|
|
1930
|
+
// Start async validation (modelValue is non-empty so the custom rule runs)
|
|
1931
|
+
const p1 = result.validate()
|
|
1932
|
+
|
|
1933
|
+
// Toggle readonly — next validate() should short-circuit and return true immediately
|
|
1934
|
+
params.readonly.value = true
|
|
1935
|
+
const valid = await result.validate()
|
|
1936
|
+
expect(valid).toBe(true)
|
|
1937
|
+
// Errors are cleared by the readonly short-circuit
|
|
1938
|
+
expect(result.errors.value).toEqual([])
|
|
1939
|
+
|
|
1940
|
+
// Resolve stale async — since the readonly short-circuit does not
|
|
1941
|
+
// invalidate the pending token, the resolved result still writes errors
|
|
1942
|
+
resolve(false)
|
|
1943
|
+
await p1
|
|
1944
|
+
expect(result.errors.value).toContain('Erreur obsolète readonly')
|
|
1945
|
+
})
|
|
1946
|
+
})
|
|
1947
|
+
|
|
1948
|
+
describe('async validation rules that throw errors', () => {
|
|
1949
|
+
it('handles thrown error in async custom rules and uses the custom message', async () => {
|
|
1950
|
+
const params = makeParams({
|
|
1951
|
+
modelValue: ref('test'),
|
|
1952
|
+
customRules: ref([{
|
|
1953
|
+
type: 'custom',
|
|
1954
|
+
options: {
|
|
1955
|
+
validate: async () => {
|
|
1956
|
+
throw new Error('Network error')
|
|
1957
|
+
},
|
|
1958
|
+
message: 'Erreur personnalisée',
|
|
1959
|
+
},
|
|
1960
|
+
}]),
|
|
1961
|
+
})
|
|
1962
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1963
|
+
|
|
1964
|
+
const valid = await result.validate()
|
|
1965
|
+
expect(valid).toBe(false)
|
|
1966
|
+
expect(result.errors.value).toContain('Erreur personnalisée')
|
|
1967
|
+
expect(result.hasError.value).toBe(true)
|
|
1968
|
+
})
|
|
1969
|
+
|
|
1970
|
+
it('uses the thrown error message when no custom message is provided', async () => {
|
|
1971
|
+
const params = makeParams({
|
|
1972
|
+
modelValue: ref('test'),
|
|
1973
|
+
customRules: ref([{
|
|
1974
|
+
type: 'custom',
|
|
1975
|
+
options: {
|
|
1976
|
+
validate: async () => {
|
|
1977
|
+
throw new Error('Service unavailable')
|
|
1978
|
+
},
|
|
1979
|
+
},
|
|
1980
|
+
}]),
|
|
1981
|
+
})
|
|
1982
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
1983
|
+
|
|
1984
|
+
const valid = await result.validate()
|
|
1985
|
+
expect(valid).toBe(false)
|
|
1986
|
+
expect(result.errors.value).toContain('Service unavailable')
|
|
1987
|
+
expect(result.hasError.value).toBe(true)
|
|
1988
|
+
})
|
|
1989
|
+
|
|
1990
|
+
it('handles thrown error in async warning rules', async () => {
|
|
1991
|
+
const params = makeParams({
|
|
1992
|
+
modelValue: ref('test'),
|
|
1993
|
+
customWarningRules: ref([{
|
|
1994
|
+
type: 'custom',
|
|
1995
|
+
options: {
|
|
1996
|
+
validate: async () => {
|
|
1997
|
+
throw new Error('Warning service failed')
|
|
1998
|
+
},
|
|
1999
|
+
isWarning: true,
|
|
2000
|
+
},
|
|
2001
|
+
}]),
|
|
2002
|
+
})
|
|
2003
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
2004
|
+
|
|
2005
|
+
await result.validate()
|
|
2006
|
+
expect(result.warnings.value).toContain('Warning service failed')
|
|
2007
|
+
expect(result.hasWarning.value).toBe(true)
|
|
2008
|
+
})
|
|
2009
|
+
|
|
2010
|
+
it('handles thrown error in async success rules gracefully', async () => {
|
|
2011
|
+
const params = makeParams({
|
|
2012
|
+
modelValue: ref('test'),
|
|
2013
|
+
customSuccessRules: ref([{
|
|
2014
|
+
type: 'custom',
|
|
2015
|
+
options: {
|
|
2016
|
+
validate: async () => {
|
|
2017
|
+
throw new Error('Success check failed')
|
|
2018
|
+
},
|
|
2019
|
+
},
|
|
2020
|
+
}]),
|
|
2021
|
+
})
|
|
2022
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
2023
|
+
|
|
2024
|
+
await result.validate()
|
|
2025
|
+
// The thrown error is caught and does not crash — no validation errors are surfaced
|
|
2026
|
+
expect(result.errors.value).toEqual([])
|
|
2027
|
+
})
|
|
2028
|
+
|
|
2029
|
+
it('handles non-Error thrown values in async rules', async () => {
|
|
2030
|
+
const params = makeParams({
|
|
2031
|
+
modelValue: ref('test'),
|
|
2032
|
+
customRules: ref([{
|
|
2033
|
+
type: 'custom',
|
|
2034
|
+
options: {
|
|
2035
|
+
validate: async () => {
|
|
2036
|
+
throw 'string error'
|
|
2037
|
+
},
|
|
2038
|
+
},
|
|
2039
|
+
}]),
|
|
2040
|
+
})
|
|
2041
|
+
const { result } = withSetup(() => useValidation(params as Parameters<typeof useValidation>[0]))
|
|
2042
|
+
|
|
2043
|
+
const valid = await result.validate()
|
|
2044
|
+
expect(valid).toBe(false)
|
|
2045
|
+
expect(result.errors.value).toContain('string error')
|
|
2046
|
+
})
|
|
2047
|
+
})
|
|
2048
|
+
})
|