Testing
This page describes how to test an application built with React Aria. It documents the available testing utilities available for each aria pattern and how they can be used to simulate common user interactions.
Testing semantics
The recommended way to query for React Aria Components and their internals is by semantics. React Aria Components implement ARIA patterns. ARIA is a W3C standard that specifies the semantics for many UI components. Unlike the DOM structure of the component, these semantics are much less likely to change over time, making them ideal to query for.
The main attribute to look for when querying is the role. This attribute represents the type of element a DOM node represents, e.g. a button, list option, or tab.
React Testing Library
React Testing Library is useful because it enforces that you write tests using semantics instead of implementation details. We use React Testing Library to test React Aria itself, and it's quite easy to query elements by role, text, label, etc.
import {render} from '@testing-library/react';
let tree = render(<MyComponent />);
let option = tree.getByRole('button');
Test ids
Querying by semantics covers many scenarios, but what if you have many buttons on a page or its text changes due to translations based on locale? In these cases, you may need a way to identify specific elements in tests, and that's where test ids come in.
React Aria Components pass all data attributes
through to their underlying DOM nodes, which allows you to use an attribute like data-testid to identify
a particular instance of a component.
import {render} from '@testing-library/react';
import {Input, Label, TextField} from 'react-aria-components';
function LoginForm() {
return (
<>
<TextField data-testid="username">
<Label>Username</Label>
<Input />
</TextField>
<TextField data-testid="password">
<Label>Username</Label>
<Input />
</TextField>
</>
);
}
let tree = render(<LoginForm />);
let username = tree.getByTestId('username');
let password = tree.getByTestId('password');
Triggering events
React Aria Components rely on many different browser events to support different devices and platforms, so it's important to simulate
these correctly in your tests. For example, a click is really a mousemove and mouseover the target, followed
by mousedown, focus, and mouseup events, and finally a click event.
The best way to handle this is with the user-event library. This lets you trigger high level interactions like a user would, and the library handles firing all of the individual events that make up that interaction.
import {render} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
let tree = render(<LoginForm />);
// Click on the username field to focus it, and enter the value.
userEvent.click(tree.getByLabelText('Username'));
userEvent.type(document.activeElement, 'devon');
// Tab to the password field, and enter the value.
userEvent.tab();
userEvent.type(document.activeElement, 'Pas$w0rd');
// Tab to the submit button and click it.
userEvent.tab();
userEvent.click(document.activeElement);
Test setup and common gotchas
Timers
If you are using fake timers in your test suite, be aware that you may need to advance your timers after various interactions. We have requestAnimationFrame calls in various underlying hooks that you will need to also handle by advancing your timers in the tests.
This happens most prominently in our collection components after selection. In Jest, this can be handled by calling act(() => jest.runAllTimers()); but you may require more precise control
depending on the other time-sensitive behavior you are testing. Please see Jest's timer docs or the equivalent docs of your test frameworks for more information on how to do so.
It is also a good idea to run all timers to completion after each test case to avoid any left over transitions or timeouts that a component may have setup during its lifecycle.
afterEach(() => {
act(() => jest.runAllTimers());
});
Consider adding a act(() => jest.runAllTimers()); after your simulated user interaction if you run into a test failure that looks like the following:
TestingLibraryElementError: Unable to find an accessible element with the role "listbox"
If you are using real timers instead, you can await a particular state of your app to be reached. If you are using React Testing Library, you can perform a waitFor query
to wait for a dialog to appear:
await waitFor(() => {
expect(getByRole('dialog')).toBeInTheDocument();
});
Simulating user long press
Some components like Menu support long press operations. Unfortunately, the approach of using the userEvent library to simulate a press event and running timers to hit the
long press internal timer threshold isn't sufficient due to useLongPress's usage of PointerEvent and our own detection of virtual vs mouse/touch pointer types. Mock PointerEvent
globally and use fireEvent from @testing-library/react to properly simulate these long press events in your tests.
If you are using Jest, you can call our installPointerEvent utility to automatically set up and tear down this mock in your test.
Additionally, if you are using fake timers and don't need to control the specific timings around the long press interaction, feel free to use our triggerLongPress utility as shown below.
import {fireEvent} from '@testing-library/react';
import {installPointerEvent, triggerLongPress} from '@react-aria/test-utils';
installPointerEvent();
// In test case
let button = getByRole('button');
// With fireEvent and specific timing control
fireEvent.pointerDown(el, {pointerType: 'touch'});
act(() => jest.advanceTimersByTime(800));
fireEvent.up(el, {pointerType: 'touch'});
// With triggerLongPress
triggerLongPress(button);
Simulating move event
Components like ColorArea, ColorSlider, ColorWheel, and Slider each feature a draggable handle that a user can interact with to change the component's value. Similar to long press, the interactions offered by userEvent library aren't sufficient to trigger
the underlying event handlers governing these drag/move operations. Mock MouseEvent globally and fireEvent from @testing-library/react to simulate these drag/move events in your tests.
If you are using Jest, you can call our installMouseEvent utility to automatically set up and tear down this mock in your test. Additionally, the track dimensions
for the draggable handle should be mocked so that the move operation calculations can be properly computed.
import {fireEvent} from '@testing-library/react';
import {installMouseEvent} from '@react-aria/test-utils';
installMouseEvent();
beforeAll(() => {
jest.spyOn(window.HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(() => ({top: 0, left: 0, width: 100, height: 10}));
})
// In test case
let sliderThumb = getByRole('slider').parentElement;
// With fireEvent, move thumb from 0 to 50
fireEvent.mouseDown(thumb, {clientX: 0, pageX: 0});
fireEvent.mouseMove(thumb, {pageX: 50});
fireEvent.mouseUp(thumb, {pageX: 50});
React Aria test utils
TODO can't place this next to the header here
beta@react-aria/test-utils is a set of testing utilities that aims to make writing unit tests easier for consumers of React Aria or for users who have built their own components following the respective ARIA pattern specification.
Installation
yarn add @react-aria/test-utils --dev
Setup
Once installed, you can access the User that @react-aria/test-utils provides in your test file as shown below. This user only needs to be initialized once and then can be used to generate
specific ARIA pattern tester in your test cases. This gives you access to that pattern's specific utilities that you can then call within your test to query for specific subcomponents or simulate common interactions.
See below for what patterns are currently supported.
// YourTest.test.ts
import {screen} from '@testing-library/react';
import {User} from '@react-aria/test-utils';
// Provide whatever method of advancing timers you use in your test, this example assumes Jest with fake timers.
// 'interactionType' specifies what mode of interaction should be simulated by the tester
// 'advanceTimer' is used by the tester to advance the timers in the tests for specific interactions (e.g. long press)
let testUtilUser = new User({interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime});
// ...
it('my test case', async function () {
// Render your test component/app
render();
// Initialize the table tester via providing the 'Table' pattern name and the root element of said table
let table = testUtilUser.createTester('Table', {root: screen.getByTestId('test_table')});
// ...
});
See below for the full definition of the User object.
Properties
| Name | Type | Default |
|---|---|---|
advanceTimer | UserOpts['advanceTimer'] | Default: — |
| A function used by the test utils to advance timers during interactions. Required for certain aria patterns (e.g. table). | ||
interactionType | UserOpts['interactionType'] | Default: mouse
|
| The interaction type (mouse, touch, keyboard) that the test util user will use when interacting with a component. This can be overridden at the aria pattern util level if needed. | ||
Methods
constructor | ||
createTester | ||
| Creates an aria pattern tester, inheriting the options provided to the original user. | ||
Patterns
// Combobox.test.ts
import {render} from '@testing-library/react';
import {User} from '@react-aria/test-utils';
let testUtilUser = new User({interactionType: 'mouse'});
// ...
it('ComboBox can select an option via keyboard', async function () {
// Render your test component/app and initialize the combobox tester
let {getByTestId} = render(
<ComboBox data-testid="test-combobox">
...
</ComboBox>
);
let comboboxTester = testUtilUser.createTester('ComboBox', {root: getByTestId('test-combobox'), interactionType: 'keyboard'});
await comboboxTester.open();
expect(comboboxTester.listbox).toBeInTheDocument();
let options = comboboxTester.options();
await comboboxTester.selectOption({option: options[0]});
expect(comboboxTester.combobox.value).toBe('One');
expect(comboboxTester.listbox).not.toBeInTheDocument();
});
Properties
| Name | Type | |
|---|---|---|
focusedOption | HTMLElement | null | |
| Returns the currently focused option in the combobox's dropdown if any. | ||
sections | HTMLElement | |
| Returns the combobox's sections if present. | ||
listbox | HTMLElement | null | |
| Returns the combobox's listbox if present. | ||
trigger | HTMLElement | |
| Returns the combobox trigger button. | ||
combobox | HTMLElement | |
| Returns the combobox. | ||
Methods
constructor | ||
setInteractionType | ||
| Set the interaction type used by the combobox tester. | ||
open | ||
| Opens the combobox dropdown. Defaults to using the interaction type set on the combobox tester. | ||
findOption | ||
| Returns an option matching the specified index or text content. | ||
selectOption | ||
| Selects the desired combobox option. Defaults to using the interaction type set on the combobox tester. If necessary, will open the combobox dropdown beforehand. The desired option can be targeted via the option's node, the option's text, or the option's index. | ||
close | ||
| Closes the combobox dropdown. | ||
options | ||
Returns the combobox's options if present. Can be filtered to a subsection of the listbox if provided via element. | ||