Simulating User Interaction

There are two ways to drive a component and they are not equivalent. fireEvent.click(node) dispatches one click event. userEvent.click(node) dispatches what a browser dispatches when a person clicks: pointerover, pointerdown, mousedown, focus, pointerup, mouseup, click. If your component listens on mousedown, moves focus, or opens on hover, only the second reproduces the bug. user.type goes further still — it fires keydown, keypress, input and keyup per character, respects maxlength and refuses to type into a disabled input. Call userEvent.setup() once per test before render: it returns an API bound to a fresh session with its own clipboard and pointer state.

src/LoginForm.test.jsx: filling in and submitting a formJSX
test('submits what the user typed', async () => {
  const user = userEvent.setup();
  const onSubmit = vi.fn();
  render(<LoginForm onSubmit={onSubmit} />);
  await user.type(screen.getByRole('textbox', { name: 'Email' }), 'ada@example.com');
  await user.type(screen.getByLabelText('Password'), 'lovelace');
  await user.click(screen.getByRole('button', { name: /sign in/i }));
  expect(onSubmit).toHaveBeenCalledTimes(1);
  expect(Object.fromEntries(onSubmit.mock.calls[0][0]))
    .toEqual({ email: 'ada@example.com', password: 'lovelace' });
});

That test passes in 540 ms. vi.fn() records every call in mock.calls, so the assertion can be about the argument — here the FormData the submit handler built — rather than the fact that something happened.

A second test, "keyboard alone can reach the button", is worth writing for any form: tab order is the accessibility defect component tests usually miss. Three await user.tab() calls walk the focus ring, then toHaveFocus() proves the submit button is reachable without a mouse. The rest of the API is dblClick, hover, unhover, keyboard('{Enter}'), selectOptions, upload and clear.