UNPKG

@mozaic-ds/vue

Version:

Mozaic-Vue is the Vue.js implementation of ADEO Design system

404 lines (356 loc) 13.5 kB
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { mount, VueWrapper } from '@vue/test-utils'; import { nextTick } from 'vue'; import MPhoneNumber from './MPhoneNumber.vue'; import { isValidPhoneNumber, type CountryCode } from 'libphonenumber-js'; vi.mock('libphonenumber-js', () => ({ default: vi.fn(), isValidPhoneNumber: vi.fn(), getCountries: vi.fn(() => ['FR', 'US', 'PT', 'DE', 'ES']), getCountryCallingCode: vi.fn((country) => { const codes = { FR: '33', US: '1', PT: '351', DE: '49', ES: '34' }; return codes[country as keyof typeof codes] || '33'; }), getExampleNumber: vi.fn(() => ({ formatNational: () => '01 23 45 67 89', })), parsePhoneNumberFromString: vi.fn((value) => { if (!value) return null; return { formatNational: () => { if (value === '+33123456789') return '01 23 45 67 89'; return value; }, number: value.startsWith('+') ? value : `+${value}`, }; }), })); vi.mock( '@mozaic-ds/icons-vue/src/components/ChevronDown20/ChevronDown20.vue', () => ({ default: { name: 'ChevronDown20', template: '<svg data-testid="chevron-icon" aria-hidden="true"></svg>', }, }), ); describe('MPhoneNumber', () => { let wrapper: VueWrapper<InstanceType<typeof MPhoneNumber>>; const defaultProps = { id: 'phone-input', defaultCountry: 'FR' as const, modelValue: '', }; beforeEach(() => { wrapper = mount(MPhoneNumber, { props: defaultProps, }); }); afterEach(() => { wrapper.unmount(); vi.clearAllMocks(); }); describe('Rendering', () => { it('should render the component', () => { expect(wrapper.find('.mc-phone-number-input').exists()).toBe(true); }); }); describe('Country Selection', () => { it('should render country selector and flag by default', () => { expect( wrapper.find('select.mc-phone-number-input__select').exists(), ).toBe(true); expect(wrapper.find('.mc-phone-number-input__flag').exists()).toBe(true); }); it('should hide country selector when flag prop is false', () => { wrapper = mount(MPhoneNumber, { props: { ...defaultProps, flag: false }, }); expect( wrapper.find('.mc-phone-number-input__select-wrapper').classes(), ).toContain('mc-phone-number-input__select-wrapper--hidden'); }); it('should render all country options', () => { const options = wrapper.findAll('option'); expect(options.length).toBeGreaterThan(1); expect(options.some((opt) => opt.text().includes('+33'))).toBe(true); expect(options.some((opt) => opt.text().includes('+1'))).toBe(true); }); it('should display correct flag image and alt attribute', () => { const flagImage = wrapper.find('img.mc-phone-number-input__flag-image'); expect(flagImage.exists()).toBe(true); expect(flagImage.attributes('src')).toContain('flagcdn.com/fr.svg'); expect(flagImage.attributes('alt')).toBeDefined(); }); it('should render chevron icon with aria-hidden true', () => { const chevron = wrapper.find('[data-testid="chevron-icon"]'); expect(chevron.exists()).toBe(true); expect(chevron.attributes('aria-hidden')).toBe('true'); }); }); describe('Phone Input', () => { it('should render phone input field with correct id', () => { const input = wrapper.find('input[type="tel"]'); expect(input.exists()).toBe(true); expect(input.attributes('id')).toBe(defaultProps.id); }); it('should display country calling code prefix by default', () => { expect( wrapper.find('.mc-phone-number-input__country-code').exists(), ).toBe(true); expect(wrapper.find('.mc-phone-number-input__country-code').text()).toBe( '+33', ); }); it('should hide prefix when prefix prop is false', () => { wrapper = mount(MPhoneNumber, { props: { ...defaultProps, prefix: false }, }); expect( wrapper.find('.mc-phone-number-input__country-code').exists(), ).toBe(false); }); it('should set custom placeholder if provided', () => { const placeholder = 'Enter your phone number'; wrapper = mount(MPhoneNumber, { props: { ...defaultProps, placeholder }, }); expect(wrapper.find('input[type="tel"]').attributes('placeholder')).toBe( placeholder, ); }); it('should use dynamic placeholder when no custom placeholder provided', () => { expect(wrapper.find('input[type="tel"]').attributes('placeholder')).toBe( '01 23 45 67 89', ); }); }); describe('States', () => { it('should be enabled by default', () => { const input = wrapper.find('input[type="tel"]'); const select = wrapper.find('select'); expect(input.attributes('disabled')).toBeUndefined(); expect(input.attributes('readonly')).toBeUndefined(); expect(select.attributes('disabled')).toBeUndefined(); expect(select.attributes('readonly')).toBeUndefined(); }); it('should be disabled when disabled prop is true', () => { wrapper = mount(MPhoneNumber, { props: { ...defaultProps, disabled: true }, }); expect( wrapper.find('input[type="tel"]').attributes('disabled'), ).toBeDefined(); expect(wrapper.find('select').attributes('disabled')).toBeDefined(); }); it('should be readonly when readonly prop is true', () => { wrapper = mount(MPhoneNumber, { props: { ...defaultProps, readonly: true }, }); expect( wrapper.find('input[type="tel"]').attributes('readonly'), ).toBeDefined(); expect(wrapper.find('select').attributes('readonly')).toBeDefined(); }); it('should apply invalid class when isInvalid is true', () => { wrapper = mount(MPhoneNumber, { props: { ...defaultProps, isInvalid: true }, }); expect(wrapper.find('.mc-phone-number-input__input').classes()).toContain( 'is-invalid', ); }); }); describe('Sizes', () => { it('should have medium size by default', () => { expect(wrapper.find('select').classes()).not.toContain('mc-select--s'); expect( wrapper.find('.mc-phone-number-input__input').classes(), ).not.toContain('mc-text-input--s'); }); it('should apply small size when size prop is s', () => { wrapper = mount(MPhoneNumber, { props: { ...defaultProps, size: 's' }, }); expect(wrapper.find('select').classes()).toContain('mc-select--s'); expect(wrapper.find('.mc-phone-number-input__input').classes()).toContain( 'mc-text-input--s', ); }); }); describe('Events', () => { it('should emit update:modelValue with international format on input', async () => { const input = wrapper.find('input[type="tel"]'); await input.setValue('+33123456789'); await nextTick(); expect(wrapper.emitted('update:modelValue')).toBeTruthy(); const emittedVal = wrapper.emitted('update:modelValue')?.[0][0] as string; expect(emittedVal.startsWith('+')).toBe(true); }); it('should emit valid event on input', async () => { const input = wrapper.find('input[type="tel"]'); await input.setValue('+33123456789'); await nextTick(); expect(wrapper.emitted('valid')).toBeTruthy(); }); it('should sanitize input removing invalid characters', async () => { const input = wrapper.find('input[type="tel"]'); await input.setValue('123abc456def+() -'); expect((input.element as HTMLInputElement).value).toBe('123456+() -'); }); }); describe('Validation', () => { beforeEach(() => { vi.mocked(isValidPhoneNumber).mockImplementation((number: string) => { const digits = number.replace(/\D/g, ''); return digits.length >= 10; }); }); it('should mark input invalid if phone number is invalid', async () => { wrapper = mount(MPhoneNumber, { props: { ...defaultProps, modelValue: '123' }, }); await nextTick(); expect(wrapper.find('.mc-phone-number-input__input').classes()).toContain( 'is-invalid', ); }); it('should not mark input invalid if empty', async () => { wrapper = mount(MPhoneNumber, { props: { ...defaultProps, modelValue: '' }, }); await nextTick(); expect( wrapper.find('.mc-phone-number-input__input').classes(), ).not.toContain('is-invalid'); }); }); describe('Accessibility', () => { it('should set aria-invalid attribute correctly', async () => { await wrapper.setProps({ isInvalid: true }); const input = wrapper.find('input[type="tel"]'); expect(input.attributes('aria-invalid')).toBeDefined(); }); it('should use provided id on input', () => { const input = wrapper.find('input[type="tel"]'); expect(input.attributes('id')).toBe(defaultProps.id); }); it('should set alt attribute on flag image', () => { const flagImg = wrapper.find('img.mc-phone-number-input__flag-image'); expect(flagImg.attributes('alt')).toBeTruthy(); }); it('should mark chevron icon aria-hidden as true', () => { const chevron = wrapper.find('[data-testid="chevron-icon"]'); expect(chevron.attributes('aria-hidden')).toBe('true'); }); }); describe('Props reactivity', () => { it('should update input when modelValue changes', async () => { await wrapper.setProps({ modelValue: '+33123456789' }); await nextTick(); const input = wrapper.find('input[type="tel"]'); expect((input.element as HTMLInputElement).value).toBe('+33123456789'); }); }); describe('Locale', () => { it('should use French locale by default', () => { const options = wrapper.findAll('option'); const franceOption = options.find( (opt) => opt.attributes('value') === 'FR', ); expect(franceOption?.text()).toContain('France'); }); it('should display country names in specified locale', () => { wrapper = mount(MPhoneNumber, { props: { ...defaultProps, locale: 'en' }, }); const options = wrapper.findAll('option'); const franceOption = options.find( (opt) => opt.attributes('value') === 'FR', ); // Country names should be translated according to the locale expect(franceOption?.text()).toBeTruthy(); }); it('should update country names when locale prop changes', async () => { await wrapper.setProps({ locale: 'es' }); await nextTick(); const options = wrapper.findAll('option'); expect(options.length).toBeGreaterThan(1); }); }); describe('Country Codes', () => { it('should display all countries by default', () => { const options = wrapper.findAll('option'); // Minus 1 for the empty hidden option expect(options.length - 1).toBe(5); // Mocked to return 5 countries }); it('should display only specified countries when countryCodes prop is provided', () => { wrapper = mount(MPhoneNumber, { props: { ...defaultProps, countryCodes: ['FR', 'US', 'GB'] as CountryCode[], }, }); const options = wrapper.findAll('option'); // Plus 1 for the empty hidden option expect(options.length).toBe(4); expect(options.some((opt) => opt.attributes('value') === 'FR')).toBe( true, ); expect(options.some((opt) => opt.attributes('value') === 'US')).toBe( true, ); expect(options.some((opt) => opt.attributes('value') === 'GB')).toBe( true, ); }); it('should update available countries when countryCodes prop changes', async () => { await wrapper.setProps({ countryCodes: ['FR', 'DE'] as CountryCode[] }); await nextTick(); const options = wrapper.findAll('option'); // Plus 1 for the empty hidden option expect(options.length).toBe(3); expect(options.some((opt) => opt.attributes('value') === 'FR')).toBe( true, ); expect(options.some((opt) => opt.attributes('value') === 'DE')).toBe( true, ); }); it('should work with single country in countryCodes', () => { wrapper = mount(MPhoneNumber, { props: { ...defaultProps, countryCodes: ['FR'] as CountryCode[], }, }); const options = wrapper.findAll('option'); // Plus 1 for the empty hidden option expect(options.length).toBe(2); expect(options.some((opt) => opt.attributes('value') === 'FR')).toBe( true, ); }); it('should combine locale and countryCodes props', () => { wrapper = mount(MPhoneNumber, { props: { ...defaultProps, locale: 'en', countryCodes: ['US', 'GB', 'CA'] as CountryCode[], }, }); const options = wrapper.findAll('option'); // Plus 1 for the empty hidden option expect(options.length).toBe(4); expect(options.some((opt) => opt.attributes('value') === 'US')).toBe( true, ); expect(options.some((opt) => opt.attributes('value') === 'GB')).toBe( true, ); expect(options.some((opt) => opt.attributes('value') === 'CA')).toBe( true, ); }); }); });