🔝

React Test npm install react-test test badge gzip size

Expressive testing library for React to make sure your code works as expected:

import $ from 'react-test';

it('increments when clicked', async () => {
  const counter = $(<Counter />);
  expect(counter).toHaveText('0');
  await counter.click();
  expect(counter).toHaveText('1');
});

The react-test syntax follows a similar schema to jQuery so it's very easy to write expressive tests. It also adds some Jest matchers (if you are using Jest) for convenience.

Early package! We are looking for beginner Open Source contributors! ❤️

Getting Started

First you'll need a working React project. As an example you can start a working React project with Create React App:

npx create-react-app my-app
cd my-app

Then install react-test. It is only needed for development:

npm install react-test --save-dev

Finally you can write tests. Let's say you have the <Counter /> component from this example and you want to test it to make sure it works as expected:

// src/Counter.js
import React, { useState } from 'react';

export default function Counter() {
  const [counter, setCounter] = useState(0);
  const increment = () => setCounter(counter + 1);
  return <button onClick={increment}>{counter}</button>;
}
// src/Counter.test.js
import React from 'react';
import $ from 'react-test';
import Counter from './Counter';

describe('Counter.js', () => {
  it('is initialized to 0', () => {
    const counter = $(<Counter />);
    expect(counter.text()).toBe('0');
  });

  it('can be incremented with a click', async () => {
    const counter = $(<Counter />);
    await counter.click();
    expect(counter.text()).toBe('1');
  });

  it('can be incremented multiple times', async () => {
    const counter = $(<Counter />);
    await counter.click();
    await counter.click();
    await counter.click();
    expect(counter.text()).toBe('3');
  });
});

Finally run the tests with Jest:

npm run test

Basics of testing

React applications are divided in components, and these components can be tested either individually or in group. Self-contained components are easier to test, document and debug.

For example, a plain button can be defined with a callback function, and change colors depending on the primary attribute:

import React from 'react';

export default function Button({ primary, onClick, children }) {
  const background = primary ? 'blue' : 'gray';
  return (
    <button onClick={onClick} style={{ background }}>
      {children}
    </button>
  );
}

Then we can test it with react-test by creating a Button.test.js file and adding some assertions:

import React from 'react';
import $ from 'react-test';
import Button from './Button';

describe('Button.js', () => {
  it('has different backgrounds depending on the props', () => {
    const $button = $(<Button>Hello</Button>);
    expect($button).toHaveStyle('background', 'gray');
    const $primary = $(<Button primary>Hello</Button>);
    expect($primary).toHaveStyle('background', 'blue');
  });

  it('can be clicked', async () => {
    const fn = jest.fn();
    const $button = $(<Button onClick={fn}>Hello</Button>);
    expect(fn).not.toBeCalled();
    await $button.click();
    expect(fn).toBeCalled();
  });

  // FAILS
  it("cannot be clicked if it's disabled", async () => {
    const fn = jest.fn();
    const $button = $(
      <Button onClick={fn} disabled>
        Hello
      </Button>
    );
    await $button.click();
    expect(fn).not.toBeCalled(); // ERROR!
  });
});

Great! All of our tests are working except for the last one. Now we can go back to our component and fix it:

import React from 'react';

export default function Button({ primary, onClick, children, ...props }) {
  const background = primary ? 'blue' : 'gray';
  return (
    <button onClick={onClick} style={{ background }} {...props}>
      {children}
    </button>
  );
}

FAQ

Is this an official Facebook/React library?

No. This follows the community convention of calling a library related to React as react-NAME. It is made by these contributors without any involvement of Facebook or React.

How can I contribute?

Thanks! Please read the Contributing Guide where we explain how to get started with the project. Right now there are some beginner-friendly issues so please feel free to implement those!

I will try to help as much as possible on the PRs.

I have a problem, how do I fix it?

Don't sweat it, just open an issue. React Test is in an early phase with incomplete documentation so feel free to read the code or ask directly in the issues.

This will change once the library is more stable, there's more documentation and if the community grows (maybe a chat, or reddit group, or ...).

How did you get react-test?

I've written a blog post about this, but the gist of it is that the npm package was taken by Deepstream.io before but not used. So I asked politely and they allowed me to use it.

How is this different from React Testing Library?

This is a difficult one. First, React Testing Library, the documentation and the work from @kentcdodds and other collaborators is amazing and I've learned a lot from it. The main differences are:

The syntax follows jQuery-style chaining:

// react-test
import $ from 'react-test';
test('Increments when clicked', async () => {
  const $counter = $(<Counter />);
  expect($counter).toHaveText('0');
  await $counter.click();
  expect($counter).toHaveText('1');
});

// react testing library
import { render, fireEvent } from '@testing-library/react';
test('Increments when clicked', () => {
  const { getByRole, container } = render(<Counter />);
  expect(container).toHaveTextContent('0');
  fireEvent.click(getByRole('button'));
  expect(container).toHaveTextContent('1');
});

React Test is a work in progress, so if you are writing tests for production right now please use one of the better known alternatives.

jQuery syntax, ewwh

That's not really a question! But if for some reason you deeply despise those dollars, perhaps because they remind you of PHP, you can avoid them altogether:

import render from 'react-test';

test('Increments when clicked', async () => {
  const counter = render(<Counter />);
  expect(counter).toHaveText('0');
  await counter.click();
  expect(counter).toHaveText('1');
});

We obviously love React, but let's not forget that jQuery also has some great things as well. This library brings some of these nice things to react testing.

Library API

The main export is a function which we call $ and accepts a React element or fragment:

import $ from "react-test";
const button = $(<button>Hello world</button>);
expect(button.text()).toBe("Hello world");
DOM navigation Read data Promises (Events+timing)
.children() .attr() .change()
.closest() .data() .click()
.each() .get() .delay()
.filter() .html() .submit()
.find() .is() .trigger()
.first() .text() .type()
.last() .toArray()
.parent()
.siblings()

Since the API is inspired on jQuery we call React Test $, but you can call it render or anything you prefer.

You cannot modify the DOM directly with this library, but you can trigger events that, depending on your React components, might modify the DOM:

const Greeter = () => {
  const [name, setName] = useState();
  return (
    <div>
      Hello {name || "Anonymous"}
      <input onChange={(e) => setName(e.target.value)} />
    </div>
  );
};

it("can type in an input", async () => {
  const greet = $(<Greeter />);
  expect(greet.text()).toBe("Hello Anonymous");
  await greet.find("input").type("Francisco");
  expect(greet.text()).toBe("Hello Francisco");

  // ERROR! this or any similar workflow doesn't work as expected!
  greet.find("input").get(0).value = "John";
});

.attr()

Read the attributes of the first matched element:

.attr("name")

It will read the attributes of the matched elements:

const $input = $(<input name="email" placeholder="me@example.com" />);
expect($input.attr("name")).toBe("email");
expect($input.attr("placeholder")).toBe("me@example.com");

You can select items with a CSS selector that matches attributes:

const $form = $(<LoginForm />);
const $firstName = $form.find('[name="firstname"]');
expect($firstName).toHaveValue("");
await $firstName.type("John");
expect($firstName).toHaveValue("John");

If you are asserting things, you might prefer .toHaveAttribute() instead of the above:

const $input = $(<input name="email" placeholder="me@example.com" />);
expect($input).toHaveAttribute("name", "email");
expect($input).toHaveAttribute("placeholder", "me@example.com");

Example: external links have "noopener noreferrer"

This security feature makes sure no one can hijack your page's data:

// Find all of the external links first
const $links = $(<Page />).find("a[target=_blank]");

// Make sure they follow the schema
for (let link of $links) {
  expect($(link).attr("rel")).toBe("noopener noreferrer");
}

When .toHaveAttribute() is available, you can shorten it:

// Find all of the external links first
const $links = $(<Page />).find("a[target=_blank]");

// Make sure they *all* have rel="noopener noreferrer"
expect($links).toHaveAttribute("rel", "noopener noreferrer");

.change()

.change(value) -> promise

Simulates a change in an element, like an input. It should be awaited for the side effects to run and the component to re-rendered:

it("can change the current element value", async () => {
  const input = $(<input defaultValue="hello" />);
  expect(input).toHaveValue("hello");
  await input.change("world");
  expect(input).toHaveValue("world");
});

It works on elements of type <input>, <textarea> and <select>.

.change() already wraps the call with act(), so there's no need for you to also wrap it. Just make sure to await for it.

Parameters

value: the new value for the element. If it's a text input, textarea or select, it should be a string. If it's a checkbox or radio, it should be a true/false boolean.

Returns

A promise that must be awaited before doing any assertion.

Examples

Simple way to test that the input text can be changed:

it("works with inputs", async () => {
  const input = $(<input defaultValue="hello" />);
  expect(input).toHaveValue("hello");
  await input.change("Francisco");
  expect(input).toHaveValue("Francisco");
});

For checkboxes it should receive a true/false:

it("works with inputs", async () => {
  const input = $(<input type="checkbox" />);
  expect(input.get(0).checked).toBe(false);
  await input.change(true);
  expect(input.get(0).checked).toBe(true);
});

Notes

Expect this component to change in the future, since its behavior now is complex and inconsistent. So in the future we will do either of these:

  • Make it more complex AND consistent, e.g. accept numbers for <input type="number" />, a text option for type="radio" (with validation), etc.
  • Split into different methods, each one being simpler, e.g. .text(newValue) for text inputs, .check, .check(false) or .uncheck() for checkboxes, .pick(opt) for selects, etc.
  • Other?

It is internally wrapping the call with act(), so there's no need for you to also wrap it. Just make sure to await for it.

.children()

Get the direct children of all of the elements with an optional filter:

.children(filter: string)

Parameters

filter: A selector expression to match elements against

Returns

An instance of react-test with the new children as itst elements.

Usage

Since we return an instance of react-test, we have to use .toArray() to convert it to an array so that we can iterate through them.

import $ from "react-test";

const List = () => (
  <ul>
    <li>A</li>
    <li>B</li>
  </ul>
);

// Find the text of each element
const text = $(<List />)
  .children()
  .toArray()
  .map((item) => $(item).text());

expect(text).toEqual(["A", "B"]);

.click()

.click() -> promise

Simulates a click on all the matched elements. It should be awaited for the side effects to run and the component to re-rendered:

it("clicks the current element", async () => {
  const counter = $(<Counter />);
  expect(counter.text()).toEqual("0");
  await counter.click();
  expect(counter.text()).toEqual("1");
});

.click() already wraps the call with act(), so there's no need for you to also wrap it. Just make sure to await for it.

Parameters

None. Any parameters passed will be ignored.

Returns

A promise that must be awaited before doing any assertion.

Examples

We might want to click a child element and not the top-level one:

it("clicks all buttons inside", async () => {
  const counter = $(<Counter />);
  expect(counter.text()).toEqual("0");
  await counter.find("button").click();
  expect(counter.text()).toEqual("1");
});

We can submit a form by clicking on a button inside it:

const CreateUser = ({ onSubmit }) => (
  <form
    onSubmit={(e) => {
      e.preventDefault(); // <- this is required _when testing_
      onSubmit();
    }}
  >
    <input name="firstname" />
    <input name="lastname" />
    <input name="age" />
    <button>Submit</button>
  </form>
);

it("submits the form when clicking the button", async () => {
  const onSubmit = jest.fn();
  const createUser = $(<CreateUser onSubmit={onSubmit} />);
  expect(onSubmit).not.toBeCalled();
  await createUser.find("button").click();
  expect(onSubmit).toBeCalled();
});

Notes

It is internally wrapping the call with act(), so there's no need for you to also wrap it. Just make sure to await for it.

.closest()

Find the first ancestor that matches the selector for each element:

.closest(selector: string)

Parameters

selector: Expression to match elements against

Returns

An instance of react-test with the new elements as nodes

Usage

Since we return an instance of react-test, we have to use .toArray() to convert it to an array so that we can iterate through them.

import $ from "react-test";

const List = () => (
  <ul>
    <li>
      <a>Hello</a>
    </li>
    <li>
      <a>World</a>
    </li>
  </ul>
);

const names = $(<List />)
  .find("a")
  .closest("li")
  .toArray()
  .map((node) => node.nodeName);

expect(names).toEqual(["LI", "LI"]);

.data()

Reads the data-attribute value for the matched element.

.data(name);

Parameters

name: the data-* attribute that we want to get from the first matched element.

Return

A string containing the value stored in the targeted data-* attribute.

Examples

Find the value of the attribute data-id:

const Hello = () => <div data-id="0">Hello World!</div>;
$(<Hello />)
  .first()
  .data("id"); //0

.delay()

Makes the component to wait for a determined period of time for async actions to happen. It will be called wrapped in "act()" inside.

.delay(time);  // milliseconds

Parameters

time: the amount of time the component will wait in milliseconds.

Return

A plain promise that needs to be awaited.

Examples

A component that changes after 1 second:

const Updater = () => {
  const [text, setText] = useState("initial");
  useEffect(() => {
    setTimeout(() => setText("updated"), 1000);
  }, []);
  return <div>{text}</div>;
};

For testing, we check the initial value and the value after 2 seconds:

const updater = $(<Updater />);
expect(updater.text()).toBe("initial");
await updater.delay(2000);
expect(updater.text()).toBe("updated");

.each()

We are looking for beginner Open Source Contributors to implement this method:

.filter()

Return all of the given nodes that match the provided selector

.filter(selector);

Parameters

selector: a string containing a selector that nodes must match.

Return

An instance of React Test with only the matching nodes.

Examples

Filter to select list items with child links from the contact page:

$(<ContactPage />)
  .find("a")
  .parent()
  .filter("li");

.find()

Get all of the descendants of the nodes with an optional filter

.find(filter);

Parameters

filter: a string containing a selector that nodes must pass or a function that return a boolean. See .filter() for a better explanation

Return

An instance of React Test with the new children as nodes

Examples

Find all the links in the contact page:

$(<ContactPage />).find("a");

Get the required fields within a submitting form:

u('form').on('submit', function(e){
  var required = u(this).find('[required]');
});

.first()

Retrieve the first of the matched nodes:

.first();
const $list = $(
  <ul>
    <li>Bananas</li>
    <li>Oranges</li>
  </ul>
);

const item = $list.find("li").first();
expect(item.textContent).toBe("Bananas");

Parameters

This method doesn't accept any parameters

Return

The first html node or null if there is none.

Examples

Retrieve the first element of a list:

const $list = $(
  <ul>
    <li>Bananas</li>
    <li>Oranges</li>
  </ul>
);

const item = $list.find("li").first();
expect(item.textContent).toBe("Bananas");

Related

.last() retrieve the last matched element

.get()

We are looking for beginner Open Source Contributors to implement this method:

.html()

Retrieve the html of the elements:

.html()

.is()

Check whether any of the nodes matches the selector:

.is('a')
.is('.active')

.last()

Retrieve the last of the matched nodes

.last();
const $list = $(
  <ul>
    <li>Bananas</li>
    <li>Oranges</li>
  </ul>
);

const item = $list.find("li").last();
expect(item.textContent).toBe("Oranges");

Parameters

This method doesn't accept any parameters

Return

The last html node or null if there is none.

Examples

Retrieve the last element of a list:

const $list = $(
  <ul>
    <li>Bananas</li>
    <li>Oranges</li>
  </ul>
);

const item = $list.find("li").first();
expect(item.textContent).toBe("Oranges");

Related

.first() retrieve the last matched element

.parent()

Return the parent node(s) of the given node(s)

.parent();

Parameters

None.

Return

An instance of React Test with the parent node(s).

Examples

Find the parent node of all anchor tags:

const $list = $(
  <ul className="boo">
    <li className="bar">
      <a href="#" className="baz">
        Link 1
      </a>
    </li>
    <li className="foo">
      <a href="#" className="bar">
        Link 2
      </a>
    </li>
  </ul>
);

const parents = $list.find("a").parent();
expect(parents.nodes).toHaveLength(2);

.siblings()

We are looking for beginner Open Source Contributors to implement this method:

.submit()

.submit() -> promise

Simulates a form submission on all the matched forms. It should be awaited for the side effects to run and the component to re-rendered (if needed). For example, let's say that you have this form component:

const CreateUser = ({ onSubmit }) => (
  <form
    onSubmit={(e) => {
      e.preventDefault(); // <- this is required _when testing_
      onSubmit();
    }}
  >
    <input name="firstname" />
    <input name="lastname" />
    <input name="age" />
    <button>Submit</button>
  </form>
);

Then you can test that the form is properly submitted like this:

it("can mock submitting a form", async () => {
  const onSubmit = jest.fn();
  const createUser = $(<CreateUser onSubmit={onSubmit} />);
  expect(onSubmit).not.toBeCalled();
  await createUser.submit();
  expect(onSubmit).toBeCalled();
});

.submit() already wraps the call with act(), so there's no need for you to also wrap it. Just make sure to await for it.

onSubmit should always call e.preventDefault(), since the browser behavior has not been imitated by this library.

Parameters

None. Any parameters passed will be ignored.

Returns

A promise that must be awaited before doing any assertion.

Examples

We can also test the submission by e.g. clicking on a button on a child node:

const CreateUser = ({ onSubmit }) => (
  <form
    onSubmit={(e) => {
      e.preventDefault(); // <- this is required _when testing_
      onSubmit();
    }}
  >
    <input name="firstname" />
    <input name="lastname" />
    <input name="age" />
    <button>Submit</button>
  </form>
);

it("submits the form when clicking the button", async () => {
  const onSubmit = jest.fn();
  const createUser = $(<CreateUser onSubmit={onSubmit} />);
  expect(onSubmit).not.toBeCalled();
  await createUser.find("button").click();
  expect(onSubmit).toBeCalled();
});

Notes

It is internally wrapping the call with act(), so there's no need for you to also wrap it. Just make sure to await for it.

.text()

Retrieve the textContent of the elements:

.text()

Whitespace is normalized for easy comparison:

$(
  <div>
    Hello <br /> world
  </div>
).text();
// "Hello world"

.toArray()

Get all of the currently matched nodes as a plain array:

const List = () => (
  <ul>
    <li>A</li>
    <li>B</li>
  </ul>
);

// Find the text of each node
const text = $(<List />)
  .children()
  .toArray()
  .map((item) => $(item).text());

expect(text).toEqual(["A", "B"]);

.trigger()

Trigger an event. Right now it only accepts 'click'.

.trigger()

.type()

.type(text) -> promise

Simulates typing the text on all the matched elements. It should be awaited for the side effects to run and the component to re-rendered:

const input = $(<Input />);
expect(input).toHaveValue("");
await input.type("Francisco");
expect(input).toHaveValue("Francisco");

Note that this simulates typing the text letter by letter, so it's useful to test more complex interactions. If you want to test a simpler onChange, you might instead use .change(text).

.type() already wraps the call with act(), so there's no need for you to also wrap it. Just make sure to await for it.

Parameters

  • text: the new value to be sent to the callback as part of the event.

Returns

A promise that must be awaited before doing any assertion.

Examples

A simple controlled input:

const Input = () => {
  const [text, setText] = useState("");
  return <input value={text} onChange={(e) => setText(e.target.value)} />;
};

const input = $(<Input />);
expect(input).toHaveValue("");
await input.type("Francisco");
expect(input).toHaveValue("Francisco");

A full component greeting whoever it's there:

const Greeter = () => {
  const [name, setName] = useState("");
  return (
    <div>
      Hello {name || "Anonymous"}
      <input value={name} onChange={(e) => setName(e.target.value)} />
    </div>
  );
};

it("clicks the current element", async () => {
  const greet = $(<Greeter />);
  expect(greet.text()).toBe("Hello Anonymous");
  await greet.find("input").type("Francisco");
  expect(greet.text()).toBe("Hello Francisco");
});

Notes

It is internally wrapping the call with act(), so there's no need for you to also wrap it. Just make sure to await for it.

Helpers

until()

This is a very useful function. It's really flexible on what it does, the basics is that it keeps waiting until some condition is met. For example, let's say that you have a timer that changes its class to active after 2s:

const timer = $(<Timer />);

expect(timer).not.toHaveClass("active");
await until(() => timer.is(".active")); // Wait until it becomes active
expect(timer).toHaveClass("active");

However it's very flexible. It is triggered and continues executing when:

  • Receiving a callback, that callback returns true or a truthy value.
  • Receiving a component, we can execute actions like .is('.active') afterwards.
  • Receiving a component, we can execute actions like .find('.active') and it'll be truthy when it finds at least 1 that matches the query.

Some examples:

// Keep pinging until the callback returns a truthy value
await until(() => timer.is(".active"));

// DATA methods, when one becomes "true" (or non-empty) it finishes executing
await until(timer).is(".active"); // Same, finish when timer gets the class
await until(timer).text(); // When the timer returns any text, finish

// DOM MANIPULATION methods; when it returns a non-empty collection it finishes
await until(timer).filter(".active"); // Same, finish when timer gets the class
await until(timer).children("li"); // Finishes when the first <li> is appended

// DOM MANIPULATION + chaining with DATA methods
// Finish when the container becomes active
await until(timer).find(".container").is(".active");
// Any child becomes active
await until(timer).children().is(".active");

// DOM MANIPULATION + chaining with other DOM MANIPULATION methods:
// Finishes when finding an active link
await until(timer).find("a").filter(".active");
// Finishes when one important child becomes active
await until(timer).children().filter(".important.active");

When there is a "DOM Manipulation" method, it'll finish executing when it returns a non-zero collection of items.

When it's reading data, it'll finish executing when it returns truthy.

Jest Matchers

These helpers are automatically available if you are using jest:

const $button = $(<button className="primary">Click me!</button>);

expect($button).toMatchSelector("button");
expect($button).toHaveClass("primary");
expect($button).toHaveText("Click me!");

This will give much more meaningful errors when failing compared to doing things manually:

// With the Jest Helpers:
expect($button).toHaveClass("secondary");
// Expected <button class="primary"> to include class "secondary"

// Without the helpers:
expect($button.attr("class")).toBe("secondary");
// Expected "primary" to be "secondary"

These expect() matchers work with either a single element or multiple elements, but it's important that you understand the differences in behavior.

When there's a single element in the expect(), then the .not Jest negation makes them a perfect opposite:

// Make sure the button has the class "primary"
expect($button).toHaveClass("primary");

// Make sure the button does NOT have the class "secondary"
expect($button).not.toHaveClass("secondary");

However when there are multiple elements in the expect(), the test follows the English meaning instead of plainly negating the affirmative statement:

// All of them have this class
expect($list.find("li")).toHaveClass("item");

// NONE of the items have the given class
expect($list.find("li")).not.toHaveClass("hidden");

React Test makes Jest's not behave as "NONE" instead of ~not all~, since we found most of the times this is the desired behavior.

.toBeEnabled()

Check whether none of the matched elements have the attribute "disabled":

const $button = $(<button />);
const $input = $(<input disabled />);

expect($button).toBeEnabled();
expect($input).not.toBeEnabled();

For a list of items, it checks whether all of them are enabled or all of them are disabled:

const $form = $(
  <form>
    <input id="banana" />
    <input id="orange" />

    <textarea id="apple" />
    <textarea id="pear" disabled />

    <button id="mango" disabled />
    <button id="coconut" disabled />
  </form>
);

// All of them are enabled
expect($form.find("input")).toBeEnabled();

// All of them are disabled
expect($form.find("button")).not.toBeEnabled();

For the same React code, these do not pass:

// ERROR! Only one of them is enabled
expect($form.find("textarea")).toBeEnabled();
// Expected <textarea id="pear" disabled=""> not to include the attribute "disabled"

// ERROR! At least one of them is enabled
expect($form.find("textarea")).not.toBeEnabled();
// Expected <textarea id="apple"> to include the attribute "disabled"

.toHaveAttribute()

Check whether the matched elements contain the attribute && value

it("has attribute and value", () => {
  const $button = (
    <button type="submit" disabled>
      click
    </button>
  );

  expect($button).toHaveAttribute("type", "submit");
  expect($button).toHaveAttribute("disabled");
});

It checks whether the matched elements do not contain the attribute

it("does not have the attribute and value", () => {
  const $button = (
    <button type="submit" disabled>
      click
    </button>
  );

  expect($button).not.toHaveAttribute("onclick");
  expect($button).not.toHaveAttribute("type", "reset");
});

It checks whether the matched elements contain the attribute and the matched regex value

it("checks if attribute has given regex value", () => {
  const $button = (
    <button type="submit" disabled>
      click
    </button>
  );

  // Positive assertions: all the given regex values match
  expect($button).toHaveAttribute("type", /submit/);
  expect($button).toHaveAttribute("type", /su?b.+/);
  expect($button).toHaveAttribute("type", /.*/);

  // Negative assertions: all the given regex values do not match
  expect($button).not.toHaveAttribute("type", /sub/);
  expect($button).not.toHaveAttribute("type", /su?b/);
  expect($button).not.toHaveAttribute("type", /.*q/);
});

For a list of items, it checks whether all have the same attribute && value or regex

const $list = $(
  <ul>
    <li value="1" title="list-item">
      apple
    </li>
    <li value="2" title="list-item">
      apple
    </li>
  </ul>
);

// PASS
expect($list.find("li")).toHaveAttribute("value");
expect($list.find("li")).toHaveAttribute("title", "list-item");
expect($list.find("li")).toHaveAttribute("title", /list-item/);
expect($list.find("li")).toHaveAttribute("title", /^li.t-.*/);

// DO NOT PASS
expect($list.find("li")).toHaveAttribute("error");
expect($list.find("li")).toHaveAttribute("id");
expect($list.find("li")).toHaveAttribute("value", "1");
expect($list.find("li")).toHaveAttribute("title", /list/);

For a list of items, it checks whether any do not have the same attribute && value or regex

const $list = $(
  <ul>
    <li id="first" value="1" title="list-item">
      apple
    </li>
    <li value="2" title="list-item">
      apple
    </li>
  </ul>
);

// PASS
expect($list.find("li")).not.toHaveAttribute("error");
expect($list.find("li")).not.toHaveAttribute("value", "1");
expect($list.find("li")).not.toHaveAttribute("title", /list/);

// DO NOT PASS
expect($list.find("li")).not.toHaveAttribute("value");
expect($list.find("li")).not.toHaveAttribute("value", "1");
expect($list.find("li")).not.toHaveAttribute("title", /^list-.*/);

.toHaveClass()

Check whether all of the matched elements have the expected class name:

const $button = $(<button className="primary">Click me</button>);

expect($button).toHaveClass("primary");
expect($button).not.toHaveClass("secondary");

For list of items, it checks whether all of them match or none of them match:

const $list = $(
  <ul>
    <li className="item main">a</li>
    <li className="item secondary">b</li>
  </ul>
);

// All of them have the class item
expect($list.find("li")).toHaveClass("item");

// None of them has the class "primary"
expect($list.find("li")).not.toHaveClass("primary");

For the same React code, these do not pass:

// ERROR! Only one of them has the class "main"
expect($list.find("li")).toHaveClass("main");
// Expected <li class="item secondary"> to include class "main"

// ERROR! At least one of them has the class "main"
expect($list.find("li")).not.toHaveClass("main");
// Expected <li class="item main"> not to include class "main"

.toHaveHtml()

Checks whether the selected elements have HTML

const $div = $(
  <div>
    <span>I am a span</span>
  </div>
);

expect($div).toHaveHtml("<span>I am a span</span>");

Checks whether the selected elements do not have HTML

const $div = $(
  <div>
    <span>I am a span</span>
  </div>
);

expect($div).not.toHaveHtml("<li>I am a list item</li>");

Trims passed HTML

const $div = $(
  <div>
    <span>I am a span</span>
  </div>
);

expect($div).toHaveHtml("<span>I am a span</span>        ");

Validates across different depth levels of inner HTML

const $div = $(
  <div>
    <span>
      I am a <b>span</b>
    </span>
  </div>
);

expect($div).toHaveHtml("<div><span>I am a <b>span</b></span></div>");
expect($div).toHaveHtml("<span>I am a <b>span</b></span>");
expect($div).toHaveHtml("<b>span</b>");

For a list of elements, checks if all the elements have HTML

const $body = $(
  <body>
    <div>
      <span>span text</span>
    </div>
    <div>
      <span>span text</span>
    </div>
  </body>
);

// PASS
expect($body.find("div")).toHaveHtml("<span>span text<span>");
expect($body.find("div")).toHaveHtml("span text");

// DO NOT PASS
expect($body.find("div")).toHaveHtml("<li>item</li>");
expect($body.find("div")).toHaveHtml("<p>text</p>");

For a list of elements, checks if any of the elements do not have HTML

const $body = $(
  <body>
    <div>
      <span>text</span>
    </div>
    <div>
      <p>text</p>
    </div>
  </body>
);

// PASS
expect($body.find("div")).not.toHaveHtml("<h1>header</h1>");
expect($body.find("div")).not.toHaveHtml("<li>item</li>");
expect($body.find("div")).not.toHaveHtml("<span>random text</span>");

// DO NOT PASS
expect($body.find("div")).not.toHaveHtml("<span>text</span>");
expect($body.find("div")).not.toHaveHtml("<p>text</p>");
expect($body.find("div")).not.toHaveHtml("text");

.toHaveStyle()

Check whether all of the matched elements have the expected styles applied:

const $button = $(<button></button>);

// Check for presence of styles using style object as an argument
expect($button).toHaveStyle({ backgroundColor: "red", textAlign: "center" });
expect($button).toHaveStyle({ textAlign: "center" });

// Check for presence of styles using style string as an argument
expect($button).toHaveStyle("background-color: red; text-align: center;");
expect($button).toHaveStyle("text-align: center;");

// Check for absence of particular styles
expect($button).not.toHaveStyle("display: none;");
expect($button).not.toHaveStyle({ display: "none" });
const $list = $(
  <ul>
    <li style={{ ...styleObj, color: "red" }}></li>
    <li style={{ ...styleObj, color: "red" }}></li>
  </ul>
);

// All of the matching elements have the searched for styles
expect($list.find("li")).toHaveStyle({ color: "red" });
expect($list.find("li")).toHaveStyle("color: red");

// None of the matching elements have the searched for styles
expect($list.find("li")).not.toHaveStyle({ color: "green" });
expect($list.find("li")).not.toHaveStyle("color: green");

.toHaveText()

Check whether the matched elements all contain the text (see the Counter example):

it("can be clicked", async () => {
  const $counter = <Counter />;
  expect($counter).toHaveText("0");
  await $counter.click();
  expect($counter).toHaveText("1");
});

It normalizes whitespace so that multiple spaces or enters are collapsed into a single one:

it("normalizes whitespace", () => {
  const $text = $(
    <div>
      Hello <br /> world!
    </div>
  );
  expect($text).toHaveText("Hello world!");
});

For list of items, it checks whether all of them match or none of them match:

const $list = $(
  <ul>
    <li>apple</li>
    <li>apple</li>
  </ul>
);
// Passes; all of them have the given text
expect($list.find("li")).toHaveText("apple");

// Passes; none of them has the given text
expect($list.find("li")).not.toHaveText("banana");

These examples do not pass:

const $list = $(
  <ul>
    <li>apple</li>
    <li>banana</li>
  </ul>
);

// ERROR! Because only one of them has the text "apple"
expect($list.find("li")).toHaveText("apple");
// Expected <li> to have text "apple" but it received "banana"

// ERROR! Because at least one of them has the text "apple"
expect($list.find("li")).not.toHaveText("apple");
// Expected <li> not to have the text "apple"

.toHaveValue()

  • Checks whether the element has the given value.
  • Only works for input, textarea, and select tags.
  • For input types of checkbox and radio, please use .checked instead.

Checks whether the form element has the given value:

const $input = $(<input type="text" value="textValue" onChange={} />);
expect($input).toHaveValue('textValue');

Checks defaultValue if set on element:

const $input = $(<input type="text" defaultValue="initial text" />);
const $textarea = $(<textarea defaultValue="initial textarea" />);

expect($input).toHaveValue("initial text");
expect($textarea).toHaveValue("initial textarea");

It only works on the input, textarea, and select tags:

const $textInput = $(<input type="text" value="text" onChange={} />);
const $numberInput = $(<input type="number" value="10" onChange={} />);
const $textarea = $(<textarea value="text description" onChange={} />);
const $select = $(
  <select value="second" onChange={}>
    <option value="first">first</option>
    <option value="second">second</option>
    <option value="third">third</option>
  </select>
);

// POSITIVE ASSERTIONS
expect($textInput).toHaveValue('text');
expect($numberInput).toHaveValue(10);
expect($textarea).toHaveValue('text description');
expect($select).toHaveValue('second');

// NEGATIVE ASSERTIONS
expect($textInput).not.toHaveValue(10);
expect($numberInput).not.toHaveValue('text');
expect($textarea).not.toHaveValue('random');
expect($select).not.toHaveValue('first');

Please use .checked for inputs of type checkbox and radio:

const $checkbox = $(<input type="checkbox" checked readOnly />);
const $radio = $(<input type="radio" value="something" checked readOnly />);

// ERROR: Cannot check .toHaveValue() for input type="checkbox" or type="radio".
expect($checkbox).toHaveValue("check");
expect($radio).toHaveValue("radio");

expect($checkbox.get(0).checked).toBe(true);
expect($radio.get(0).checked).toBe(true);

Element that don't contain the value attribute will throw errors:

const $button = $(<button>click</button>);
const $link = $(<a href="hello.com">click</a>);

// ERROR: 'Not a valid element that has a value attribute. Please insert an element that has a value.'
expect($button).toHaveValue("button");
expect($link).toHaveValue("link");

.toMatchSelector()

Checks whether the matched elements match the selector

const $button = $(
  <button id="the-button" className="a-button">
    click
  </button>
);

expect($button).toMatchSelector("#the-button");
expect($button).toMatchSelector(".a-button");

Checks whether the matched elements do not match the selector

const $button = $(
  <button id="the-button" className="a-button">
    click
  </button>
);

expect($button).not.toMatchSelector("#hello");
expect($button).not.toMatchSelector(".world");

For a list of items, it checks if all the elements match the provided selector

const $list = $(
  <ul>
    <li id="first-list-item" className="list-item">
      apple
    </li>
    <li className="list-item">apple</li>
  </ul>
);

// PASS
expect($list.find("li")).toMatchSelector("li");
expect($list.find("li")).toMatchSelector(".list-item");

// DO NOT PASS
expect($list.find("li")).toMatchSelector(".item");
expect($list.find("li")).toMatchSelector("#first-list-item");

For a list of items, it checks if any of the elements do not match the provided selector

const $list = $(
  <ul>
    <li id="first-list-item" className="list-item">
      apple
    </li>
    <li className="list-item">apple</li>
  </ul>
);

// PASS
expect($list.find("li")).not.toMatchSelector("div");
expect($list.find("li")).not.toMatchSelector(".hello");
expect($list.find("li")).not.toMatchSelector("#first-list-item");

// DO NOT PASS
expect($list.find("li")).not.toMatchSelector("li");
expect($list.find("li")).not.toMatchSelector(".list-item");

Examples

These examples define some component or components, and then explain how to test them with react-test. The idea is to have a variety of examples so that some of them will be similar to your code.

These are the examples we want to have:

  • <Counter />: increments when you click it.
  • <Todo />: can add items with React's useState().
  • <Signup />: to see handling of forms
  • <Subscribe />: to see how to test API calls.
  • <Portfolio />: with React Router to see how routing works.
  • Redux example: to see the possibilities of integration with Redux. Use one from the official docs.

<Counter />

Let's say that we have a simple counter. Every time you click it, its value increments by one:

// Counter.js
import React, { useState } from "react";

export default function Counter() {
  const [counter, setCounter] = useState(0);
  const increment = () => setCounter(counter + 1);
  return <button onClick={increment}>{counter}</button>;
}

All of our tests must be wrapped by describe() and need to import at least React, react-test and the component that we want to test:

// Counter.test.js
import React from "react";
import $ from "react-test";
import Counter from "./Counter";

describe("Counter.js", () => {
  // Write your tests here
  // All of the examples below should go here
});

First, we might want to check the initial value of this counter. We will render it and use .toHaveText() to check the plain-text content. Please note how this is text "0" and not a number ~0~:

it("starts with 0", () => {
  const counter = $(<Counter />);
  expect(counter).toHaveText("0");
});

Great, this passes our test.

Now let's try clicking it once. Any user action must be treated as asynchronous, so we create a new async test for this.

We are also going to be using the .click() method and awaiting for this click to be resolved:

it("increments when clicked", async () => {
  const counter = $(<Counter />);
  await counter.click();
  expect(counter).toHaveText("1");
});

Let's repeat this with multiple clicks as well:

it("can be incremented multiple times", async () => {
  const counter = $(<Counter />);
  await counter.click();
  await counter.click();
  await counter.click();
  expect(counter).toHaveText("3");
});

This component is working; the user clicks it, the value increments. That's awesome and most of the cases we would be done 🎉

But I also want to make sure there's not an issue where the state is shared. While in here the implementation is trivial, but in some cases it's not. So let's create two components and only one of them:

it("remains independent of other components", async () => {
  const counter1 = $(<Counter />);
  const counter2 = $(<Counter />);
  await counter2.click();
  expect(counter1).toHaveText("0");
  expect(counter2).toHaveText("1");
});

These also remain independent, great! I am sure this simple counter is working as expected. Let's see it all put together:

// Counter.test.js
import React from "react";
import $ from "react-test";
import Counter from "./Counter";

describe("Counter.js", () => {
  it("starts with 0", () => {
    const counter = $(<Counter />);
    expect(counter).toHaveText("0");
  });

  it("increments when clicked", async () => {
    const counter = $(<Counter />);
    await counter.click();
    expect(counter).toHaveText("1");
  });

  it("can be incremented multiple times", async () => {
    const counter = $(<Counter />);
    await counter.click();
    await counter.click();
    await counter.click();
    expect(counter).toHaveText("3");
  });

  it("remains independent of other components", async () => {
    const counter1 = $(<Counter />);
    const counter2 = $(<Counter />);
    await counter2.click();
    expect(counter1).toHaveText("0");
    expect(counter2).toHaveText("1");
  });
});

<Signup />

We are going to see now how we can simulate interactions with a form. This goes from the basics of typing text, validating the output, to more advanced features like validating as we type. We are going to use the library form-mate to greatly simplify our code, our base form is this:

export default function Signup({ onSubmit = () => {} }) {
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        onSubmit(formToObject(e.target));
      }}
    >
      <input name="username" type="text" />
      <input name="tos" type="checkbox" />
      <input name="option" type="radio" value="a" defaultChecked />
      <input name="option" type="radio" value="b" />
      <button>Send</button>
    </form>
  );
}

All of our tests must be wrapped by describe() and need to import at least React, react-test and the component that we want to test:

// Signup.test.js
import React from "react";
import $ from "react-test";
import Signup from "./Signup";

describe("Signup.js", () => {
  // Write your tests here
  // All of the examples below should go here
});

First let's check what happens if the user presses the submit button without changing anything:

it("can be submitted empty", async () => {
  const cb = jest.fn();
  const form = $(<Signup onSubmit={cb} />);

  expect(cb).not.toBeCalled();
  await form.submit();
  expect(cb).toBeCalledWith({ username: "", option: "a" });
});

Perfect, we define a callback with Jest to mock the onSubmit action, and we see that the data is parsed correctly and submitted as an empty username and the option "a" which is selected by default.

Now let's simulate that the user modifies the form, we use .type() for the text fields and just .click() for the checkbox and radio:

it("can modify each of the fields properly", async () => {
  const cb = jest.fn();
  const form = $(<Signup onSubmit={cb} />);

  await form.find('[type="text"]').type("hello");
  await form.find('[type="checkbox"]').click();
  await form.find('[type="radio"][value="b"]').click();
  await form.submit();
  expect(cb).toBeCalledWith({ username: "hello", tos: "on", option: "b" });
});