react-input-mask icon indicating copy to clipboard operation
react-input-mask copied to clipboard

Does not work with react-testing-library

Open lukescott opened this issue 5 years ago • 23 comments

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.

lukescott avatar May 13 '19 22:05 lukescott

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

pjaws avatar Feb 04 '20 22:02 pjaws

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

Smona avatar Feb 25 '20 03:02 Smona

@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.

pjaws avatar Feb 25 '20 05:02 pjaws

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="primary_contact_phone" value={value} onChange={event => onChange(event)} />
));

kamwoz avatar Mar 09 '20 08:03 kamwoz

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 [email protected] 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 .

pjaws avatar Mar 09 '20 17:03 pjaws

This might have been fixed in a recent update to jsdom: https://github.com/jsdom/jsdom/issues/2787. I saw this mentioned as part of https://github.com/testing-library/react-testing-library/issues/247.

lukescott avatar Mar 13 '20 16:03 lukescott

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.

BrunoQuaresma avatar Apr 02 '20 14:04 BrunoQuaresma

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.

pethersonmoreno avatar Apr 16 '20 15:04 pethersonmoreno

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.

How did you insert [data-testid] into your InputMask tag? I'm trying to put [data-testid] and [inputProps = {{"data-testid": ...}}] but nothing works.

dsmartins98 avatar Apr 27 '20 16:04 dsmartins98

Any update on this?

It seems that onChange is triggered however the input value is not updated.

I tried using fireEvent and also userEvent but no one of those works for me. :/

Can someone help with that?

nicolaszein avatar Jun 23 '20 02:06 nicolaszein

My solution is to use another lib

iagopiimenta avatar Jun 23 '20 14:06 iagopiimenta

@iagopiimenta which one are you using?

nicolaszein avatar Jun 23 '20 14:06 nicolaszein

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);
};

LucasCalazans avatar Jul 06 '20 03:07 LucasCalazans

Something i've noticed is that if there are any "blur" events in the test the "change" event does not work but for tests that don't have any "blur" events, we are able to use the "change" event.

mitchconquer avatar Jul 16 '20 17:07 mitchconquer

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

andremw avatar Jul 17 '20 14:07 andremw

Any update on this?

It seems that onChange is triggered however the input value is not updated.

I tried using fireEvent and also userEvent but no one of those works for me. :/

Can someone help with that?

@iagopiimenta which one are you using?

https://github.com/s-yadav/react-number-format

iagopiimenta avatar Aug 03 '20 13:08 iagopiimenta

Any news?? A component that cannot be tested is something really sad

afucher avatar Dec 12 '20 12:12 afucher

@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();
});

yordis avatar Dec 12 '20 21:12 yordis

Why fireEvent doesn't work? Didn't try this way, will check

afucher avatar Dec 13 '20 05:12 afucher

My quick fix for that (using Sinon for testing):

Create function that will return native input component:

function FakePhoneInput(props): ReactElement {
  return (
    <label>
      Phone number
      <input type="tel" {...props} autoComplete="off"></input>
    </label>
  );
}

Later create a stub that will fake your original component (component which uses masked-input):

sandbox.stub(phoneInputComponent, 'default').callsFake((props) => FakePhoneInput(props));

In this case during tests you will create normal 'plain' input, instead of react-input-mask component.

MIKOLAJW197 avatar Dec 14 '20 18:12 MIKOLAJW197

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

mikhailsmyslov avatar Mar 23 '21 14:03 mikhailsmyslov

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.

hsaldanha avatar Feb 10 '22 19:02 hsaldanha

Yep, this one seem to work, though a bit quirky:

interface IElement extends Element {
  value: string
  selectionStart: number
  selectionEnd: number
}

const changeInputMaskValue = (element: IElement, value: string | any[]) => {
  if (typeof value === 'string') {
    element.value = value
  }
  element.selectionStart = element.selectionEnd = value.length
  TestUtils.Simulate.change(element)
}

....
  test('Goes through the entire flow - happy path', async () => {
    const user = userEvent.setup()
    const inputPhoneNumber = screen.getByRole('textbox', { name: /phone number/i })

    await act(async () => {
      user.type(inputPhoneNumber, '+48790789789')
      changeInputMaskValue(inputPhoneNumber, '+48790789789')
    })
  })

w90 avatar Nov 02 '22 20:11 w90