import { mount as baseMount, VueWrapper } from '@vue/test-utils' import PhoneField from '../PhoneField.vue' import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' import { indicatifs } from '../indicatifs' import type { ComponentPublicInstance } from 'vue' import { locales } from '../locales' import SyForm from '@/components/Customs/SyForm/SyForm.vue' interface PhoneFieldInstance extends ComponentPublicInstance { phoneNumber: string internalDialCode: { code: string, country: string, abbreviation: string, phoneLength: number, mask: string, displayText?: string } dialCodeList: unknown[] hasError: boolean errors: string[] validation: { clearValidation: () => void errors: string[] warnings: string[] successes: string[] hasError: boolean hasWarning: boolean hasSuccess: boolean } validateOnSubmit: () => Promise phoneMask: string clearValidation: () => void } type IndicatifLike = { code: string country: string abbreviation: string phoneLength: number mask: string displayText?: string } const mount = (component: unknown, options?: Record) => baseMount(component as never, options as never) as unknown as VueWrapper describe('PhoneField', () => { afterEach(() => { vi.clearAllMocks() document.body.innerHTML = '' }) it('renders correctly with default props', () => { const wrapper = mount(PhoneField) expect(wrapper.exists()).toBe(true) }) it('emits update:modelValue and change events on phone input', async () => { const wrapper = mount(PhoneField) const input = wrapper.find('input') await input.setValue('1234567890') expect(wrapper.emitted('update:modelValue')).toBeTruthy() await input.trigger('blur') expect(wrapper.emitted('change')).toBeTruthy() }) it('cleans spaces from phone number before validation', async () => { const wrapper = mount(PhoneField, { props: { required: true, modelValue: '01 23 45 67 89', isValidateOnBlur: true, }, }) await wrapper.vm.validateOnSubmit() expect(wrapper.vm.hasError).toBe(false) }) it('validates phone number and country code on blur', async () => { const wrapper = mount(PhoneField, { props: { required: true, modelValue: '', isValidateOnBlur: true, showSuccessMessages: true, }, }) const input = wrapper.find('input') await input.trigger('focus') await input.setValue('123456') await input.trigger('blur') expect(wrapper.vm.hasError).toBe(true) // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Nécessaire pour accéder à errors expect((wrapper.vm as any).errors.length).toBeGreaterThan(0) }) it('keeps a consistent success message before and after blur when withCountryCode is true', async () => { const wrapper = mount(PhoneField, { props: { withCountryCode: true, isValidateOnBlur: false, showSuccessMessages: true, modelValue: '', }, }) const textField = wrapper.findComponent({ name: 'SyTextField' }) const input = textField.find('input') await input.setValue('0123456789') await wrapper.vm.$nextTick() const messageBeforeBlur = textField.find('.v-messages__message') expect(messageBeforeBlur.exists()).toBe(true) expect(messageBeforeBlur.text()).toBe('Le champ Numéro de téléphone sans indicatif est valide.') expect(messageBeforeBlur.text()).not.toBe('Le champ Numéro de téléphone est valide.') await input.trigger('blur') await wrapper.vm.$nextTick() const messageAfterBlur = textField.find('.v-messages__message') expect(messageAfterBlur.exists()).toBe(true) expect(messageAfterBlur.text()).toBe('Le champ Numéro de téléphone sans indicatif est valide.') }) it('trims input to the expected phoneLength', async () => { const wrapper = mount(PhoneField, { props: { withCountryCode: true, modelValue: '', }, }) await wrapper.setProps({ dialCodeModel: '+27' }) const textField = wrapper.findComponent({ name: 'SyTextField' }) const input = textField.find('input') await input.setValue('01234567890') await wrapper.vm.$nextTick() const lastModelValueEmission = wrapper.emitted('update:modelValue')?.at(-1)?.[0] expect(typeof lastModelValueEmission).toBe('string') expect(String(lastModelValueEmission).replace(/\D/g, '').length).toBe(9) }) it('keeps counter max aligned with dial code phoneLength (+27 => 9)', async () => { const wrapper = mount(PhoneField, { props: { withCountryCode: true, modelValue: '', }, }) await wrapper.setProps({ dialCodeModel: '+27' }) const textField = wrapper.findComponent({ name: 'SyTextField' }) expect(textField.props('counter')).toBe(9) }) it('applies default phone mask correctly', async () => { const wrapper = mount(PhoneField, { props: { modelValue: '0619123456' }, }) await wrapper.vm.$nextTick() expect(wrapper.find('input').element.value).toBe('06 19 12 34 56') }) it('renders SySelect when withCountryCode is true', () => { const wrapper = mount(PhoneField, { props: { withCountryCode: true }, }) expect(wrapper.findComponent({ name: 'SySelect' }).exists()).toBe(true) }) it('updates phone mask and counter when dialCode changes', async () => { const wrapper = mount(PhoneField, { props: { withCountryCode: true }, }) await wrapper.setProps({ dialCodeModel: '+1' }) // dialCode is normalized against the canonical indicatifs list by code expect(wrapper.vm.phoneMask).toBe('### ### ####') expect(wrapper.findComponent({ name: 'SyTextField' }).props('counter')).toBe(10) }) it('uses only custom indicatifs when useCustomIndicatifsOnly is true', async () => { const customIndicatifs = [{ code: '+99', abbreviation: 'XX', country: 'Testland', phoneLength: 10, displayText: '+99' }] const wrapper = mount(PhoneField, { props: { useCustomIndicatifsOnly: true, customIndicatifs, }, }) expect(wrapper.vm.dialCodeList).toEqual(customIndicatifs.map(ind => expect.objectContaining(ind))) }) it('renders VTextField with outlined variant when outlined prop is true', () => { const wrapper = mount(PhoneField, { props: { outlined: true, }, }) const textField = wrapper.findComponent({ name: 'VTextField' }) expect(textField.props('variant')).toBe('outlined') }) it('renders VTextField with underlined variant when outlined prop is false', () => { const wrapper = mount(PhoneField, { props: { outlined: false, }, }) const textField = wrapper.findComponent({ name: 'VTextField' }) expect(textField.props('variant')).toBe('underlined') }) it('passes dialCode object to SyTextField when dialCode is set', async () => { const dialCodeModelValue = { code: '+33', abbreviation: 'FR', country: 'France', phoneLength: 10, mask: '## ## ## ## ##' } const wrapper = mount(PhoneField, { props: { withCountryCode: true, dialCodeModel: dialCodeModelValue, }, }) await wrapper.vm.$nextTick() const select = wrapper.findComponent({ name: 'SySelect' }) expect(select.exists()).toBe(true) expect(typeof select.props('modelValue')).toBe('object') type Indicatif = { code: string country: string abbreviation: string phoneLength: number mask: string displayText?: string } const dialCode = select.props('modelValue') as Indicatif expect(dialCode.code).toBe('+33') expect(dialCode.country).toBe('France') expect(dialCode.phoneLength).toBe(10) expect(dialCode.abbreviation).toBe('FR') expect(dialCode.mask).toBe('## ## ## ## ##') expect(dialCode).toHaveProperty('displayText') expect(typeof dialCode.displayText).toBe('string') }) it('formats phone number correctly', async () => { const wrapper = mount(PhoneField, { props: { modelValue: '0123456789', }, }) await wrapper.vm.$nextTick() const input = wrapper.find('input') expect(input.element.value).toBe('01 23 45 67 89') }) it('emits update:dialCodeModel when dialCode changes', async () => { const wrapper = mount(PhoneField, { props: { withCountryCode: true, }, }) await wrapper.setProps({ dialCodeModel: '+34' }) expect(wrapper.emitted('update:dialCodeModel')).toBeTruthy() const emittedEvents = wrapper.emitted('update:dialCodeModel') const lastEmitted = emittedEvents && emittedEvents[emittedEvents.length - 1]?.[0] expect(lastEmitted).toHaveProperty('code', '+34') }) it('exposes necessary properties and methods', () => { const wrapper = mount(PhoneField) expect(wrapper.vm.phoneMask).toBeDefined() expect(wrapper.vm.dialCodeList).toBeDefined() expect(wrapper.vm.validation).toBeDefined() expect(wrapper.vm.validateOnSubmit).toBeDefined() }) it('updates validation rules when counter changes', async () => { const wrapper = mount(PhoneField, { props: { withCountryCode: true, modelValue: '0123456789', }, }) expect(wrapper.findComponent({ name: 'SyTextField' }).props('counter')).toBe(10) await wrapper.setProps({ dialCodeModel: { code: '+44', abbreviation: 'UK', country: 'United Kingdom', phoneLength: 11, mask: '### ### #####' }, }) await wrapper.vm.$nextTick() // In the indicatifs list, +44 is associed with phoneLength 10 expect(wrapper.findComponent({ name: 'SyTextField' }).props('counter')).toBe(10) }) it('handles disabled state correctly', async () => { const wrapper = mount(PhoneField, { props: { disabled: true, withCountryCode: true, }, }) const textField = wrapper.findComponent({ name: 'SyTextField' }) expect(textField.props('disabled')).toBe(true) const select = wrapper.findComponent({ name: 'SySelect' }) expect(select.props('disabled')).toBe(true) }) it('handles readonly state correctly', async () => { const wrapper = mount(PhoneField, { props: { readonly: true, withCountryCode: true, }, }) const textField = wrapper.findComponent({ name: 'SyTextField' }) expect(textField.props('readonly')).toBe(true) const select = wrapper.findComponent({ name: 'SySelect' }) expect(select.props('readonly')).toBe(true) }) it('forwards hideDetails prop to SyTextField', () => { const wrapper = mount(PhoneField, { props: { hideDetails: true, }, }) const textField = wrapper.findComponent({ name: 'SyTextField' }) expect(textField.props('hideDetails')).toBe(true) }) it('shows and handles clear button when isClearable is true', async () => { const wrapper = mount(PhoneField, { props: { isClearable: true, modelValue: '0123456789', }, }) await wrapper.vm.$nextTick() const clearButton = wrapper.find(`button[aria-label="${locales.clearButtonAriaLabel}"]`) expect(clearButton.exists()).toBe(true) await clearButton.trigger('click') await wrapper.vm.$nextTick() expect(wrapper.vm.phoneNumber).toBe('') expect(wrapper.emitted('update:modelValue')).toBeTruthy() expect(wrapper.emitted('update:modelValue')?.at(-1)?.[0]).toBe('') await wrapper.setProps({ isClearable: false, modelValue: '0123456789' }) await wrapper.vm.$nextTick() expect(wrapper.find(`button[aria-label="${locales.clearButtonAriaLabel}"]`).exists()).toBe(false) }) it('verifies SyTextField and SySelect props are correctly passed', async () => { const wrapper = mount(PhoneField, { props: { withCountryCode: true, dialCodeModel: { code: '+33', abbreviation: 'FR', country: 'France', phoneLength: 10, mask: '## ## ## ## ##' }, }, }) const phoneInput = wrapper.find('input[type="tel"]') await phoneInput.setValue('0123456789') expect(phoneInput.element.value).toBe('01 23 45 67 89') const textField = wrapper.findComponent({ name: 'SyTextField' }) expect(textField.exists()).toBe(true) expect(textField.props('counter')).toBe(10) const select = wrapper.findComponent({ name: 'SySelect' }) expect(select.exists()).toBe(true) expect(select.props('returnObject')).toBe(true) expect((select.props('modelValue') as IndicatifLike).code).toBe('+33') }) it('updates dialCode when dialCodeModel changes after mount', async () => { const wrapper = mount(PhoneField, { props: { withCountryCode: true, }, }) // France est sélectionnée par défaut quand withCountryCode=true expect((wrapper.findComponent({ name: 'SySelect' }).props('modelValue') as IndicatifLike).code).toBe('+33') await wrapper.setProps({ dialCodeModel: { code: '+1', country: 'USA', abbreviation: 'US', phoneLength: 10, mask: '###-###-####' }, }) await wrapper.vm.$nextTick() const select = wrapper.findComponent({ name: 'SySelect' }) expect(select.exists()).toBe(true) expect(typeof select.props('modelValue')).toBe('object') type Indicatif = { code: string country: string abbreviation: string phoneLength: number mask: string displayText?: string } const dialCode = select.props('modelValue') as Indicatif expect(dialCode.code).toBe('+1') expect(dialCode.country).toBe('USA/Canada') expect(wrapper.vm.phoneMask).toBe('### ### ####') expect(wrapper.findComponent({ name: 'SyTextField' }).props('counter')).toBe(10) }) it('handles dialCodeModel objects without displayText property', async () => { const indicatifSansDisplayText = { code: '+44', country: 'United Kingdom', abbreviation: 'GB', phoneLength: 10, mask: '#### ### ####', } const wrapper = mount(PhoneField, { props: { withCountryCode: true, dialCodeModel: indicatifSansDisplayText, }, }) await wrapper.vm.$nextTick() const select = wrapper.findComponent({ name: 'SySelect' }) expect(select.exists()).toBe(true) type Indicatif = { code: string country: string abbreviation: string phoneLength: number mask: string displayText?: string } const dialCode = select.props('modelValue') as Indicatif expect(dialCode.code).toBe('+44') expect(dialCode.country).toBe('United Kingdom') expect(dialCode).toHaveProperty('displayText') expect(typeof dialCode.displayText).toBe('string') expect(dialCode.displayText).toContain('+44') }) it('should display helpText below by default when helpText is provided', async () => { const wrapper = mount(PhoneField, { props: { modelValue: '', required: true, helpText: 'Saisissez votre numéro de téléphone au format 01 23 45 67 89', }, }) await wrapper.vm.$nextTick() // Check that helpText is displayed by default when provided const helpTextDiv = wrapper.find('.help-text-below') expect(helpTextDiv.exists()).toBe(true) expect(helpTextDiv.text()).toBe('Saisissez votre numéro de téléphone au format 01 23 45 67 89') expect(helpTextDiv.classes()).toContain('help-text-below') }) it('should display helpText below even when field has valid value', async () => { const wrapper = mount(PhoneField, { props: { modelValue: '01 23 45 67 89', required: true, helpText: 'Saisissez votre numéro de téléphone au format 01 23 45 67 89', }, }) await wrapper.vm.$nextTick() // Check that helpText is displayed even when there are no errors const helpTextDiv = wrapper.find('.help-text-below') expect(helpTextDiv.exists()).toBe(true) expect(helpTextDiv.text()).toBe('Saisissez votre numéro de téléphone au format 01 23 45 67 89') }) it('should not display helpText below when helpText is not provided', async () => { const wrapper = mount(PhoneField, { props: { modelValue: '', required: true, // No helpText prop }, }) // Trigger validation by blurring the field const phoneInput = wrapper.find('input[type="tel"]') await phoneInput.trigger('blur') await wrapper.vm.$nextTick() // Check that helpText div is not displayed when helpText is not provided const helpTextDiv = wrapper.find('.help-text-below') expect(helpTextDiv.exists()).toBe(false) }) it('should apply default autocomplete attributes correctly', async () => { const wrapper = mount(PhoneField, { props: { modelValue: '', withCountryCode: true, }, }) await wrapper.vm.$nextTick() // Check that phone input has default tel-national autocomplete const phoneInput = wrapper.find('input[type="tel"]') expect(phoneInput.attributes('autocomplete')).toBe('tel-national') // Check that country code select has default tel-country-code autocomplete const selectInput = wrapper.find('.dial-code-select input') expect(selectInput.attributes('autocomplete')).toBe('tel-country-code') }) it('should apply custom autocomplete attributes when provided', async () => { const wrapper = mount(PhoneField, { props: { modelValue: '', withCountryCode: true, autocompleteCountryCode: 'tel-country-code', autocompletePhone: 'tel-extension', }, }) await wrapper.vm.$nextTick() // Check that phone input has custom autocomplete const phoneInput = wrapper.find('input[type="tel"]') expect(phoneInput.attributes('autocomplete')).toBe('tel-extension') // Check that country code select has custom autocomplete const selectInput = wrapper.find('.dial-code-select input') expect(selectInput.attributes('autocomplete')).toBe('tel-country-code') }) it('should verify autocomplete attributes are present in the actual DOM', async () => { const wrapper = mount(PhoneField, { props: { modelValue: '', withCountryCode: true, autocompleteCountryCode: 'tel-country-code', autocompletePhone: 'tel-national', }, }) await wrapper.vm.$nextTick() // Verify tel input has correct autocomplete const telInput = wrapper.find('input[type="tel"]') expect(telInput.exists()).toBe(true) const telAutocomplete = telInput.attributes('autocomplete') expect(telAutocomplete).toBe('tel-national') // Verify country select input has correct autocomplete const selectInput = wrapper.find('.dial-code-select input') expect(selectInput.exists()).toBe(true) const selectAutocomplete = selectInput.attributes('autocomplete') expect(selectAutocomplete).toBe('tel-country-code') }) it('should apply autocomplete to phone field only when no country code', async () => { const wrapper = mount(PhoneField, { props: { modelValue: '', withCountryCode: false, autocompletePhone: 'tel', }, }) await wrapper.vm.$nextTick() // Check that phone input has autocomplete const phoneInput = wrapper.find('input[type="tel"]') expect(phoneInput.attributes('autocomplete')).toBe('tel') // Check that country code select doesn't exist const selectInput = wrapper.find('.dial-code-select input') expect(selectInput.exists()).toBe(false) }) it('works correctly with standard indicatifs imported from indicatifs.ts', async () => { const franceIndicatif = indicatifs.find(ind => ind.country === 'France') expect(franceIndicatif).toBeDefined() const wrapper = mount(PhoneField, { props: { withCountryCode: true, dialCodeModel: franceIndicatif, }, }) await wrapper.vm.$nextTick() const select = wrapper.findComponent({ name: 'SySelect' }) expect(select.exists()).toBe(true) type Indicatif = { code: string country: string abbreviation: string phoneLength: number mask: string displayText?: string } const dialCode = select.props('modelValue') as Indicatif expect(dialCode.code).toBe('+33') expect(dialCode.country).toBe('France') expect(select.props('modelValue')).toEqual(expect.objectContaining({ code: '+33' })) }) // Tests pour les formats d'affichage avec abréviations encapsulées describe('Display formats with abbreviations', () => { let wrapper: VueWrapper beforeEach(() => { wrapper = mount(PhoneField, { props: { withCountryCode: true, displayFormat: 'code', }, }) }) it('formats display text as code by default', () => { const select = wrapper.findComponent({ name: 'SySelect' }) const firstItem = select.props('items')[0] expect(firstItem.displayText).toBe(firstItem.code) }) it('formats display text as code-abbreviation', async () => { await wrapper.setProps({ displayFormat: 'code-abbreviation' }) const select = wrapper.findComponent({ name: 'SySelect' }) const firstItem = select.props('items')[0] const expectedCountry = firstItem.countryFr || firstItem.country expect(firstItem.displayText).toBe(`${firstItem.code} (${firstItem.abbreviation})`) }) it('formats display text as code-country', async () => { await wrapper.setProps({ displayFormat: 'code-country' }) const select = wrapper.findComponent({ name: 'SySelect' }) const firstItem = select.props('items')[0] const expectedCountry = firstItem.countryFr || firstItem.country expect(firstItem.displayText).toBe(`${firstItem.code} ${expectedCountry}`) }) it('formats display text as country', async () => { await wrapper.setProps({ displayFormat: 'country' }) const select = wrapper.findComponent({ name: 'SySelect' }) const firstItem = select.props('items')[0] const expectedCountry = firstItem.countryFr || firstItem.country expect(firstItem.displayText).toBe(expectedCountry) }) it('formats display text as abbreviation', async () => { await wrapper.setProps({ displayFormat: 'abbreviation' }) const select = wrapper.findComponent({ name: 'SySelect' }) const firstItem = select.props('items')[0] const expectedCountry = firstItem.countryFr || firstItem.country expect(firstItem.displayText).toBe(`${firstItem.abbreviation}`) }) it('escapes HTML special characters in country name and abbreviation for code-abbreviation format', async () => { const maliciousIndicatif = { code: '+99', country: 'Bad"Country