react-input-mask: Does not work with react-testing-library

This fails:

test("react-input-mask and react-testing-library", () => {
	const props = {
		type: "text",
		mask: "(999) 999-9999",
		maskChar: null,
		onChange: event => updateValue(event.target.value),
		value: "",
	}
	const {container, rerender} = render(<InputMask {...props} />)
	const updateValue = jest.fn(value => {
		props.value = value
		rerender(<InputMask {...props} />)
	})
	const input = container.querySelector("input")
	const phoneValue = "(111) 222-3333"
	fireEvent.change(input!, {target: {value: phoneValue}})
	expect(updateValue).toBeCalledWith(phoneValue)
	expect(input).toHaveProperty("value", phoneValue)
})

With:

Expected mock function to have been called with:
      "(111) 222-3333"
    as argument 1, but it was called with
      "(".

If if I change the test to:

test("react-input-mask and react-testing-library", () => {
	const props = {
		type: "text",
		mask: "(999) 999-9999",
		maskChar: null,
		onChange: event => updateValue(event.target.value),
		value: "",
	}
	const {container, rerender} = render(<InputMask {...props} />)
	const updateValue = jest.fn(value => {
		props.value = value
		rerender(<InputMask {...props} />)
	})
	const input = container.querySelector("input")
	const phoneValue = "(111) 222-3333"
	input.value = phoneValue
	input.selectionStart = input.selectionEnd = phoneValue.length
	TestUtils.Simulate.change(input)
	// fireEvent.change(input!, {target: {value: phoneValue}})
	expect(updateValue).toBeCalledWith(phoneValue)
	expect(input).toHaveProperty("value", phoneValue)
})

It works. There doesn’t seem to be a way to use fireEvent and change selectionStart.

It would seem if selection were set to the length of the value inside of if (this.isInputAutofilled(...) {, similar to how if (beforePasteState) { does, it seems to work. I’m not sure what the consequences of that are though.

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 38
  • Comments: 23

Most upvoted comments

Any updates on this issue?

For now, I followed the @lukescott solution

import TestUtils from 'react-dom/test-utils';

const changeInputMaskValue = (element, value) => {
  element.value = value;
  element.selectionStart = element.selectionEnd = value.length;
  TestUtils.Simulate.change(element);
};

@afucher I am not sure what do you mean by “cannot be tested”, here I leave you how to test it, or at least it works for me.

import userEvent from '@testing-library/user-event';
import TestUtils from 'react-dom/test-utils';

function changeInputMaskValue(element, value) {
  element.value = value;
  element.selectionStart = element.selectionEnd = value.length;
  TestUtils.Simulate.change(element);
};

it('example test', async () => {
  render(
    <MyComponent
      amount="100.00"
    />,
  );

  act(() => {
    // Both lines of codes are required
    userEvent.type(screen.getByLabelText('Amount'), '300');
    changeInputMaskValue(screen.getByLabelText('Amount'), '300');
  });

  act(() => {
    // Do not move the form submitting to the previous `act`, it must be in two
    // separate `act` calls.
    userEvent.click(screen.getByText('Next'));
  });

  // You must use `findByText`
  const error = await screen.findByText(/\$100.00 to redeem/);
  expect(error).toBeInTheDocument();
});

That’s not a good solution at all. This is not something you want to mock.

On Mon, Mar 9, 2020 at 1:19 AM Kamil Woźny notifications@github.com wrote:

Simplest solution is to mock whole library with simple input jest.mock(‘react-input-mask’, () => ({ value, onChange, id, autoFocus = false }) => ( <input id={id} type=“text” name=“phone” value={value} onChange={event => onChange(event)} /> ));

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/sanniassin/react-input-mask/issues/174?email_source=notifications&email_token=AMVRRISTW5Z6EJUS73FQ5YLRGSQ7XA5CNFSM4HMT6IH2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEOGC7OY#issuecomment-596389819, or unsubscribe https://github.com/notifications/unsubscribe-auth/AMVRRIVFZB2FIGV2I4GLCR3RGSQ7XANCNFSM4HMT6IHQ .

I found a problem in another library react-imask, solved it using change event:

    test('change on react-imask component', async () => {
      const { getByTestId } = render(<Step1 />);
      const inputRealState: any = getByTestId('realStateValue');

      expect(inputRealState.value).toBe('');
      await fireEvent.change(inputRealState, { target: { value: '3' } });
      expect(inputRealState.value).toBe('3');
    });

Maybe it can be useful to react-input-mask too.

@Smona for this particular issue, userEvent.type worked for me; however, I had other issues with this lib that lead me to switch to react-text-mask, which solved all of them. Hope that helps.

In case if somebody also struggling from this issue, here is yet another hack, that may be useful:

const simulateInput = async (
  element?: HTMLInputElement | null,
  value = '',
  delay = 0
): Promise<void> => {
  if (!element) return
  element.click()
  const inner = async (rest = ''): Promise<void> => {
    if (!rest) return
    const { value: domValue } = element
    const caretPosition = domValue.search(/[A-Z_a-z]/) // regExp to match maskPlaceholder (which default is "_"), change according to your needs
    const newValue = domValue
      .slice(0, caretPosition)
      .concat(rest.charAt(0))
      .concat(domValue.slice(caretPosition))
    fireEvent.change(element, { target: { value: newValue } })
    await new Promise((resolve) => setTimeout(resolve, delay))
    return inner(rest.slice(1))
  }
  return inner(value)
}

const clearInput = (element?: HTMLInputElement | null): void => {
  if (!element) return
  fireEvent.change(element, { target: { value: '' } })
}

Usage inside test block:

...
  const input = screen.getByTestId('input')
  expect(input).toHaveValue('YYYY - mm - dd')

  await simulateInput(input, '2099abcdEFG-+=/\\*,. |') // only 1998-2000 years are allowed, so '99' should also be truncated
  expect(input).toHaveValue('20YY - mm - dd')

  await simulateInput(input, '000229')
  expect(input).toHaveValue('2000 - 02 - 29')

  clearInput(input)
  expect(input).toHaveValue('YYYY - mm - dd')
...

Seems that this issue related to how JSDOM maintains focus of the input (always places caret at the end of entered value), so, all chars entered with userEvent.type are placed outside the mask and therefofe truncated by internal logic of react-input-mask.

When using fireEvent.change, everything works as expected (thanks to internal logic of react-input-mask, which fills chars that match mask into placeholders), except the case with dynamic mask generation as shown in the example above with 1998-2000 years. If i call single fireEvent.change(‘2099’), the input value will be ‘2099 - mm - dd’ instead of ‘20yy - mm - dd’ which is wrong in mentioned case.

So, the hack purpose is to fire change events sequentially for a complete input value with only one char replacement at time (i.e. simulating typing symbols one by one), next typed symbol will replace the first maskPlaceholder char.

Promises can be omitted from simulateInput (as well as setTimeout call), and then it will be possible to use it without await keyword.

Hope, it’ll help somebody.

P.S. The same idea can be used to simulate backspace removal one by one. react-input-mask version 3.0.0-alpha.2

I’m using create-react-app + @testing-library/user-event with the type() but doesn’t work for me as well. I also updated react-scripts to be ^3.4.1.

I had this problem too. Moved to https://nosir.github.io/cleave.js/ 😕

@pjaws, how did you solve this issue? userEvent.type isn’t changing the value of the masked input for me.

I solved this issue myself by using @testing-library/user-event’s type method, rather than fireEvent.change.

I found a problem in another library react-imask, solved it using change event:

    test('change on react-imask component', async () => {
      const { getByTestId } = render(<Step1 />);
      const inputRealState: any = getByTestId('realStateValue');

      expect(inputRealState.value).toBe('');
      await fireEvent.change(inputRealState, { target: { value: '3' } });
      expect(inputRealState.value).toBe('3');
    });

Maybe it can be useful to react-input-mask too.

This solved the problem for me.

My solution is to use another lib