React Testing Library (RTL) is a popular tool used for testing React components. Its primary goal is to help developers write tests that closely resemble how users interact with the UI.

Why Do We Need RTL Queries?

  • RTL queries are used to find elements in a rendered UI during testing.
  • They help simulate user interactions by selecting elements in a way similar to how a user would perceive them—via roles, labels, text, placeholders, etc.
  • RTL promotes accessible and semantic HTML by encouraging developers to use elements that have proper ARIA roles, labels, and attributes.

Steps in Testing UI with RTL

  1. Render the Component: Use render() from RTL to render the component.
  2. Query the Element: Use RTL queries like getByRole, getByText, or getByPlaceholderText to find elements.
  3. Simulate User Interaction: Use fireEvent or userEvent to simulate user actions.
  4. Assert the Result: Use assertions (from Jest or other testing frameworks) to check the expected outcome.

How RTL Finds Elements

RTL queries elements based on:

  1. Role (getByRole): Finds elements by ARIA roles.
  2. Text Content (getByText, getByLabelText): Finds elements by their visible text or associated labels.
  3. Attributes (getByPlaceholderText, getByDisplayValue, getByAltText): Finds elements using attributes like placeholder, value, alt, etc.

Types of RTL Queries

  1. getByRole and getAllByRole
  2. getByText and getAllByText
  3. getByLabelText and getAllByLabelText
  4. getByPlaceholderText and getAllByPlaceholderText
  5. getByDisplayValue and getAllByDisplayValue
  6. getByAltText and getAllByAltText
  7. getByTitle and getAllByTitle

Detailed Explanation of RTL Queries

1. getByRole

getByRole is one of the most important and commonly used RTL queries. It searches for elements by their ARIA role, which ensures that your UI is accessible.

What is a Role in getByRole?

Roles are accessibility attributes that describe the purpose of an element. Common roles include:

  • button: for <button> elements.
  • heading: for <h1>, <h2>, etc.
  • textbox: for <input> and <textarea> elements.

Example: Using getByRole for a Single Element

test('finds a button by role', () => {
  render(<button>Click Me</button>);
  const btn = screen.getByRole('button', { name: 'Click Me' });
  expect(btn).toBeInTheDocument();
});

Handling Multiple Elements with getByRole

When there are multiple elements with the same role, you can use the second parameter to differentiate them by their accessible name.

test('finds multiple buttons by role', () => {
  render(
    <>
      <button>Click 1</button>
      <button>Click 2</button>
    </>
  );
  const btn1 = screen.getByRole('button', { name: 'Click 1' });
  const btn2 = screen.getByRole('button', { name: 'Click 2' });

  expect(btn1).toBeInTheDocument();
  expect(btn2).toBeInTheDocument();
});

Using getAllByRole

When you want to retrieve multiple elements with the same role:

test('finds all buttons by role', () => {
  render(
    <>
      <button>Click 1</button>
      <button>Click 2</button>
    </>
  );
  const buttons = screen.getAllByRole('button');
  expect(buttons.length).toBe(2);
  buttons.forEach((btn) => expect(btn).toBeInTheDocument());
});

2. getByLabelText

getByLabelText is used to find form inputs by their associated label. This query ensures that form fields are properly labeled for accessibility.

Example: Using getByLabelText for an Input Field

test('finds an input by its label', () => {
  render(
    <div>
      <label htmlFor="name">Name</label>
      <input id="name" />
    </div>
  );
  const input = screen.getByLabelText('Name');
  expect(input).toBeInTheDocument();
});

3. getByPlaceholderText

getByPlaceholderText is useful for finding input fields by their placeholder attribute.

test('finds an input by its placeholder', () => {
  render(<input placeholder="Enter your name" />);
  const input = screen.getByPlaceholderText('Enter your name');
  expect(input).toBeInTheDocument();
});

4. getByText

getByText is used to find elements by their text content.

test('finds a paragraph by text', () => {
  render(<p>Hello World</p>);
  const paragraph = screen.getByText('Hello World');
  expect(paragraph).toBeInTheDocument();
});

5. getByDisplayValue

getByDisplayValue finds input elements by their current value.

test('finds an input by its display value', () => {
  render(<input value="John Doe" readOnly />);
  const input = screen.getByDisplayValue('John Doe');
  expect(input).toBeInTheDocument();
});

6. getByAltText

getByAltText is used to find images by their alt attribute, ensuring images are accessible.

test('finds an image by alt text', () => {
  render(<img src="logo.png" alt="Company Logo" />);
  const img = screen.getByAltText('Company Logo');
  expect(img).toBeInTheDocument();
});

Handling Non-Semantic Elements

If you have non-semantic elements (like <div> or <span>), you may need to add custom roles or use data-testid attributes.

test('finds a div with a custom role', () => {
  render(<div role="alert">This is an alert</div>);
  const alert = screen.getByRole('alert');
  expect(alert).toBeInTheDocument();
});

Overriding data-testid

You can override the default data-testid query by configuring RTL:

import { render, screen, configure } from '@testing-library/react';

configure({ testIdAttribute: 'custom-id' });

test('finds an element by custom test id', () => {
  render(<div custom-id="test-div">Hello</div>);
  const div = screen.getByTestId('test-div');
  expect(div).toBeInTheDocument();
});

Conclusion

  • RTL queries ensure that tests are written in a way that reflects real user interactions.
  • The most commonly used query is getByRole because it encourages accessible design.
  • Each query has a specific use case, and understanding them will help you write more robust UI tests.


7. getAllByRole

getAllByRole is used to retrieve an array of all elements matching a specific role. This is especially useful when there are multiple elements of the same type (e.g., multiple buttons, inputs, etc.).

Why Do We Need getAllByRole?

  • When multiple elements share the same role, getByRole only retrieves the first matching element.
  • To test scenarios where you need to interact with or assert multiple elements, getAllByRole is necessary.

Example: Using getAllByRole to Retrieve Multiple Buttons

test('finds all buttons by role', () => {
  render(
    <>
      <button>Button 1</button>
      <button>Button 2</button>
    </>
  );
  
  const buttons = screen.getAllByRole('button');
  
  // Check that two buttons are present
  expect(buttons.length).toBe(2);
  
  // Loop through the buttons array and assert they are in the document
  buttons.forEach((button) => expect(button).toBeInTheDocument());
});

8. getAllByText

getAllByText retrieves all elements containing the specified text content.

Example: Using getAllByText

test('finds all paragraphs by text', () => {
  render(
    <>
      <p>Hello World</p>
      <p>Hello World</p>
    </>
  );

  const paragraphs = screen.getAllByText('Hello World');
  expect(paragraphs.length).toBe(2);
  
  paragraphs.forEach((para) => expect(para).toBeInTheDocument());
});

9. getAllByLabelText

getAllByLabelText is used when you have multiple input fields with the same label or similar labels.

Example: Using getAllByLabelText for Multiple Input Fields

test('finds multiple input fields by their labels', () => {
  render(
    <>
      <label htmlFor="input1">Name</label>
      <input id="input1" />
      <label htmlFor="input2">Name</label>
      <input id="input2" />
    </>
  );

  const inputs = screen.getAllByLabelText('Name');
  expect(inputs.length).toBe(2);

  inputs.forEach((input) => expect(input).toBeInTheDocument());
});

10. getAllByPlaceholderText

getAllByPlaceholderText retrieves all input fields with a specific placeholder attribute.

Example: Using getAllByPlaceholderText

test('finds multiple input fields by placeholder text', () => {
  render(
    <>
      <input placeholder="Enter your name" />
      <input placeholder="Enter your name" />
    </>
  );

  const inputs = screen.getAllByPlaceholderText('Enter your name');
  expect(inputs.length).toBe(2);

  inputs.forEach((input) => expect(input).toBeInTheDocument());
});

11. getAllByAltText

getAllByAltText is useful when you have multiple images with the same or similar alt attributes.

Example: Using getAllByAltText for Multiple Images

test('finds multiple images by alt text', () => {
  render(
    <>
      <img src="image1.png" alt="Product Image" />
      <img src="image2.png" alt="Product Image" />
    </>
  );

  const images = screen.getAllByAltText('Product Image');
  expect(images.length).toBe(2);

  images.forEach((img) => expect(img).toBeInTheDocument());
});

12. getAllByTitle

getAllByTitle retrieves all elements with a specific title attribute.

Example: Using getAllByTitle

test('finds multiple elements by title', () => {
  render(
    <>
      <div title="Tooltip">Hover over me</div>
      <div title="Tooltip">Hover over me too</div>
    </>
  );

  const elements = screen.getAllByTitle('Tooltip');
  expect(elements.length).toBe(2);

  elements.forEach((element) => expect(element).toBeInTheDocument());
});

Handling Multiple Buttons with the Same Role

In cases where you have multiple buttons with the same role, you can differentiate them using the name option.

Example: Handling Multiple Buttons by Role

test('finds multiple buttons by role with different names', () => {
  render(
    <>
      <button>Submit</button>
      <button>Cancel</button>
    </>
  );

  const submitButton = screen.getByRole('button', { name: 'Submit' });
  const cancelButton = screen.getByRole('button', { name: 'Cancel' });

  expect(submitButton).toBeInTheDocument();
  expect(cancelButton).toBeInTheDocument();
});

Handling Multiple Input Fields with Labels

When you have multiple input fields, use the label and htmlFor attributes properly so getByLabelText can differentiate between them.

Example: Differentiating Input Fields by Label

test('finds multiple input fields by different labels', () => {
  render(
    <>
      <label htmlFor="email">Email</label>
      <input id="email" />
      
      <label htmlFor="password">Password</label>
      <input id="password" />
    </>
  );

  const emailInput = screen.getByLabelText('Email');
  const passwordInput = screen.getByLabelText('Password');

  expect(emailInput).toBeInTheDocument();
  expect(passwordInput).toBeInTheDocument();
});

Why So Many Methods?

RTL provides many query methods because:

  1. Different elements may require different ways of identification (by text, label, role, etc.).
  2. To encourage developers to use semantic HTML and proper accessibility practices.
  3. To support various use cases in testing, including finding single or multiple elements, form inputs, images, etc.

Conclusion

  • getBy* vs getAllBy*: Use getBy* for single elements and getAllBy* for multiple elements.
  • Always prefer queries like getByRole and getByLabelText for better accessibility.
  • Use getByPlaceholderText and getByDisplayValue for form fields when labels aren’t available.
  • Differentiating multiple elements can be done using the name option or labels.