import { Field } from '../field/Field'; import { mockMatchMedia, mockResizeObserver, render, screen, within, userEvent, } from '../test-utils'; import PhoneNumberInput, { PhoneNumberInputProps } from './PhoneNumberInput'; mockMatchMedia(); mockResizeObserver(); describe('PhoneNumberInput', () => { afterEach(jest.clearAllMocks); const props = { onChange: jest.fn() }; const customRender = (overrides: Partial = {}, locale?: string) => render(, { locale }); const getCountryCodeSelect = () => screen.getByRole('combobox'); const getCountryCodeLabel = () => screen.getByText('Country code'); const getPhoneNumberInput = () => screen.getByRole('textbox'); const getPhoneNumberLabel = () => screen.getByText('Phone number'); describe('defaults', () => { it('should set prefix control to default UK value', () => { customRender(); expect(getCountryCodeSelect()).toHaveTextContent('+44'); }); it('should set number control to empty', () => { customRender(); expect(getPhoneNumberInput()).toHaveValue(''); }); it('should not disable the select', () => { customRender(); expect(getCountryCodeSelect()).toBeEnabled(); }); it('should not disable the input', () => { customRender(); expect(getPhoneNumberInput()).toBeEnabled(); }); }); it('should propagate `initialValue`', () => { const prefix = '+39'; const number = '123456789'; customRender({ initialValue: `${prefix}${number}` }); expect(getCountryCodeSelect()).toHaveTextContent(prefix); expect(getPhoneNumberInput()).toHaveValue(number); }); describe('id prop', () => { it('should render sensible default IDs', () => { customRender(); const countryCodeSelectID = getCountryCodeSelect().getAttribute('id'); expect(countryCodeSelectID).toMatch(/^country-code-select-[a-z0-9]{6}$/); const countryCodeLabelID = getCountryCodeLabel().getAttribute('id'); expect(countryCodeLabelID).toMatch(/^country-code-label-[a-z0-9]{6}$/); const phoneNumberInputID = getPhoneNumberInput().getAttribute('id'); expect(phoneNumberInputID).toMatch(/^phone-number-input-[a-z0-9]{6}$/); const phoneNumberLabelID = getPhoneNumberLabel().getAttribute('id'); expect(phoneNumberLabelID).toMatch(/^phone-number-label-[a-z0-9]{6}$/); }); it('should use the custom `id` as-is for the input but with slight modification for select', () => { const id = 'component-id'; customRender({ id }); expect(getPhoneNumberInput()).toHaveAttribute('id', id); expect(getPhoneNumberLabel()).toHaveAttribute('id', `${id}-phone-number-label`); expect(getCountryCodeSelect()).toHaveAttribute('id', `${id}-country-code-select`); expect(getCountryCodeLabel()).toHaveAttribute('id', `${id}-country-code-label`); }); it('should set correct ARIA attributes on the country code select when id is provided', () => { const id = 'my-id'; customRender({ id }); expect(getCountryCodeSelect()).toHaveAttribute('aria-labelledby', `${id}-country-code-label`); }); it('should set correct ARIA attributes on the country code select when id is not provided', () => { customRender(); const selectLabelledBy = getCountryCodeSelect().getAttribute('aria-labelledby'); expect(selectLabelledBy).toMatch(/^country-code-label-[a-z0-9]{6}$/); }); it('should set correct ARIA attributes on the phone number input when id is provided', () => { const id = 'my-id'; customRender({ id }); expect(getPhoneNumberInput()).toHaveAttribute('aria-labelledby', `${id}-phone-number-label`); }); it('should set correct ARIA attributes on the phone number input when id is not provided', () => { customRender(); const inputLabelledBy = getPhoneNumberInput().getAttribute('aria-labelledby'); expect(inputLabelledBy).toMatch(/^phone-number-label-[a-z0-9]{6}$/); }); }); describe('pasting', () => { const initialPrefix = '+48'; const initialNumber = '987654321'; const renderAndPaste = async (value: string) => { customRender({ initialValue: `${initialPrefix}${initialNumber}` }); await userEvent.tab(); await userEvent.tab(); await userEvent.paste(value); }; [ { number: '+36303932551', countryCode: '+36', localNumber: '303932551' }, { number: '+39123456781', countryCode: '+39', localNumber: '123456781' }, { number: '+44 7700 900415', countryCode: '+44', localNumber: '7700900415' }, { number: '+2975557308515', countryCode: '+297', localNumber: '5557308515' }, { number: '+297-555-7217-360', countryCode: '+297', localNumber: '5557217360' }, { number: '+213-555-5160-67', countryCode: '+213', localNumber: '555516067' }, { number: '+246-387-5553', countryCode: '+246', localNumber: '3875553' }, { number: '+852-940-5558--6', countryCode: '+852', localNumber: '94055586' }, { number: '+228 253 5558 4', countryCode: '+228', localNumber: '25355584' }, ].forEach(({ number, countryCode, localNumber }) => { it(`'${number}' number should update the value properly`, async () => { await renderAndPaste(number); expect(getCountryCodeSelect()).toHaveTextContent(countryCode); expect(getPhoneNumberInput()).toHaveValue(localNumber); expect(props.onChange).toHaveBeenCalledWith(number.replace(/[\s-]+/g, ''), countryCode); }); }); it('should not paste invalid characters', async () => { await renderAndPaste('+36asdasdasd'); expect(getCountryCodeSelect()).toHaveTextContent(initialPrefix); expect(getPhoneNumberInput()).toHaveValue(initialNumber); expect(props.onChange).not.toHaveBeenCalled(); }); it('should not paste countries which are not in the select', async () => { await renderAndPaste('+9992342343423'); expect(getCountryCodeSelect()).toHaveTextContent(initialPrefix); expect(getPhoneNumberInput()).toHaveValue(initialNumber); expect(props.onChange).not.toHaveBeenCalled(); }); it("should not paste numbers which doesn't start with the country code", async () => { await renderAndPaste('0+36303932551'); expect(getCountryCodeSelect()).toHaveTextContent(initialPrefix); expect(getPhoneNumberInput()).toHaveValue(initialNumber); expect(props.onChange).not.toHaveBeenCalled(); }); it("should allow pasting numbers which don't contain a country code", async () => { const newNumber = '06303932551'; await renderAndPaste(newNumber); expect(getCountryCodeSelect()).toHaveTextContent(initialPrefix); expect(getPhoneNumberInput()).toHaveValue(newNumber); expect(props.onChange).toHaveBeenCalledWith(`${initialPrefix}${newNumber}`, initialPrefix); }); }); describe('initialValue', () => { describe('when a model is supplied that could match more than one prefix', () => { const initialProps = { initialValue: '+1868123456789' }; it('should set the select to the longest matching prefix', () => { customRender(initialProps); expect(getCountryCodeSelect()).toHaveTextContent('+1868'); }); it('should set the number input to the rest of the number', () => { customRender(initialProps); expect(getPhoneNumberInput()).toHaveValue('123456789'); }); }); describe('when a model is supplied with no matching prefix', () => { const initialProps = { initialValue: '+999123456789' }; it('should empty the select', () => { customRender(initialProps); expect(getCountryCodeSelect()).toHaveTextContent('Select an option...'); }); it('should put the whole value in the input without the plus', () => { customRender(initialProps); expect(getPhoneNumberInput()).toHaveValue('999123456789'); }); }); describe('when an partial model is supplied (with a matching prefix)', () => { it('should set the select to the matching prefix and put the rest of the number in the suffix', () => { customRender({ initialValue: '+123' }); expect(getCountryCodeSelect()).toHaveTextContent('+1'); expect(getPhoneNumberInput()).toHaveValue('23'); }); }); }); describe('when disabled is true', () => { it('should disable both controls', () => { customRender({ disabled: true }); expect(getCountryCodeSelect()).toBeDisabled(); expect(getPhoneNumberInput()).toBeDisabled(); }); }); describe('placeholders', () => { it('should use the provided placeholder', () => { const placeholder = 'custom placeholder'; customRender({ placeholder }); expect(getPhoneNumberInput()).toHaveAttribute('placeholder', placeholder); }); it('should use the provided searchPlaceholder', async () => { const searchPlaceholder = 'search placeholder'; customRender({ searchPlaceholder }); await userEvent.click(getCountryCodeSelect()); expect(screen.getByRole('combobox', { name: searchPlaceholder })).toBeInTheDocument(); }); }); describe('when supplied with a locale', () => { describe('and a value', () => { it('should use the prefix of the supplied value', () => { customRender({ initialValue: '+12345678' }, 'es'); expect(getCountryCodeSelect()).toHaveTextContent('+1'); }); }); describe('and no value', () => { describe('and no country code', () => { it('should default the prefix to the local country', () => { customRender(undefined, 'es'); expect(getCountryCodeSelect()).toHaveTextContent('+34'); }); }); describe('and country code', () => { it('should override locale prefix with country specific prefix', () => { customRender({ countryCode: 'US' }, 'es'); expect(getCountryCodeSelect()).toHaveTextContent('+1'); }); }); }); }); describe('user input', () => { describe('valid number', () => { it('should trigger onChange handler', async () => { customRender(); await userEvent.type(getPhoneNumberInput(), '123'); expect(props.onChange).toHaveBeenCalledWith('+44123', '+44'); }); }); describe('invalid number', () => { it('should trigger onChange with null value', async () => { customRender({ initialValue: '+1234' }); await userEvent.type(getPhoneNumberInput(), '{Backspace}{Backspace}{Backspace}1'); expect(props.onChange).toHaveBeenCalledWith(null, '+1'); }); }); describe('when user insert invalid character', () => { it('should strip them', async () => { customRender({ initialValue: '+12345678' }); await userEvent.type(getPhoneNumberInput(), '123--'); expect(props.onChange).toHaveBeenCalledWith('+12345678123', '+1'); }); }); describe('overlapping prefix and suffix numbers', () => { it("shouldn't change the prefix number on matching suffix input", async () => { customRender({ countryCode: 'eg' }); await userEvent.type(getPhoneNumberInput(), '1111111'); expect(getPhoneNumberInput()).toHaveValue('1111111'); expect(props.onChange).toHaveBeenCalledWith('+201111111', '+20'); }); }); }); describe('when selectProps is supplied', () => { it('renders Select component with expected props', () => { customRender({ selectProps: { className: 'custom-class' } }); expect(getCountryCodeSelect().parentElement).toHaveClass('custom-class'); }); }); it('supports custom `aria-labelledby` attribute', () => { render( <> {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} {}} /> , ); expect( within(screen.getByLabelText('Prioritized label')).getByRole('textbox'), ).toBeInTheDocument(); }); it('supports `Field` for labeling', () => { render( {}} /> , ); expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Phone number/); }); it('focuses country code input when `Field` label is clicked', async () => { const label = 'Phone number'; render( {}} /> , ); await userEvent.click(screen.getByText(label, { selector: 'label:not(.sr-only)' })); // Have to use `getByText` due to the way `Field` handles group labelling const countryCodeFilterInput = screen.getByRole('combobox'); expect(countryCodeFilterInput).toHaveFocus(); }); it('focuses number input when `Field` label is clicked and country code is disabled', async () => { const label = 'Phone number'; render( {}} /> , ); const phoneNumberInput = screen.getByRole('textbox'); await userEvent.click(screen.getByText(label, { selector: 'label:not(.sr-only)' })); // Have to use `getByText` due to the way `Field` handles group labelling expect(phoneNumberInput).toHaveFocus(); }); });