import { mount } from '@vue/test-utils' import NirField from '../NirField.vue' import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { useValidation } from '@/composables/validation/useValidation' describe('NirField.vue', () => { let wrapper: ReturnType keyValidation: ReturnType }>> // Tableau pour suivre tous les wrappers créés pendant les tests let activeWrappers: ReturnType[] = [] // Helper pour attendre que les opérations asynchrones soient terminées async function flushPromises() { return new Promise(resolve => setTimeout(resolve, 0)) } beforeEach(async () => { wrapper = mount(NirField, { props: { modelValue: undefined, required: true, showSuccessMessages: true, outlined: true, }, }) // Ajouter le wrapper à la liste pour le démontage activeWrappers.push(wrapper) // Attendre que le montage initial soit terminé await wrapper.vm.$nextTick() await flushPromises() }) afterEach(async () => { // Attendre que toutes les promesses soient résolues avant de démonter await flushPromises() // Démonter tous les composants pour éviter les fuites mémoire for (const wrapper of activeWrappers) { if (wrapper && typeof wrapper.unmount === 'function') { wrapper.unmount() // Attendre après chaque démontage pour permettre le nettoyage await flushPromises() } } activeWrappers = [] // Réinitialiser tous les mocks vi.resetAllMocks() // Attendre une dernière fois pour s'assurer que tout est nettoyé await flushPromises() }) it('renders correctly', async () => { // Attendre que tous les effets asynchrones soient terminés await wrapper.vm.$nextTick() await flushPromises() expect(wrapper.exists()).toBe(true) expect(wrapper.find('.number-field').exists()).toBe(true) expect(wrapper.find('.key-field').exists()).toBe(true) }) it('displays error message for invalid NIR length', async () => { await wrapper.find('.number-field input').trigger('focus') await wrapper.find('.number-field input').setValue('123') await wrapper.vm.$nextTick() await wrapper.find('.number-field input').trigger('blur') await wrapper.vm.$nextTick() await flushPromises() expect(wrapper.vm.numberValidation.errors.value[0]).toBe('Le numéro de sécurité sociale est invalide.') }) it('validates the NIR field successfully', async () => { await wrapper.find('.number-field input').trigger('focus') await wrapper.find('.number-field input').setValue('2940375120005') await wrapper.vm.$nextTick() await wrapper.find('.number-field input').trigger('blur') await wrapper.vm.$nextTick() await flushPromises() expect(wrapper.vm.numberValidation.successes.value).toContain('Le numéro de sécurité sociale est valide.') }) it('displays error message for invalid key length', async () => { await wrapper.find('.number-field input').setValue('2940375120005') await wrapper.vm.$nextTick() await wrapper.find('.key-field input').trigger('focus') await wrapper.find('.key-field input').setValue('1') await wrapper.vm.$nextTick() await wrapper.find('.key-field input').trigger('blur') await wrapper.vm.$nextTick() await flushPromises() expect(wrapper.vm.keyValidation.errors.value[0]).toBe('La clé de contrôle est invalide.') }) it('validates the key field successfully', async () => { await wrapper.find('.number-field input').setValue('2940375120005') await wrapper.vm.$nextTick() await wrapper.find('.key-field input').trigger('focus') await wrapper.find('.key-field input').setValue('91') await wrapper.vm.$nextTick() await wrapper.find('.key-field input').trigger('blur') await wrapper.vm.$nextTick() await flushPromises() expect(wrapper.vm.keyValidation.successes.value).toContain('La clé de contrôle est valide.') }) it('hides the key field when displayKey is false', async () => { await wrapper.setProps({ displayKey: false }) await wrapper.vm.$nextTick() await flushPromises() expect(wrapper.find('.key-field').exists()).toBe(false) }) it('calls validateOnSubmit and returns true if no errors', async () => { const testWrapper = mount(NirField, { props: { modelValue: undefined, required: false, outlined: true, }, }) // Ajouter le wrapper à la liste pour le démontage activeWrappers.push(testWrapper) const numberField = testWrapper.find('.number-field input') await numberField.setValue('2940375120005') await numberField.trigger('blur') await testWrapper.vm.$nextTick() await flushPromises() const isValid = await testWrapper.vm.validateOnSubmit() expect(isValid).toBe(true) }) it('applies custom key rules when provided', async () => { const customKeyRules = [{ type: 'custom', options: { validate: (value: string) => value === '91', message: 'Custom key validation failed.', successMessage: 'Custom key validation passed.', fieldIdentifier: 'clé', }, }] const customWrapper = mount(NirField, { props: { label: 'NIR Field with custom key validation', modelValue: undefined, customKeyRules, showSuccessMessages: true, outlined: true, }, }) activeWrappers.push(customWrapper) await customWrapper.vm.$nextTick() await flushPromises() const numberField = customWrapper.find('.number-field input') const keyField = customWrapper.find('.key-field input') await numberField.setValue('2940375120005') await keyField.trigger('focus') await keyField.setValue('91') await keyField.trigger('blur') await customWrapper.vm.$nextTick() await flushPromises() expect(customWrapper.vm.keyValidation.successes.value).toContain('Custom key validation passed.') }) it('emits update:modelValue with correct format', async () => { const numberField = wrapper.find('.number-field input') const keyField = wrapper.find('.key-field input') await numberField.setValue('2940375120005') await wrapper.vm.$nextTick() await flushPromises() await keyField.setValue('91') await wrapper.vm.$nextTick() await flushPromises() expect(wrapper.emitted('update:modelValue')?.slice(-1)[0]).toEqual(['294037512000591']) }) it('emits undefined when both fields are empty', async () => { const numberField = wrapper.find('.number-field input') const keyField = wrapper.find('.key-field input') await numberField.setValue('') await keyField.setValue('') await wrapper.vm.$nextTick() await flushPromises() expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([undefined]) }) it('splits modelValue correctly when provided', async () => { await wrapper.setProps({ modelValue: '294037512000591' }) await wrapper.vm.$nextTick() await flushPromises() const numberInput = wrapper.find('.number-field input').element as Element & { value: string } const keyInput = wrapper.find('.key-field input').element as Element & { value: string } expect(numberInput.value.replace(/\s/g, '')).toBe('2940375120005') expect(keyInput.value).toBe('91') }) it('applies numberMask correctly with vMaska directive', async () => { // On teste d'abord la saisie normale de chiffres const numberInput = wrapper.find('.number-field input') await numberInput.setValue('294037512000') await wrapper.vm.$nextTick() await flushPromises() // Vérifier que le masque applique correctement les espaces const inputElement = numberInput.element as Element & { value: string } expect(inputElement.value).toBe('2 94 03 75 120 00') // On ajoute un caractère '5' supplémentaire await numberInput.setValue('2940375120005') await wrapper.vm.$nextTick() await flushPromises() expect(inputElement.value).toBe('2 94 03 75 120 005') // Testons le cas où on utilise le caractère spécial 'A' dans le NIR // Plutôt que de tester la valeur formatée exacte (qui peut changer selon l'implémentation), // testons simplement que la valeur contient 'A' et que la valeur sans espaces est celle attendue // Test plus simple avec juste des chiffres pour vérifier que le masque accepte 13 chiffres await numberInput.setValue('1234567891234') await wrapper.vm.$nextTick() await flushPromises() // Vérifier que la valeur masquée contient bien 13 chiffres (sans les espaces) const maskedValue = inputElement.value.replace(/\s/g, '') expect(maskedValue.length).toBe(13) expect(maskedValue).toBe('1234567891234') }) it('applies keyMask correctly with vMaska directive', async () => { // On teste la saisie de la clé (seulement 2 chiffres autorisés) const keyInput = wrapper.find('.key-field input') await keyInput.setValue('9') await wrapper.vm.$nextTick() await flushPromises() // Vérifier qu'un seul chiffre est accepté const inputElement = keyInput.element as Element & { value: string } expect(inputElement.value).toBe('9') // On ajoute un deuxième chiffre await keyInput.setValue('91') await wrapper.vm.$nextTick() await flushPromises() expect(inputElement.value).toBe('91') // On essaie d'ajouter un troisième chiffre await keyInput.setValue('913') await wrapper.vm.$nextTick() await flushPromises() // Vérifier que le troisième chiffre n'est pas accepté expect(inputElement.value).toBe('91') // On essaie d'ajouter une lettre (non autorisée par le masque) await keyInput.setValue('9A') await wrapper.vm.$nextTick() await flushPromises() // Vérifier que la lettre n'est pas acceptée expect(inputElement.value).toBe('9') }) it('automatically focuses key field when number field is complete', async () => { // Spy sur la méthode focus de l'élément input const focusSpy = vi.spyOn(HTMLElement.prototype, 'focus') // On remplit complètement le champ NIR await wrapper.find('.number-field input').setValue('2940375120005') await wrapper.vm.$nextTick() await flushPromises() // Vérifier que le focus a été appelé au moins une fois // (la méthode focusField est appelée et met le focus sur le champ clé) expect(focusSpy).toHaveBeenCalled() // Restaurer le spy focusSpy.mockRestore() }) describe('Internal update flag protection', () => { it('prevents infinite loops between watch and emitValue', async () => { // Spy sur emit pour compter les appels const emitSpy = vi.spyOn(wrapper.vm, '$emit') // Définir une valeur initiale await wrapper.setProps({ modelValue: '294037512000591' }) await wrapper.vm.$nextTick() await flushPromises() // Réinitialiser le spy pour ne compter que les nouveaux appels emitSpy.mockClear() // Simuler une saisie utilisateur qui devrait déclencher emitValue const numberField = wrapper.find('.number-field input') await numberField.setValue('2940375120006') // Changer le dernier chiffre await wrapper.vm.$nextTick() await flushPromises() // Vérifier qu'il n'y a qu'un seul appel à emit (pas de boucle infinie) const updateModelValueCalls = emitSpy.mock.calls.filter(call => call[0] === 'update:modelValue') expect(updateModelValueCalls.length).toBeLessThanOrEqual(2) // Maximum 2 appels (un pour chaque champ) emitSpy.mockRestore() }) it('allows external modelValue changes to update internal fields', async () => { // Définir une valeur initiale await wrapper.setProps({ modelValue: '294037512000591' }) await wrapper.vm.$nextTick() await flushPromises() // Vérifier que les champs internes sont mis à jour const numberInput = wrapper.find('.number-field input').element as HTMLInputElement const keyInput = wrapper.find('.key-field input').element as HTMLInputElement expect(numberInput.value.replace(/\s/g, '')).toBe('2940375120005') expect(keyInput.value).toBe('91') // Changer la valeur externe await wrapper.setProps({ modelValue: '123456789012345' }) await wrapper.vm.$nextTick() await flushPromises() // Vérifier que les champs internes sont mis à jour correctement expect(numberInput.value.replace(/\s/g, '')).toBe('1234567890123') expect(keyInput.value).toBe('45') }) it('handles null/undefined modelValue without triggering loops', async () => { // Spy sur emit pour compter les appels const emitSpy = vi.spyOn(wrapper.vm, '$emit') // Définir une valeur initiale await wrapper.setProps({ modelValue: '294037512000591' }) await wrapper.vm.$nextTick() await flushPromises() // Réinitialiser le spy emitSpy.mockClear() // Changer vers null await wrapper.setProps({ modelValue: null }) await wrapper.vm.$nextTick() await flushPromises() // Vérifier que les champs sont vidés const numberInput = wrapper.find('.number-field input').element as HTMLInputElement const keyInput = wrapper.find('.key-field input').element as HTMLInputElement expect(numberInput.value).toBe('') expect(keyInput.value).toBe('') // Vérifier qu'il n'y a pas d'appels emit supplémentaires (pas de boucle) const updateModelValueCalls = emitSpy.mock.calls.filter(call => call[0] === 'update:modelValue') expect(updateModelValueCalls.length).toBe(0) // Aucun emit car c'est un changement externe emitSpy.mockRestore() }) }) describe('Configuration UI et UX', () => { it('applies disabled state to both inputs', async () => { await wrapper.setProps({ disabled: true }) await wrapper.vm.$nextTick() await flushPromises() const numberInput = wrapper.find('.number-field input') const keyInput = wrapper.find('.key-field input') expect(numberInput.attributes('disabled')).toBeDefined() expect(keyInput.attributes('disabled')).toBeDefined() }) it('applies readonly state to both inputs', async () => { await wrapper.setProps({ readonly: true }) await wrapper.vm.$nextTick() await flushPromises() const numberInput = wrapper.find('.number-field input') const keyInput = wrapper.find('.key-field input') expect(numberInput.attributes('readonly')).toBeDefined() expect(keyInput.attributes('readonly')).toBeDefined() }) it('respects showSuccessMessages prop', async () => { // Par défaut, showSuccessMessages = false await wrapper.setProps({ showSuccessMessages: true }) expect(wrapper.props('showSuccessMessages')).toBe(true) await wrapper.setProps({ showSuccessMessages: false }) await wrapper.vm.$nextTick() await flushPromises() expect(wrapper.props('showSuccessMessages')).toBe(false) }) it('respects disableErrorHandling prop', async () => { await wrapper.setProps({ disableErrorHandling: true }) const numberField = wrapper.find('.number-field input') await numberField.setValue('123') // Invalid NIR await numberField.trigger('blur') await wrapper.vm.$nextTick() await flushPromises() // La validation a lieu, mais disableErrorHandling est appliqué const numberTextField = wrapper.findComponent({ name: 'SyTextField' }) expect(numberTextField.props('disableErrorHandling')).toBe(true) }) it('renders tooltips correctly when provided', async () => { const nirTooltip = 'Tooltip NIR' const keyTooltip = 'Tooltip Clé' await wrapper.setProps({ nirTooltip, keyTooltip, }) await wrapper.vm.$nextTick() await flushPromises() // Les tooltips sont passés aux SyTextField en tant que props 'appendTooltip' (par défaut) const textFields = wrapper.findAllComponents({ name: 'SyTextField' }) expect(textFields[0]?.props('appendTooltip')).toBe(nirTooltip) expect(textFields[1]?.props('appendTooltip')).toBe(keyTooltip) }) it('affiche les hints internes (numberHint/keyHint) quand persistentHint est true', async () => { const w = mount(NirField, { props: { label: 'Identifiant', numberHint: 'Indice numéro', keyHint: 'Indice clé', persistentHint: true, }, }) activeWrappers.push(w) await w.vm.$nextTick() await flushPromises() expect(w.text()).toContain('Indice numéro') expect(w.text()).toContain('Indice clé') }) it('renders asterisks correctly when displayAsterisk is true AND required is true', async () => { // L'astérisque n'est affiché que si required = true ET displayAsterisk = true await wrapper.setProps({ required: true, displayAsterisk: true, numberLabel: 'Numéro', keyLabel: 'Clé' }) await wrapper.vm.$nextTick() await flushPromises() // Dans NirField.vue, l'astérisque est ajouté directement à la string 'label' // transmise aux composants enfants const textFields = wrapper.findAllComponents({ name: 'SyTextField' }) expect(textFields[0]?.props('label')).toBe('Numéro *') expect(textFields[1]?.props('label')).toBe('Clé *') // Si on désactive l'astérisque await wrapper.setProps({ displayAsterisk: false }) await wrapper.vm.$nextTick() await flushPromises() expect(textFields[0]?.props('label')).toBe('Numéro') expect(textFields[1]?.props('label')).toBe('Clé') }) it('removes fieldset when withoutFieldset is true', async () => { // Par défaut, le fieldset est présent quand displayKey est true expect(wrapper.find('fieldset').exists()).toBe(true) await wrapper.setProps({ withoutFieldset: true }) await wrapper.vm.$nextTick() await flushPromises() // Le fieldset ne doit plus être présent expect(wrapper.find('fieldset').exists()).toBe(false) // Mais les champs doivent toujours être là expect(wrapper.find('.number-field').exists()).toBe(true) expect(wrapper.find('.key-field').exists()).toBe(true) }) }) describe('Cursor position preservation when displayKey=false', () => { let wrapperWithoutKey: ReturnType> beforeEach(async () => { wrapperWithoutKey = mount(NirField, { props: { modelValue: undefined, displayKey: false, required: false, outlined: true, }, }) activeWrappers.push(wrapperWithoutKey) await wrapperWithoutKey.vm.$nextTick() await flushPromises() }) it('does not add our custom keydown event listener when displayKey is false', async () => { // Spy spécifiquement sur notre fonction handleNumberKeydown const handleNumberKeydownSpy = vi.spyOn(HTMLElement.prototype, 'addEventListener') // Remonter le composant pour déclencher onMounted wrapperWithoutKey.unmount() activeWrappers.pop() const newWrapper = mount(NirField, { props: { displayKey: false, required: false, }, }) activeWrappers.push(newWrapper) await newWrapper.vm.$nextTick() await flushPromises() // Vérifier que notre logique onMounted n'a pas ajouté d'écouteur keydown // (d'autres composants peuvent en ajouter, mais pas notre logique spécifique) const ourKeydownCalls = handleNumberKeydownSpy.mock.calls.filter(call => call[0] === 'keydown' && call[1] && call[1].toString().includes('handleNumberKeydown'), ) expect(ourKeydownCalls).toHaveLength(0) handleNumberKeydownSpy.mockRestore() }) it('does not trigger focus when editing NIR without key field', async () => { // Spy sur la méthode focus const focusSpy = vi.spyOn(HTMLElement.prototype, 'focus') // Saisir un NIR complet const numberInput = wrapperWithoutKey.find('.number-field input') await numberInput.setValue('2940375120005') await wrapperWithoutKey.vm.$nextTick() await flushPromises() // Réinitialiser le spy pour ne compter que les appels suivants focusSpy.mockClear() // Modifier un chiffre au milieu (simuler l'édition) await numberInput.setValue('2940375120006') await wrapperWithoutKey.vm.$nextTick() await flushPromises() // Vérifier qu'aucun focus n'a été déclenché lors de l'édition expect(focusSpy).not.toHaveBeenCalled() focusSpy.mockRestore() }) it('watchers do not execute when displayKey is false', async () => { // Spy sur les méthodes internes pour vérifier qu'elles ne sont pas appelées const focusSpy = vi.spyOn(HTMLElement.prototype, 'focus') // Simuler une modification qui déclencherait normalement les watchers const numberInput = wrapperWithoutKey.find('.number-field input') // Saisir puis effacer pour déclencher les watchers await numberInput.setValue('123') await wrapperWithoutKey.vm.$nextTick() await numberInput.setValue('') await wrapperWithoutKey.vm.$nextTick() await flushPromises() // Vérifier qu'aucun focus automatique n'a été déclenché expect(focusSpy).not.toHaveBeenCalled() focusSpy.mockRestore() }) it('preserves cursor position during editing when displayKey is false', async () => { const numberInput = wrapperWithoutKey.find('.number-field input') const inputElement = numberInput.element as HTMLInputElement // Saisir un NIR complet await numberInput.setValue('2940375120005') await wrapperWithoutKey.vm.$nextTick() await flushPromises() // Simuler le positionnement du curseur au milieu (position 7) inputElement.setSelectionRange(7, 7) // Simuler une modification (suppression d'un caractère) const currentValue = inputElement.value const newValue = currentValue.slice(0, 7) + currentValue.slice(8) await numberInput.setValue(newValue.replace(/\s/g, '')) await wrapperWithoutKey.vm.$nextTick() await flushPromises() // Dans un comportement correct, le curseur ne devrait pas être forcé à la fin // On vérifie que la logique de focus automatique n'interfère pas expect(wrapperWithoutKey.find('.key-field').exists()).toBe(false) }) it('handleKeyInput does not trigger focus when displayKey is false', async () => { const focusSpy = vi.spyOn(HTMLElement.prototype, 'focus') // Déclencher handleKeyInput via une saisie const numberInput = wrapperWithoutKey.find('.number-field input') await numberInput.setValue('123') await numberInput.trigger('input') await wrapperWithoutKey.vm.$nextTick() await flushPromises() // Vérifier qu'aucun focus n'a été déclenché expect(focusSpy).not.toHaveBeenCalled() focusSpy.mockRestore() }) }) describe('helpText', () => { it('affiche le helpText quand aucun message de validation', async () => { const w = mount(NirField, { props: { label: 'Identifiant', helpText: 'Saisissez le NIR' } }) activeWrappers.push(w) await w.vm.$nextTick() await flushPromises() const help = w.find('.sy-nir-help-text') expect(help.exists()).toBe(true) expect(help.text()).toBe('Saisissez le NIR') }) it('n\'affiche pas le helpText quand hideDetails est true', async () => { const w = mount(NirField, { props: { label: 'Identifiant', helpText: 'Saisissez le NIR', hideDetails: true } }) activeWrappers.push(w) await w.vm.$nextTick() await flushPromises() expect(w.find('.sy-nir-help-text').exists()).toBe(false) }) it('affiche toujours le helpText lorsqu\'une erreur est présente', async () => { const w = mount(NirField, { props: { label: 'Identifiant', helpText: 'Saisissez le NIR', required: true } }) activeWrappers.push(w) await w.vm.$nextTick() await flushPromises() // Déclenche l'erreur "requis" (champ vide) await w.vm.validateOnSubmit() await w.vm.$nextTick() await flushPromises() const help = w.find('.sy-nir-help-text') expect(help.exists()).toBe(true) expect(help.text()).toBe('Saisissez le NIR') }) }) describe('clearable', () => { it('affiche le bouton clear quand clearable et qu\'il y a une valeur', async () => { const w = mount(NirField, { props: { label: 'Identifiant', clearable: true } }) activeWrappers.push(w) await w.find('.number-field input').setValue('2940375120005') await w.vm.$nextTick() await flushPromises() expect(w.find('.number-field .sy-text-field__clear').exists()).toBe(true) }) it('n\'affiche pas le bouton clear quand clearable est false', async () => { const w = mount(NirField, { props: { label: 'Identifiant', clearable: false } }) activeWrappers.push(w) await w.find('.number-field input').setValue('2940375120005') await w.vm.$nextTick() await flushPromises() expect(w.find('.number-field .sy-text-field__clear').exists()).toBe(false) }) it('vide le champ au clic sur le bouton clear', async () => { const w = mount(NirField, { props: { label: 'Identifiant', clearable: true } }) activeWrappers.push(w) const numberInput = w.find('.number-field input') await numberInput.setValue('2940375120005') await w.vm.$nextTick() await flushPromises() await w.find('.number-field .sy-text-field__clear').trigger('click') await w.vm.$nextTick() await flushPromises() expect((numberInput.element as HTMLInputElement).value).toBe('') }) }) describe('validation Vuetify (numberRules / keyRules)', () => { const numberRules = [(v: string) => (!!v && v.replace(/\s/g, '').length === 13) || 'Le numéro doit contenir 13 chiffres'] const keyRules = [(v: string) => (!!v && v.replace(/\s/g, '').length === 2) || 'La clé doit contenir 2 chiffres'] it('applique numberRules : un NIR partiel est invalide', async () => { const w = mount(NirField, { props: { label: 'Identifiant', useVuetifyValidation: true, numberRules, keyRules } }) activeWrappers.push(w) await w.find('.number-field input').setValue('12345') await w.vm.$nextTick() await flushPromises() const isValid = await w.vm.validateOnSubmit() await flushPromises() expect(isValid).toBe(false) }) it('applique keyRules : une clé vide invalide même si le numéro est complet', async () => { const w = mount(NirField, { props: { label: 'Identifiant', useVuetifyValidation: true, numberRules, keyRules } }) activeWrappers.push(w) await w.find('.number-field input').setValue('2940375120005') await w.vm.$nextTick() await flushPromises() const isValid = await w.vm.validateOnSubmit() await flushPromises() expect(isValid).toBe(false) }) it('valide quand le numéro (13 chiffres) et la clé (2 chiffres) respectent les règles', async () => { const w = mount(NirField, { props: { label: 'Identifiant', useVuetifyValidation: true, numberRules, keyRules } }) activeWrappers.push(w) await w.find('.number-field input').setValue('2940375120005') await w.find('.key-field input').setValue('05') await w.vm.$nextTick() await flushPromises() const isValid = await w.vm.validateOnSubmit() await flushPromises() expect(isValid).toBe(true) }) }) describe('RGAA 7.4 - Auto-focus behavior and user notification', () => { it('le message d\'avertissement est présent mais caché visuellement lorsque displayKey est true', async () => { const noticeWrapper = wrapper.find('.d-sr-only') expect(noticeWrapper.exists()).toBe(true) expect(noticeWrapper.text()).toContain('Après la saisie des 13 caractères du numéro de sécurité sociale') }) it('n\'affiche pas le message d\'avertissement lorsque displayKey est false', async () => { await wrapper.setProps({ displayKey: false }) await wrapper.vm.$nextTick() await flushPromises() const noticeWrapper = wrapper.find('.d-sr-only') expect(noticeWrapper.exists()).toBe(false) }) it('n\'affiche pas le message d\'avertissement lorsque disabled est true', async () => { await wrapper.setProps({ disabled: true }) await wrapper.vm.$nextTick() await flushPromises() const noticeWrapper = wrapper.find('.d-sr-only') expect(noticeWrapper.exists()).toBe(false) }) it('n\'affiche pas le message d\'avertissement lorsque readonly est true', async () => { await wrapper.setProps({ readonly: true }) await wrapper.vm.$nextTick() await flushPromises() const noticeWrapper = wrapper.find('.d-sr-only') expect(noticeWrapper.exists()).toBe(false) }) it('le champ numéro possède un aria-describedby contenant l\'ID du message d\'avertissement', async () => { const noticeWrapper = wrapper.find('.d-sr-only') const noticeId = noticeWrapper.attributes('id') const numberField = wrapper.find('.number-field input') const describedBy = numberField.attributes('aria-describedby') expect(describedBy).toContain(noticeId) }) it('le champ clé ne possède pas l\'ID du message d\'avertissement dans son aria-describedby', async () => { const noticeWrapper = wrapper.find('.d-sr-only') const noticeId = noticeWrapper.attributes('id') const keyField = wrapper.find('.key-field input') const describedBy = keyField.attributes('aria-describedby') expect(describedBy).not.toContain(noticeId) }) it('le focus passe du champ numéro au champ clé lorsque le numéro atteint 13 caractères', async () => { const focusSpy = vi.spyOn(HTMLElement.prototype, 'focus') const numberInput = wrapper.find('.number-field input') await numberInput.setValue('2940375120005') await wrapper.vm.$nextTick() await flushPromises() expect(focusSpy).toHaveBeenCalled() focusSpy.mockRestore() }) it('le focus revient du champ clé au champ numéro lorsque la clé est vidée', async () => { const focusSpy = vi.spyOn(HTMLElement.prototype, 'focus') // D'abord remplir le numéro pour activer le comportement await wrapper.find('.number-field input').setValue('2940375120005') await wrapper.vm.$nextTick() await flushPromises() // Réinitialiser le spy pour ne compter que les appels suivants focusSpy.mockClear() // Vider la clé const keyInput = wrapper.find('.key-field input') await keyInput.setValue('') await wrapper.vm.$nextTick() await flushPromises() expect(focusSpy).toHaveBeenCalled() focusSpy.mockRestore() }) it('le focus ne se déplace pas automatiquement lorsque modelValue est synchronisé depuis l\'extérieur', async () => { const focusSpy = vi.spyOn(HTMLElement.prototype, 'focus') // Synchroniser modelValue depuis l'extérieur await wrapper.setProps({ modelValue: '294037512000591' }) await wrapper.vm.$nextTick() await flushPromises() // Vérifier qu'aucun focus automatique n'a été déclenché expect(focusSpy).not.toHaveBeenCalled() focusSpy.mockRestore() }) it('le focus ne se déplace pas lorsque le composant est disabled', async () => { const focusSpy = vi.spyOn(HTMLElement.prototype, 'focus') await wrapper.setProps({ disabled: true }) await wrapper.vm.$nextTick() await flushPromises() const numberInput = wrapper.find('.number-field input') await numberInput.setValue('2940375120005') await wrapper.vm.$nextTick() await flushPromises() expect(focusSpy).not.toHaveBeenCalled() focusSpy.mockRestore() }) it('le focus ne se déplace pas lorsque le composant est readonly', async () => { const focusSpy = vi.spyOn(HTMLElement.prototype, 'focus') await wrapper.setProps({ readonly: true }) await wrapper.vm.$nextTick() await flushPromises() const numberInput = wrapper.find('.number-field input') await numberInput.setValue('2940375120005') await wrapper.vm.$nextTick() await flushPromises() expect(focusSpy).not.toHaveBeenCalled() focusSpy.mockRestore() }) it('lorsque le numéro contient déjà 13 caractères et qu\'un chiffre est saisi, ce chiffre est ajouté au champ clé', async () => { // Remplir d'abord le numéro await wrapper.find('.number-field input').setValue('2940375120005') await wrapper.vm.$nextTick() await flushPromises() // Vérifier la valeur de la clé avant const keyInputBefore = wrapper.find('.key-field input').element as HTMLInputElement const keyBefore = keyInputBefore.value // Simuler la saisie d'un chiffre supplémentaire via keydown const numberInput = wrapper.find('.number-field input') await numberInput.trigger('keydown', { key: '9' }) await wrapper.vm.$nextTick() await flushPromises() // Vérifier que le chiffre a été ajouté à la clé const keyInputAfter = wrapper.find('.key-field input').element as HTMLInputElement const keyAfter = keyInputAfter.value expect(keyAfter).toBe(keyBefore + '9') }) it('le comportement reste identique avec withoutFieldset', async () => { await wrapper.setProps({ withoutFieldset: true }) await wrapper.vm.$nextTick() await flushPromises() // Vérifier que le message d'avertissement est toujours présent const noticeWrapper = wrapper.find('.d-sr-only') expect(noticeWrapper.exists()).toBe(true) // Vérifier que les aria-describedby sont toujours corrects const noticeId = noticeWrapper.attributes('id') const numberField = wrapper.find('.number-field input') const describedBy = numberField.attributes('aria-describedby') expect(describedBy).toContain(noticeId) }) it('shouldUseAutoFocus retourne false lorsque displayKey est false', async () => { await wrapper.setProps({ displayKey: false }) await wrapper.vm.$nextTick() await flushPromises() // shouldUseAutoFocus est une propriété interne, on teste son comportement // en vérifiant que le message n'est pas affiché const noticeWrapper = wrapper.find('.sy-nir-focus-notice') expect(noticeWrapper.exists()).toBe(false) }) it('autoFocusNoticeText est vide lorsque shouldUseAutoFocus est false', async () => { await wrapper.setProps({ disabled: true }) await wrapper.vm.$nextTick() await flushPromises() // Le computed doit retourner une chaîne vide const noticeWrapper = wrapper.find('.sy-nir-focus-notice') expect(noticeWrapper.exists()).toBe(false) }) }) describe('Exposed values: errors, warnings, successes', () => { // Helper pour créer un wrapper avec des props personnalisées const createNirFieldWrapper = (props: Record = {}) => { const defaultProps = { modelValue: undefined, required: false, showSuccessMessages: true, outlined: true, } const testWrapper = mount(NirField, { props: { ...defaultProps, ...props }, }) activeWrappers.push(testWrapper) return testWrapper } // Helper pour remplir et valider le NIR const fillAndValidateNir = async (w: ReturnType, nirValue: string, keyValue?: string) => { const numberInput = w.find('.number-field input') await numberInput.trigger('focus') await numberInput.setValue(nirValue) await w.vm.$nextTick() await numberInput.trigger('blur') await w.vm.$nextTick() await flushPromises() if (keyValue !== undefined) { const keyInput = w.find('.key-field input') await keyInput.trigger('focus') await keyInput.setValue(keyValue) await w.vm.$nextTick() await keyInput.trigger('blur') await w.vm.$nextTick() await flushPromises() } } describe('Structure and initialization', () => { it('exposes errors object with number and key refs', async () => { expect(wrapper.vm.errors).toBeDefined() expect(wrapper.vm.errors).toHaveProperty('number') expect(wrapper.vm.errors).toHaveProperty('key') expect(wrapper.vm.errors.number.value).toEqual([]) expect(wrapper.vm.errors.key.value).toEqual([]) }) it('exposes warnings object with number and key refs', async () => { expect(wrapper.vm.warnings).toBeDefined() expect(wrapper.vm.warnings).toHaveProperty('number') expect(wrapper.vm.warnings).toHaveProperty('key') expect(wrapper.vm.warnings.number.value).toEqual([]) expect(wrapper.vm.warnings.key.value).toEqual([]) }) it('exposes successes object with number and key refs', async () => { expect(wrapper.vm.successes).toBeDefined() expect(wrapper.vm.successes).toHaveProperty('number') expect(wrapper.vm.successes).toHaveProperty('key') expect(wrapper.vm.successes.number.value).toEqual([]) expect(wrapper.vm.successes.key.value).toEqual([]) }) it('keeps number and key errors separate', async () => { await fillAndValidateNir(wrapper, '123', '1') expect(wrapper.vm.errors.number.value.length).toBeGreaterThan(0) expect(wrapper.vm.errors.key.value.length).toBeGreaterThan(0) // Les deux devraient contenir des messages d'erreur différents expect(wrapper.vm.errors.number.value[0]).not.toBe(wrapper.vm.errors.key.value[0]) }) }) describe('Error handling', () => { it('populates number errors when NIR is invalid', async () => { await fillAndValidateNir(wrapper, '123') expect(wrapper.vm.errors.number.value.length).toBeGreaterThan(0) expect(wrapper.vm.errors.number.value[0]).toBe('Le numéro de sécurité sociale est invalide.') expect(wrapper.vm.errors.key.value.length).toBe(0) // Pas d'erreur clé }) it('populates key errors when key is invalid', async () => { await fillAndValidateNir(wrapper, '2940375120005', '1') expect(wrapper.vm.errors.key.value.length).toBeGreaterThan(0) expect(wrapper.vm.errors.key.value[0]).toBe('La clé de contrôle est invalide.') expect(wrapper.vm.errors.number.value.length).toBe(0) // Pas d'erreur NIR }) it('clears number errors when corrected', async () => { // D'abord créer une erreur await fillAndValidateNir(wrapper, '123') expect(wrapper.vm.errors.number.value.length).toBeGreaterThan(0) // Corriger la valeur await fillAndValidateNir(wrapper, '2940375120005') expect(wrapper.vm.errors.number.value.length).toBe(0) }) it('includes injected error messages in exposed errors', async () => { const testWrapper = createNirFieldWrapper({ errorMessages: ['Erreur injectée depuis le parent'], }) const allErrors = [ ...testWrapper.vm.errors.number.value, ...testWrapper.vm.errors.key.value, ] expect(allErrors).toContain('Erreur injectée depuis le parent') }) it('respects disableErrorHandling prop', async () => { const testWrapper = createNirFieldWrapper({ required: true, disableErrorHandling: true, }) await fillAndValidateNir(testWrapper, '123') // Les erreurs doivent être vides car disableErrorHandling est true expect(testWrapper.vm.errors.number.value.length).toBe(0) expect(testWrapper.vm.errors.key.value.length).toBe(0) }) it('respects maxErrors limit for exposed errors', async () => { const testWrapper = createNirFieldWrapper({ maxErrors: 1, customNumberRules: [ { type: 'custom', options: { validate: () => 'Erreur 1', message: 'Erreur 1' } }, { type: 'custom', options: { validate: () => 'Erreur 2', message: 'Erreur 2' } }, ], }) await fillAndValidateNir(testWrapper, '123') // Même si plusieurs erreurs existent, maxErrors limite l'affichage const errorCount = testWrapper.vm.errors.number.value.length expect(errorCount).toBeLessThanOrEqual(1) }) }) describe('Warning handling', () => { it('includes injected warning messages in exposed warnings', async () => { const testWrapper = createNirFieldWrapper({ warningMessages: ['Avertissement injecté depuis le parent'], }) const allWarnings = [ ...testWrapper.vm.warnings.number.value, ...testWrapper.vm.warnings.key.value, ] expect(allWarnings).toContain('Avertissement injecté depuis le parent') }) it('keeps number and key warnings separate when warnings are injected', async () => { const testWrapper = createNirFieldWrapper({ warningMessages: ['Avertissement NIR'], }) // Les avertissements injectés expect(testWrapper.vm.warnings.number.value.length >= 0).toBe(true) expect(testWrapper.vm.warnings.key.value.length >= 0).toBe(true) }) }) describe('Success handling', () => { it('populates number successes when NIR is valid', async () => { const testWrapper = createNirFieldWrapper({ required: false, showSuccessMessages: true, }) await fillAndValidateNir(testWrapper, '2940375120005') expect(testWrapper.vm.successes.number.value.length).toBeGreaterThan(0) expect(testWrapper.vm.successes.number.value[0]).toBe('Le numéro de sécurité sociale est valide.') expect(testWrapper.vm.successes.key.value.length).toBe(0) // Pas de succès clé }) it('populates key successes when key is valid', async () => { const testWrapper = createNirFieldWrapper({ required: false, showSuccessMessages: true, }) await fillAndValidateNir(testWrapper, '2940375120005', '91') expect(testWrapper.vm.successes.key.value.length).toBeGreaterThan(0) expect(testWrapper.vm.successes.key.value[0]).toBe('La clé de contrôle est valide.') expect(testWrapper.vm.successes.number.value.length).toBeGreaterThan(0) }) it('includes injected success messages in exposed successes', async () => { const testWrapper = createNirFieldWrapper({ successMessages: ['Succès injecté depuis le parent'], showSuccessMessages: true, }) const allSuccesses = [ ...testWrapper.vm.successes.number.value, ...testWrapper.vm.successes.key.value, ] expect(allSuccesses).toContain('Succès injecté depuis le parent') }) it('hides successes when showSuccessMessages is false', async () => { const testWrapper = createNirFieldWrapper({ showSuccessMessages: false, }) await fillAndValidateNir(testWrapper, '2940375120005') // Les successes ne doivent pas être affichés expect(testWrapper.vm.successes.number.value.length).toBe(0) }) it('shows successes when showSuccessMessages changes to true', async () => { const testWrapper = createNirFieldWrapper({ showSuccessMessages: false, }) await fillAndValidateNir(testWrapper, '2940375120005') expect(testWrapper.vm.successes.number.value.length).toBe(0) // Changer la prop await testWrapper.setProps({ showSuccessMessages: true }) await testWrapper.vm.$nextTick() await flushPromises() // Les successes doivent maintenant être affichés expect(testWrapper.vm.successes.number.value.length).toBeGreaterThan(0) }) }) describe('Lifecycle and reactivity', () => { it('clears all exposed values when clearValidation is called', async () => { // D'abord créer des erreurs await fillAndValidateNir(wrapper, '123') expect(wrapper.vm.errors.number.value.length).toBeGreaterThan(0) // Appeler clearValidation await wrapper.vm.clearValidation() await wrapper.vm.$nextTick() await flushPromises() // Vérifier que tout est vidé expect(wrapper.vm.errors.number.value.length).toBe(0) expect(wrapper.vm.errors.key.value.length).toBe(0) expect(wrapper.vm.warnings.number.value.length).toBe(0) expect(wrapper.vm.warnings.key.value.length).toBe(0) expect(wrapper.vm.successes.number.value.length).toBe(0) expect(wrapper.vm.successes.key.value.length).toBe(0) }) it('exposed values are readonly refs', async () => { expect(wrapper.vm.errors.number.value).toBeDefined() expect(Array.isArray(wrapper.vm.errors.number.value)).toBe(true) // Vérifier que l'accès multiple retourne la même valeur const first = wrapper.vm.errors.number.value const second = wrapper.vm.errors.number.value expect(first).toEqual(second) }) it('updates exposed values reactively on prop changes', async () => { const testWrapper = createNirFieldWrapper({ errorMessages: [], }) expect(testWrapper.vm.errors.number.value.length).toBe(0) // Changer la prop errorMessages await testWrapper.setProps({ errorMessages: ['Nouvelle erreur injectée'], }) await testWrapper.vm.$nextTick() await flushPromises() // Les erreurs exposées doivent être mises à jour const allErrors = [ ...testWrapper.vm.errors.number.value, ...testWrapper.vm.errors.key.value, ] expect(allErrors).toContain('Nouvelle erreur injectée') }) it('updates exposed values reactively on field value changes', async () => { const testWrapper = createNirFieldWrapper({ required: false, showSuccessMessages: true, }) expect(testWrapper.vm.successes.number.value.length).toBe(0) // Remplir le NIR valide await fillAndValidateNir(testWrapper, '2940375120005') // Les successes doivent être actualisées expect(testWrapper.vm.successes.number.value.length).toBeGreaterThan(0) // Vider le NIR const numberInput = testWrapper.find('.number-field input') await numberInput.setValue('') await testWrapper.vm.$nextTick() await numberInput.trigger('blur') await testWrapper.vm.$nextTick() await flushPromises() // Les successes doivent être vides expect(testWrapper.vm.successes.number.value.length).toBe(0) }) it('preserves error state during field navigation', async () => { await fillAndValidateNir(wrapper, '123') const errorsBefore = wrapper.vm.errors.number.value.length // Naviguer vers le champ clé await wrapper.find('.key-field input').trigger('focus') await wrapper.vm.$nextTick() await flushPromises() // Les erreurs du NIR doivent persister expect(wrapper.vm.errors.number.value.length).toBe(errorsBefore) }) it('exposes independent state for number and key fields', async () => { // Créer une erreur sur le NIR await wrapper.find('.number-field input').trigger('focus') await wrapper.find('.number-field input').setValue('123') await wrapper.vm.$nextTick() await wrapper.find('.number-field input').trigger('blur') await wrapper.vm.$nextTick() await flushPromises() const numberErrors = wrapper.vm.errors.number.value.length const keyErrors = wrapper.vm.errors.key.value.length // Les deux doivent être indépendants expect(numberErrors).toBeGreaterThan(0) expect(keyErrors).toBe(0) // Modifier la clé ne devrait pas affecter les erreurs du NIR await wrapper.find('.key-field input').trigger('focus') await wrapper.find('.key-field input').setValue('1') await wrapper.vm.$nextTick() await wrapper.find('.key-field input').trigger('blur') await wrapper.vm.$nextTick() await flushPromises() // Les erreurs du NIR doivent rester inchangées expect(wrapper.vm.errors.number.value.length).toBe(numberErrors) }) }) describe('Multiple messages handling', () => { it('combines multiple error sources', async () => { const testWrapper = createNirFieldWrapper({ errorMessages: ['Erreur injectée'], required: true, }) // Trigger validation error await fillAndValidateNir(testWrapper, '') const allErrors = [ ...testWrapper.vm.errors.number.value, ...testWrapper.vm.errors.key.value, ] // Doit contenir à la fois l'erreur injectée et l'erreur de validation expect(allErrors.length).toBeGreaterThan(0) }) it('removes duplicates from exposed errors', async () => { const testWrapper = createNirFieldWrapper({ errorMessages: ['Erreur identique'], customNumberRules: [ { type: 'custom', options: { validate: () => 'Erreur identique', message: 'Erreur identique' } }, ], }) await fillAndValidateNir(testWrapper, '123') // Les doublons doivent être supprimés const uniqueErrors = new Set(testWrapper.vm.errors.number.value) expect(uniqueErrors.size).toBeLessThanOrEqual(testWrapper.vm.errors.number.value.length) }) }) }) })