Testing
This page describes how to test an application built with with React Spectrum, including how to query the DOM tree for elements and simulate user interactions.
Introduction#
Running automated tests on your application helps ensure that it continues to work as expected over time. You can use testing tools like Enzyme or React Testing Library along with test runners like Jest or Mocha to test React Spectrum applications. These generally work quite well out of the box but there are a few things to consider to ensure your tests are the best they can be.
Testing semantics#
Many testing libraries expect you to query for elements in the DOM tree. For example, you might have a test that renders a login page, finds the username and password fields, and simulates filling them out and submitting the form.
Querying for these elements can be done in many ways. It's common to use CSS selectors for this, but this can be problematic if you rely on implementation details of the component in these queries. For example, if you query by a CSS class name that may change in a future update of React Spectrum, your test will be brittle.
This also applies if you query for internal elements within a React Spectrum component. The DOM structure that React Spectrum renders should be considered a black box, and it may change at any time, so you should avoid relying on it.
The recommended way to query for React Spectrum components and their internals is by semantics. React Spectrum components implement ARIA patterns. ARIA is a W3C standard that specifies the semantics for many UI components. This is used to expose them to assistive technology such as screen readers, but can also be used in tests to operate the application programmatically. These semantics are much less likely to change over time, and while other DOM nodes may be added or removed, and CSS classes changed, the semantics are more likely to stay stable.
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.
This is similar to the HTML element type (e.g. <button>
or <option>
), but since many React Spectrum
components are implemented with <div>
for styling purposes, the role
attribute is used instead to
communicate the element's semantics.
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 Spectrum 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');
import {render} from '@testing-library/react';
let tree = render(<MyComponent />);
let option = tree.getByRole('button');
import {render} from '@testing-library/react';
let tree = render(
<MyComponent />
);
let option = tree
.getByRole('button');
Enzyme#
In Enzyme, you can use CSS selectors to find elements. You can query by role using an attribute selector:
import {mount} from 'enzyme';
let wrapper = mount(<MyComponent />);
let option = wrapper.find('[role="option"]');
import {mount} from 'enzyme';
let wrapper = mount(<MyComponent />);
let option = wrapper.find('[role="option"]');
import {mount} from 'enzyme';
let wrapper = mount(
<MyComponent />
);
let option = wrapper
.find(
'[role="option"]'
);
Test ids#
Querying by semantics covers many scenarios, but what if you have many buttons on a page? How do you find the specific button you're looking for in a test? In many cases this could be done by querying by the text in the button or an accessibility label associated with it, but sometimes this might change over time or may be affected by things like translations in different languages. In these cases, you may need a way to identify specific elements in tests, and that's where test ids come in.
React Spectrum 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. For example, you could add test ids to the two input elements
in a login form and use them to find the username and password fields.
This example uses React Testing Library, but the idea could be applied in a similar way with Enzyme or other testing libraries.
import {TextField} from '@adobe/react-spectrum';
import {render} from '@testing-library/react';
function LoginForm() {
return (
<>
<TextField label="Username" data-testid="username" />
<TextField label="Password" data-testid="password" />
</>
);
}
let tree = render(<LoginForm />);
let username = tree.getByTestId('username');
let password = tree.getByTestId('password');
import {TextField} from '@adobe/react-spectrum';
import {render} from '@testing-library/react';
function LoginForm() {
return (
<>
<TextField label="Username" data-testid="username" />
<TextField label="Password" data-testid="password" />
</>
);
}
let tree = render(<LoginForm />);
let username = tree.getByTestId('username');
let password = tree.getByTestId('password');
import {TextField} from '@adobe/react-spectrum';
import {render} from '@testing-library/react';
function LoginForm() {
return (
<>
<TextField
label="Username"
data-testid="username"
/>
<TextField
label="Password"
data-testid="password"
/>
</>
);
}
let tree = render(
<LoginForm />
);
let username = tree
.getByTestId(
'username'
);
let password = tree
.getByTestId(
'password'
);
Triggering events#
Most testing libraries include a way to simulate events on an element. React Spectrum 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, rather than only simulating a click event, the tests should simulate all of the events that would occur if a real user were interacting with the component.
For example, a click is really a mousemove
and mouseover
the target, followed
by mousedown
, focus
, and mouseup
events, and finally a click
event. If you only simulated the click
event, you would be missing all of these other preceding events that occur in real-world situations and this
may make your test not work correctly. The implementation of the component may also change in the future to
expect these events, making your test brittle. In addition, browsers have default behavior that occurs on
certain events which would be missing, like focusing elements on mouse down, and toggling checkboxes on click.
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. If you use this library rather than firing events manually, your tests will simulate real-world behavior much better and work as expected.
user-event can handle many types of interactions, e.g. clicks, tabbing, typing, etc. This example shows how you could use it to render a login form and enter text in each field and click the submit button, just as a real user would.
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);
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);
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#
Providers#
When testing anything that uses a React Spectrum component, you must provide a wrapping Provider at the top level. Components rely on this wrapping provider to inform them of the app's theme, locale, breakpoints and more. See the Provider page for a list of properties your Provider can accept. If you see the following errors, you should check to see if you have a wrapping Provider.
Error: Uncaught [TypeError: Cannot destructure property 'scale' of '(0 , _provider.useProvider)(...)' as it is null.]
TypeError: Cannot read properties of null (reading 'scale')
If you are using React Testing Library, you can overwrite the provided render
function so that it automatically includes a Provider by default.
Note the below example allows you to rerender the component with new Provider props but you'll need to requery any element you are asserting against
to get its updated state (e.g. button = getByRole('button)
).
import {render, RenderOptions as BaseRenderOptions} from '@testing-library/react';
import {defaultTheme, Provider} from '@adobe/react-spectrum';
import React, {ReactElement} from 'react';
// export everything from React Testing Library
export * from '@testing-library/react';
function ProviderWrapper(props) {
let { children, ...providerProps } = props;
return (
<Provider theme={defaultTheme} scale="medium" {...providerProps}>
{props.children}
</Provider>
);
}
interface RenderOptions extends BaseRenderOptions {
providerProps: Omit<ProviderProps, 'children'>;
}
function customRender(ui: ReactElement, options?: RenderOptions) {
let rendered = render(ui, {
wrapper: (props) => (
<ProviderWrapper {...props} {...options?.providerProps} />
),
...options
});
return {
...rendered,
rerender: (ui, options) =>
providerWrapperRender(ui, { container: rendered.container, ...options })
};
}
// override render method
export { customRender as render };
import {
render,
RenderOptions as BaseRenderOptions
} from '@testing-library/react';
import {
defaultTheme,
Provider
} from '@adobe/react-spectrum';
import React, {ReactElement} from 'react';
// export everything from React Testing Library
export * from '@testing-library/react';
function ProviderWrapper(props) {
let { children, ...providerProps } = props;
return (
<Provider
theme={defaultTheme}
scale="medium"
{...providerProps}
>
{props.children}
</Provider>
);
}
interface RenderOptions extends BaseRenderOptions {
providerProps: Omit<ProviderProps, 'children'>;
}
function customRender(
ui: ReactElement,
options?: RenderOptions
) {
let rendered = render(ui, {
wrapper: (props) => (
<ProviderWrapper
{...props}
{...options?.providerProps}
/>
),
...options
});
return {
...rendered,
rerender: (ui, options) =>
providerWrapperRender(ui, {
container: rendered.container,
...options
})
};
}
// override render method
export { customRender as render };
import {
render,
RenderOptions
as BaseRenderOptions
} from '@testing-library/react';
import {
defaultTheme,
Provider
} from '@adobe/react-spectrum';
import React, {
ReactElement
} from 'react';
// export everything from React Testing Library
export * from '@testing-library/react';
function ProviderWrapper(
props
) {
let {
children,
...providerProps
} = props;
return (
<Provider
theme={defaultTheme}
scale="medium"
{...providerProps}
>
{props.children}
</Provider>
);
}
interface RenderOptions
extends
BaseRenderOptions {
providerProps: Omit<
ProviderProps,
'children'
>;
}
function customRender(
ui: ReactElement,
options?: RenderOptions
) {
let rendered = render(
ui,
{
wrapper: (
props
) => (
<ProviderWrapper
{...props}
{...options
?.providerProps}
/>
),
...options
}
);
return {
...rendered,
rerender: (
ui,
options
) =>
providerWrapperRender(
ui,
{
container:
rendered
.container,
...options
}
)
};
}
// override render method
export { customRender as render };
Timers#
If you are using fake timers in your test suite, be aware that you may need to advance your timers whenever an animation or load is taking place.
Historically, this most commonly happened when a modals, popovers, or trays is opened or closed as a result of a simulated user action (e.g. opening a Picker's dropdown via click).
Additionally, we have requestAnimationFrame
calls in various areas that you will need to also handle by advancing your timers in the tests. This happens most prominently
in our virtualized components due to the internals of our Virtualizer. 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());
});
afterEach(() => {
act(() => jest.runAllTimers());
});
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 for popovers to appear or for 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();
});
await waitFor(() => {
expect(getByRole('dialog')).toBeInTheDocument();
});
await waitFor(() => {
expect(
getByRole('dialog')
).toBeInTheDocument();
});
Desktop vs Mobile#
Components like ComboBox and Picker render a different experience on mobile versus desktop. If you need to test mobile vs desktop for you app or want to make sure the React Spectrum
components are rendered only against a specific experience, you'll need to mock the window screen width accordingly. We currently use a
screen width of 700px as the breakpoint
between mobile and desktop, so you should mock the width
getter of window.screen
to a value greater or less than this value accordingly. You may use simulateMobile
and
simulateDesktop
from the @react-spectrum/test-utils
package to handle this width mocking for you in Jest:
The example below shows how you would set this up in Jest.
import {simulateDesktop, simulateMobile} from '@react-spectrum/test-utils';
beforeAll(() => {
simulateMobile();
});
afterAll(() => {
jest.restoreAllMocks();
});
import {
simulateDesktop,
simulateMobile
} from '@react-spectrum/test-utils';
beforeAll(() => {
simulateMobile();
});
afterAll(() => {
jest.restoreAllMocks();
});
import {
simulateDesktop,
simulateMobile
} from '@react-spectrum/test-utils';
beforeAll(() => {
simulateMobile();
});
afterAll(() => {
jest.restoreAllMocks();
});
Virtualized components#
Many of our collection components are virtualized out of the box and thus rely on DOM node measurement to know how large a item should be and how many items can be rendered at once.
To properly render virtualized components in a test environment, you'll have to to mock the clientHeight
and clientWidth
getters of the HTMLElement
prototype.
scrollHeight
and scrollWidth
should also be mocked to set the height and width of each item. See below for an example implementation in Jest.
beforeAll(function () {
// The virtualizer will now think it has a visible area of 1000px x 1000px and that the items within it are 40px x 40px
jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get')
.mockImplementation(() => 1000);
jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get')
.mockImplementation(() => 1000);
jest.spyOn(window.HTMLElement.prototype, 'scrollHeight', 'get')
.mockImplementation(() => 40);
jest.spyOn(window.HTMLElement.prototype, 'scrollWidth', 'get')
.mockImplementation(() => 40);
});
afterAll(() => {
jest.restoreAllMocks();
});
beforeAll(function () {
// The virtualizer will now think it has a visible area of 1000px x 1000px and that the items within it are 40px x 40px
jest.spyOn(
window.HTMLElement.prototype,
'clientWidth',
'get'
).mockImplementation(() => 1000);
jest.spyOn(
window.HTMLElement.prototype,
'clientHeight',
'get'
).mockImplementation(() => 1000);
jest.spyOn(
window.HTMLElement.prototype,
'scrollHeight',
'get'
).mockImplementation(() => 40);
jest.spyOn(
window.HTMLElement.prototype,
'scrollWidth',
'get'
).mockImplementation(() => 40);
});
afterAll(() => {
jest.restoreAllMocks();
});
beforeAll(function () {
// The virtualizer will now think it has a visible area of 1000px x 1000px and that the items within it are 40px x 40px
jest.spyOn(
window.HTMLElement
.prototype,
'clientWidth',
'get'
).mockImplementation(
() => 1000
);
jest.spyOn(
window.HTMLElement
.prototype,
'clientHeight',
'get'
).mockImplementation(
() => 1000
);
jest.spyOn(
window.HTMLElement
.prototype,
'scrollHeight',
'get'
).mockImplementation(
() => 40
);
jest.spyOn(
window.HTMLElement
.prototype,
'scrollWidth',
'get'
).mockImplementation(
() => 40
);
});
afterAll(() => {
jest.restoreAllMocks();
});
Please note that this is greatly simplified and still won't accurately reflect the behavior of the virtualizer in browser so we highly recommended that you avoid doing the above or use a browser based automated testing framework to test virtualizer behavior. If you run into a test error where your collection component doesn't render any items/rows within it, try adding the above mocks.
Simulating user long press#
Components like ListView and TableView support long press to perform specific interactions like drag and drop or entering selection mode on mobile devices. 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, 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-spectrum/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);
import {fireEvent} from '@testing-library/react';
import {
installPointerEvent,
triggerLongPress
} from '@react-spectrum/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);
import {fireEvent} from '@testing-library/react';
import {
installPointerEvent,
triggerLongPress
} from '@react-spectrum/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-spectrum/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 });
import {fireEvent} from '@testing-library/react';
import {installMouseEvent} from '@react-spectrum/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 });
import {fireEvent} from '@testing-library/react';
import {installMouseEvent} from '@react-spectrum/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 }
);
Snapshot tests#
If you are using React 16 or 17, you may run into an issue where the ids generated by the React Spectrum components are changing on every snapshot. To remedy this, simply wrap your component in a SSRProvider. Note that you can use the SSRProvider even if your component doesn't support server side rendering, it will simply stabilize the ids generated.
import {SSRProvider, Provider, lightTheme} from '@adobe/react-spectrum';
<SSRProvider>
<Provider theme={lightTheme}>
<YourApp />
</Provider>
</SSRProvider>
import {
lightTheme,
Provider,
SSRProvider
} from '@adobe/react-spectrum';
<SSRProvider>
<Provider theme={lightTheme}>
<YourApp />
</Provider>
</SSRProvider>
import {
lightTheme,
Provider,
SSRProvider
} from '@adobe/react-spectrum';
<SSRProvider>
<Provider
theme={lightTheme}
>
<YourApp />
</Provider>
</SSRProvider>