Skip to main content

React Testing Library Usage

Resources

📘 Types of Queries

📘 Query Priority

Test Boilerplate

SomeComponent.test.js
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import SomeComponent from './SomeComponent';

test('test description goes here', async () => {
// ARRANGE
render(<SomeComponent />);

// ACT
await userEvent.click(screen.getByText('Load Greeting'));
await screen.findByRole('heading');

// ASSERT
expect(screen.getByRole('heading')).toHaveTextContent('hello there');
expect(screen.getByRole('button')).toBeDisabled();
});

Rules of Thumb

Target using roles.

We always want to target elements using their roles if possible, as this seperates the test from the implementation details.

In my experience however this is also the most difficult part of testing with React Testing Library, as it can be difficult to correctly target element using roles.

Smoke Test

Always pop a smoke test in there first

DataTable.test.js
import React from 'react';
import {render} from '@testing-library/react';
import DataTable from './DataTable';

test('DataTable component loads without crashing', () => {
render(<DataTable />);
});

Additionally if you want to have some kind of test on a component, but don't have the time to write a more comprehensive test, you can always just start with a smoke test and then add more tests later. It's a good way to get started.

Difference between act and waitFor

act is used to wrap code that causes side effects, such as rendering, fetching data, or updating the DOM. It is used to make sure that the side effects are completed before the test continues.

import {render} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MyComponent from './MyComponent';

test('button click updates text', () => {
const {getByText} = render(<MyComponent />);
const button = getByText('Click me');

await act(() => {
userEvent.click(button);
});

expect(getByText('Clicked')).toBeInTheDocument();
});

waitFor Waits for an expectation to be met, typically used for assertions that depend on asynchronous operations. Used when you need to wait for an element to appear, disappear, or change state in the DOM after an asynchronous operation.

import {render, screen, waitFor} from '@testing-library/react';
import MyComponent from './MyComponent';

test('loads and displays greeting', async () => {
render(<MyComponent />);

await waitFor(() => {
expect(screen.getByText('Hello, World!')).toBeInTheDocument();
});
});

Sample Tests

Here is the component we will be testing. It's a Data Table component build with React-Table v8 (typescript version!), which has a search box in one column, a custom multi-select filter in another column, and some pagination features.

Page
1 of 82
Domain
Type
Description
.abbottBrandAssociated with Abbott Laboratories, for their use
.abogadogTLDSpanish for "lawyer", used by legal professionals
.acccTLDAcademic institutions, primarily used in Ascension Island
.academygTLDEducational institutions and academies
.accountantgTLDUsed by accountants and accounting firms
.accountantsgTLDUsed by accountants and accounting firms
.activegTLDGeneral use, often associated with active lifestyles
.actorgTLDActors and acting industry
.adccTLDCountry code for Andorra, general use in advertising
.adsgTLDShort for advertisements, used in the advertising industry
domaintypedescription

and here are some tests for It

DataTable.test.js
import {
render,
screen,
waitFor,
within,
fireEvent,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import DomainsTable from '@site/src/components/tables/DomainsTable.tsx';

// smoke test
test('DataTable component loads without crashing', async () => {
// ARRANGE
render(<DomainsTable />);
});

// search renders
test('renders search input', () => {
render(<DomainsTable />);

const searchInput = screen.getByPlaceholderText(/Search\.\.\. \(\d+\)/i);

expect(searchInput).toBeInTheDocument();
});

// column search filter test
test('column search correctly filters rows', async () => {
render(<DomainsTable />);

const searchInput = screen.getByPlaceholderText(/Search\.\.\. \(\d+\)/i);

// search for '.airforce'

userEvent.click(searchInput);
userEvent.type(searchInput, '.airforce');

await waitFor(() => {
// there should be one row in the table body
const table = screen.getByRole('table');
const tbody = within(table).getByTestId('data-table-body');
const rows = within(tbody).getAllByRole('row');
const firstRow = rows[0];
const cells = within(firstRow).getAllByRole('cell');

expect(rows.length).toBe(1);
expect(cells[0]).toHaveTextContent('.airforce');
});
});

// column multiselect filter test
test('column multiselect filter correctly filters rows', async () => {
render(<DomainsTable />);

const multiSelectFilterButton = screen.getByTestId(
'multi-select-filter-button',
);

// Simulate a button click on multi-select filter
userEvent.click(multiSelectFilterButton);

// find multi-select popover
// Wait for the multi-select popover to appear
const listbox = await waitFor(() =>
screen.getByTestId('multi-select-popover'),
);

const infrastructureOption = within(listbox).getByRole('option', {
name: /infrastructure/i,
});

// click the infrastructure filter option

userEvent.click(infrastructureOption);

await waitFor(() => {
const table = screen.getByRole('table');
const tbody = within(table).getByTestId('data-table-body');
const rows = within(tbody).getAllByRole('row');
const firstRow = rows[0];
const cells = within(firstRow).getAllByRole('cell');

expect(rows.length).toBe(1);
expect(cells[1]).toHaveTextContent('infrastructure');
});
});

// pagination test
test('pagination elements have all rendered and perform expected actions', async () => {
const user = userEvent.setup();
render(<DomainsTable />);

const pagination = screen.getByRole('pagination');
const paginationButtons = within(pagination).getAllByRole('button');
const paginationInfo = screen.getByRole('pagination-info');
const paginationPageSizeSelect = screen.getByRole(
'pagination-page-size-select',
);

expect(pagination).toBeInTheDocument();
expect(paginationButtons).toHaveLength(4);
expect(paginationInfo).toBeInTheDocument();
expect(paginationPageSizeSelect).toBeInTheDocument();

// click the third pagination button (go forward one page)
user.click(paginationButtons[2]);

await waitFor(() => {
// Use a regular expression to match the text "Page 2 of x" where x is any number, ignoring spaces and case
expect(paginationInfo).toHaveTextContent(/page\s*2\s*of\s*\d+/i);
});

user.click(paginationPageSizeSelect);

const pageSizeOptions = within(paginationPageSizeSelect).getAllByRole(
'option',
);

// select show 20 rows
// I could not get this .click method to actually change the value of the select, odd
//user.click(pageSizeOptions[1]);
fireEvent.change(paginationPageSizeSelect, {target: {value: '20'}});

// make sure there are 20 rows in the table
await waitFor(() => {
const table = screen.getByRole('table');
const tbody = within(table).getByTestId('data-table-body');
const rows = within(tbody).getAllByRole('row');

// Log the rows for debugging
//console.log(rows);

// We should have 20 rows
expect(rows.length).toBe(20);
});
});

Comments

Recent Work

Free desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.

Learn More

BidBear

bidbear.io

Bidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.

Learn More