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