This rule raises an issue when a promise returned by a Testing Library userEvent asynchronous method is not handled.

Why is this an issue?

Starting with version 14, @testing-library/user-event asynchronous event methods return promises. These methods simulate interactions as a user would perform them and may trigger asynchronous application behavior. Ignoring a returned promise lets the test continue before the interaction finishes, so assertions can run against an intermediate state and produce unreliable results.

The asynchronous userEvent methods covered by this rule are clear, click, copy, cut, dblClick, deselectOptions, hover, keyboard, pointer, paste, selectOptions, tripleClick, type, unhover, upload, and tab. Handle the promise by awaiting it, returning it to a caller that handles it, chaining it with then, or including it in Promise.all or Promise.allSettled. The rule covers direct userEvent calls, calls through an instance created by userEvent.setup(), and calls to wrappers that return a userEvent promise.

The rule applies to projects using a known @testing-library/user-event version 14 or later. Asynchronous fireEvent methods are not covered.

Code examples

Noncompliant code example

import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/dom';

test('submits the form', async () => {
  userEvent.click(screen.getByRole('button', { name: 'Submit' })); // Noncompliant: the promise returned by async event method "click" is unhandled
});

Compliant solution

import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/dom';

test('submits the form', async () => {
  await userEvent.click(screen.getByRole('button', { name: 'Submit' }));
});

Noncompliant code example

import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/dom';

test('types a name', async () => {
  const user = userEvent.setup();
  user.type(screen.getByRole('textbox'), 'Ada'); // Noncompliant: the promise returned by async event method "type" is unhandled
});

Compliant solution

import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/dom';

test('types a name', async () => {
  const user = userEvent.setup();
  await user.type(screen.getByRole('textbox'), 'Ada');
});

Noncompliant code example

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

const pressEscape = () => userEvent.keyboard('{Escape}');

test('closes the dialog', async () => {
  pressEscape(); // Noncompliant: the promise returned by "pressEscape" is unhandled
});

Compliant solution

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

const pressEscape = () => userEvent.keyboard('{Escape}');

test('closes the dialog', async () => {
  await pressEscape();
});

Noncompliant code example

import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/dom';

test('dismisses notifications', async () => {
  userEvent.click(screen.getByRole('button', { name: 'Dismiss warning' })); // Noncompliant: the promise returned by async event method "click" is unhandled
  userEvent.click(screen.getByRole('button', { name: 'Dismiss update' })); // Noncompliant: the promise returned by async event method "click" is unhandled
});

Compliant solution

import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/dom';

test('dismisses notifications', async () => {
  await Promise.all([
    userEvent.click(screen.getByRole('button', { name: 'Dismiss warning' })),
    userEvent.click(screen.getByRole('button', { name: 'Dismiss update' })),
  ]);
});

Resources

Documentation