@mozaic-ds/vue
Version:
Mozaic-Vue is the Vue.js implementation of ADEO Design system
404 lines (356 loc) • 13.5 kB
text/typescript
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,
);
});
});
});