Chapter 3 of 4

Interactions and Async

Simulating real user behaviour and waiting for the result.

user-event simulates what a real interaction does: focus, keydown, keypress, input, keyup, change. fireEvent dispatches a single raw event, which often misses behaviour your component depends on.

import userEvent from "@testing-library/user-event";

test("submits the form", async () => {
  const user = userEvent.setup();
  const onSubmit = vi.fn();

  render(<LoginForm onSubmit={onSubmit} />);

  await user.type(screen.getByLabelText(/email/i), "ada@example.com");
  await user.type(screen.getByLabelText(/password/i), "hunter2000");
  await user.click(screen.getByRole("button", { name: /sign in/i }));

  expect(onSubmit).toHaveBeenCalledWith({
    email: "ada@example.com",
    password: "hunter2000",
  });
});

Waiting for updates

// Preferred: retries until the element appears
expect(await screen.findByText(/saved/i)).toBeInTheDocument();

// For assertions that are not about finding an element
await waitFor(() => {
  expect(mockSave).toHaveBeenCalledTimes(1);
});

// For something disappearing
await waitForElementToBeRemoved(() => screen.queryByRole("progressbar"));

The act warning

'An update to X was not wrapped in act(...)' almost always means a state update happened after the test finished - usually an unawaited promise. The fix is nearly always an await, not wrapping things in act yourself.

// Causes the warning: the resolved fetch updates state after the test ends
render(<Profile />);
expect(screen.getByText("Ada")).toBeInTheDocument();

// Fixed: wait for the update
render(<Profile />);
expect(await screen.findByText("Ada")).toBeInTheDocument();