React Testing Library Part 2 - Asynchronous Tests

In the previous article we learnt about the getByText() and getByRole() queries. The getByX() queries throw an error and immediately cause tests to fail if they are not present in the DOM.

But often we need to test for elements that perhaps appear in the DOM after a certain action is performed such as api requests or another user interaction.

Queries - queryByX(), findByX()

To do this we need to use two variant queries; queryByX() and findByX().

// App.js
import { useState } from "react";

const App = () => {
  const [text, setText] = useState("Hello Sauce!");

  // Changes header text after interval of 500ms
  const handleClick = () => {
    setTimeout(() => {
      setText("Goodbye Sauce!");
    }, 500);
  };

  return (
    <div>
      <h1>{text}</h1>
      <button onClick={handleClick}>click me</button>
    </div>
  );
};

export default App;

 

queryByX - returns null if the element is not found. This can then be validated with the toBeNull() assertion.

Below we see the <h1> text is set to ‘Hello Sauce!’. Since we queryByText() for ‘Goodbye Sauce!’ it returns null and we can assert that the header text is null.

//App.test.js
import App from "./components/App";
import { render, screen } from "@testing-library/react";

test("Header should not show Goodbye yet", () => {
  // Render App
  render(<App />);

  // Attempt to extract the header element
  const header = screen.queryByText("Goodbye Sauce!");

  // Assert null as we have not clicked the button
  expect(header).toBeNull();
});

 

findByX - queries are used to query for asynchronous elements that may eventually appear in the DOM. This methods return a promise.

//App.test.js
import App from "./components/App";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

test("should show text content as Goodbye Sauce!", async () => {
  // Render App
  render(<App />);

  // Extract button node
  const button = screen.getByRole("button");

  // click button
  userEvent.click(button);

  // Wait for the text 'Goodbye Sauce!' to appear
  const header = await screen.findByText("Goodbye Sauce!");

  // Assert header to exist in the DOM
  expect(header).toBeInTheDocument();
});

The example above (App.js) has a setTimeout on the handle click meaning the ‘Goodbye Sauce!’ text does not appear immediately after the use clicks the button.

We must identify the unit test callback as async and the findByText() method is proceeded with the await keyword.

 

waitFor

The previous methods allowed us to test components that render asynchronously but to test components that disappear asynchronously there is another RTL function called waitFor().

The waitFor() function also returns a promise so we have to preface it’s call with the await keyword.

// header.js
export const Header = () => {
  const handleClick = () => {
    setTimeout(() => {
      document.querySelector("h1").remove();
    }, 250);
  };
  return (
    <div>
      <h1>Hey Sauce!</h1>
      <button onClick={handleClick}>Remove Header</button>
    </div>
  );
};

 

We combine the waitFor() method with the queryByX() methods.

 

// header.test.js
import { waitFor, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import userEvent from "@testing-library/user-event";
import { Header } from "./header.js";

test("should remove header display", async () => {
  // Render Header
  render(<Header />);

  // Extract button node
  const button = screen.getByRole("button");

  // click button
  userEvent.click(button);

  // Wait for the element to be removed asynchronously
  await waitFor(() => {
    const header = screen.queryByText("Hey Sauce!");
    expect(header).toBeNull();
  });
});

 

« RTL part 1 - Introduction to React testing library

Mikey Anson

💻 Software engineer. React enthusiast. Elm and Elixir curious 💻