user-event: userEvent.type doesn't work well when a value is formatted after the onChange is fired

  • @testing-library/user-event version: 12.0.2
  • Testing Framework and version: jest (24.9.0 locally and whatever version CodeSandbox uses)
  • DOM Environment: @testing-library/react’s render function, so jsdom?

React component logic

export default function App() {
	const [rawPhone, setRawPhone] = useState('');
	const [formattedPhone, setFormattedPhone] = useState('');

	return (
		<div className="App">
			<form>
				<label htmlFor="raw-phone">Raw Phone: </label>
				<input id="raw-phone" type="text" value={rawPhone} onChange={event => setRawPhone(event.target.value)} />

				<label htmlFor="formatted-phone">Formatted Phone: </label>
				<input id="formatted-phone" type="text" value={formattedPhone} onChange={event => setFormattedPhone(formatPhone(event.target.value)) } />
			</form>
		</div>
	);
}

Format function

export function formatPhone(unformattedPhone?: string): string {
	if (unformattedPhone == null)  return '';

	// Remove all formatting from input
	let matchedValue = unformattedPhone.replace(/[^0-9]*/g, '').match(/\d*/g);

	const cleanedValue = matchedValue ? matchedValue.join('') : '';

	// group between areaCode, firstGroup, and secondGroup
	matchedValue = cleanedValue.match(/(\d{0,3})(\d{0,3})(\d{0,4})/);

	const [areaCode, firstGroup, secondGroup] = matchedValue ? matchedValue.slice(1) : ['', '', '']; 

	// initialize phoneNumber
	let phoneNumber = '';

	// begin with '(' after any digit input
	if (areaCode !== '') phoneNumber = `(${areaCode}`;

	// end area code with ')' if there are more than 3 digits (at least 1 digit in firstGroup)
	if (firstGroup !== '') phoneNumber = `${phoneNumber}) ${firstGroup}`;

	// add '-' if there are more than 6 digits (at least 1 digit in secondGroup)
	if (secondGroup !== '') phoneNumber = `${phoneNumber}-${secondGroup}`;

	return phoneNumber;
}

Tests

test('Raw phone should set the value to match excatly what is typed', async () => {
	render(<App />);
	userEvent.type(screen.getByLabelText(/Raw Phone/i), '1234567890');
	expect(screen.getByLabelText(/Raw Phone/i)).toHaveValue('1234567890');
});

test('Formatted phone should format the value as it is typed', async () => {
	render(<App />);
	userEvent.type(screen.getByLabelText(/Formatted Phone/i), '1234567890');
	expect(screen.getByLabelText(/Formatted Phone/i)).toHaveValue('(123) 456-7890');
});

What you did:

I have a field where we collect a phone number and format it as the user types. To do so, we format the value onChange before we call the setState function from the useState hook`.

What happened:

When attempting to test this by calling userEvent.type(..., '1234567890') the value was being set to (098) 765-4321 instead of (123) 456-7890.

Reproduction repository:

https://codesandbox.io/s/usereventtype-reverses-value-mgi2n?file=/src/App.spec.tsx

Problem description:

This was extremely confusing at first and led me to do a lot of digging in the source code of userEvent.type(...). Once I did so for a while I came across the idea that it may be an issue with the selected location returning to the beginning of the input. I think it may have something to do with the newValue being different than it expected.

Suggested solution:

I don’t know that I have a suggested solution, as I don’t have a complete grasp over the source code and the cause of the specific problem. However, I’m more than happy to help discuss options and take a crack at implementing a fix with some guidance.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 1
  • Comments: 18 (17 by maintainers)

Commits related to this issue

Most upvoted comments

@terraelise @mickcastle Please don’t add noise to the issue board. File an issue with a reproducible example.

Also try upgrading to the latest version. The issue above has been resolved. There might be another issue with 13.5.0 that might or might not be already resolved.

I am experiencing this issue with 13.5.0, had to downgrade to 12.0.4 to solve it.