React Test
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>
);
}
Concepts
Matched nodes
When we talk about "the first element" or "the elements matched" we always refer to the top-level element (unless specified differently). So in this example:
const list = $(
<ul>
<li>A</li>
<li>B</li>
</ul>
);
The first element, which is the same as the matched nodes, is the ul
and not the . We can always "go down a level" with the proper DOM navigation methods:
const list = $(...); // The node <ul>
const items = list.children(); // An array of <li> nodes
In this case the matched nodes of list
is an array containing only the <ul>
, while the matched nodes for items
is an array with both of the <li>
.
This is very important for many things, e.g. if you are trying to .filter()
the <li>
you need to use items
and not list
, same as if you want to get the first <li>
's Node:
list.get(0); // <ul>...</ul> ~> The whole thing
items.get(0); // <li>A</li> ~> The first item
items.get(1); // <li>B</li> ~> The second item
items.get(-1); // <li>B</li> ~> The last item
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.
When will the 1.0 be ready?
To launch the version 1.0, I'd like to finish a few tasks:
- Write more documentation and normalize it
- Normalize code, specially across testing
- Add some more event-based functionality, like extending native events (if possible).
- Write 5 working examples in total. Counter, Signup, MovieList, CRUD and Swipe (names TBD).
I don't know how long that'll take, right now I'm normalizing the code and documentation.
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 | Events | Others |
---|---|---|---|
.children() | .array() | .change() | .delay() |
.closest() | .attr() | .click() | .props() |
.each() | .data() | .submit() | .render() |
.filter() | .get() | .trigger() | |
.find() | .html() | .type() | |
.not() | .is() | ||
.parent() | .text() | ||
.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";
});
You can iterate over the matched elements with for ... of
:
const list = $(
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
);
for (let node of list.children()) {
expect(node.nodeName).toBe("LI");
}
.array()
.array(callback) -> Array
Get all of the currently matched nodes as a plain array:
it("can get the text of the children", () => {
const list = $(
<ul>
<li>A</li>
<li>B</li>
</ul>
);
const texts = list.children().array("textContent");
expect(texts).toEqual(["A", "B"]);
});
Parameters
callback
: it can be either of these:
Function
: a function that will behave like.map()
String
: the key to extract the value from each node.
Return
A plain array, with the nodes if there's no callback, with the value the callback returns if it's a function or with the values for the given keys passed as a string.
Examples
It's very useful to make plain assertions for groups of items:
it("can use a key for each of the nodes", () => {
const list = $(
<ul>
<li>A</li>
<li>B</li>
</ul>
);
const items = list.children().array("textContent");
expect(items).toEqual(["A", "B"]);
});
With a callback you can perform more expressive methods:
it("can use a function to return more complex data", () => {
const list = $(
<ul>
<li>A</li>
<li>B</li>
</ul>
);
const items = list
.children()
.array((node) => node.nodeName + " " + node.textContent);
expect(items).toEqual(["LI A", "LI B"]);
});
.attr()
.attr(name) -> String|null
Read the attribute value of the first node and return its value:
it("can read the different attributes of an input", async () => {
const input = $(<input name="email" defaultValue="" disabled />);
expect(input.attr("name")).toBe("email");
expect(input.attr("value")).toBe("");
expect(input.attr("disabled")).toBe("");
expect(input.attr("placeholder")).toBe(null);
});
Parameters
name
(required): the name of the attribute to select.
Return
String|null
: the value of the attribute, or null if the attribute is not set at all.
Examples
All the possible returns for different situations:
const input = $(<input name="email" defaultValue="" disabled />);
expect(input.attr("name")).toBe("email");
expect(input.attr("value")).toBe("");
expect(input.attr("disabled")).toBe("");
expect(input.attr("placeholder")).toBe(null);
input.attr("name")
: returns"email"
, since it has a key and string value.input.attr("value")
: returns""
, since the value (defaultValue) is set but empty.input.attr("disabled")
: returns""
, since a boolean attribute value defaults to an empty string.input.attr("placeholder")
: returnsnull
, since the attribute is not set at all.
Find .find()
to find a specific attribute, use the attribute selector:
const $form = $(<LoginForm />);
const $firstName = $form.find('[name="firstname"]');
expect($firstName).toHaveValue("");
await $firstName.type("John");
expect($firstName).toHaveValue("John");
Check all external links have the "noopener noreferrer"
value for rel
:
// 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");
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");
Related
expect().toHaveAttribute()
: Jest Matcher to check that the element(s) matched have the specified attribuye and/or value.
.change()
.change(value) -> Promise
Trigger a change in all of the matched elements. It should be awaited for the side effects to run and the component to re-rendered:
it("can change the input 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.
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
.
Return
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 fortype="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()
.children(selector) -> $
Get the children nodes of all of the matched elements, optionally filtering them with a CSS selector:
it("can select all list items", async () => {
const list = $(
<ul>
<li>A</li>
<li>B</li>
</ul>
);
expect(list.children().text()).toBe("A");
expect(list.children(":last-child").text()).toBe("B");
});
Parameters
selector
: A CSS selector expression to match elements against
Return
An instance of react-test
with the new children as itst elements.
Examples
Since we return an instance of react-test
, we have to use .array()
to convert it to an array so that we can iterate through them.
it("can get the children", () => {
const List = () => (
<ul>
<li>A</li>
<li>B</li>
</ul>
);
// Find the text of each element
const text = $(<List />)
.children()
.array((item) => item.textContent);
expect(text).toEqual(["A", "B"]);
});
.click()
.click() -> Promise
Trigger a click on all the matched elements. It should be awaited for the side effects to run and the component to re-rendered:
it("can click the counter", 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.
Parameters
None. Any parameters passed will be ignored.
Return
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()
.closest(selector) -> $
Find the first ancestor that matches the selector for each element (deduplicated):
it("finds all the list items with a link", async () => {
const list = $(
<ul>
<li>
<a>A</a>
</li>
<li>B</li>
</ul>
);
const item = list.find("a").closest("li");
expect(item.text()).toBe("A");
expect(item.html()).toBe("<li><a>A</a></li>");
});
Parameters
selector
: Expression to match elements against
Return
An instance of react-test
with the new elements as nodes
Usage
Since we return an instance of react-test
, we have to use .array()
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")
.array()
.map((node) => node.nodeName);
expect(names).toEqual(["LI", "LI"]);
.data()
.data(name) -> String|null
Read the data-attribute value of the first node and return its value:
it("can read the data attributes", () => {
const card = $(
<div data-id="25" data-selected>
Card
</div>
);
expect(card.data("id")).toBe("25");
expect(card.data("selected")).toBe("true"); // T_T 🤷♂️ gh/facebook/react/24812
expect(card.data("name")).toBe(null);
});
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>);
expext(hello.data("id")).toBe("0"); //0
.delay()
.delay(time) -> Promise
Makes the component to wait for the specified period of time in milliseconds:
it("can wait for an async action", async () => {
const down = $(<CountDown />);
expect(down.text()).toBe("3");
await down.delay(4000); // 4 seconds
expect(down.text()).toBe("Done!");
});
.delay()
already wraps the call with act(), so there's no need for you to also wrap it.
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()
.each(callback) -> $
Iterates over each of the nodes and returns the same collection of nodes as there was before:
it("can iterate over the items", () => {
const list = $(
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
);
const texts = [];
const out = list.find("li").each((node) => texts.push(node.textContent));
expect(texts).toEqual(["A", "B", "C"]);
expect(out.get().textContent).toBe("A");
});
Parameters
callback
: the function that receives each of the nodes currently matched. It receives (similarly to JS' .forEach()
):
node
: the current node being iterated on.index
: the index of the current node in the matched list.list
: an array with all of the nodes that are being iterated over.
Return
An instance of React-Test with the same collection as before calling .each()
.
.filter()
.filter(selector) -> $
Keep only the nodes that match the selector, removing the others:
it("can get just the users", () => {
const list = $(
<ul>
<li className="user">John</li>
<li className="group">Ibiza</li>
<li className="user">Sarah</li>
</ul>
);
const people = list.children().filter(".user");
expect(people.array((node) => node.textContent)).toEqual(["John", "Sarah"]);
});
Parameters
selector
: any one of these:
- a string containing the CSS selector that nodes must match
- a ReactTest instance containing a number of nodes. All the matched nodes must be in the ReactTest instance nodes
- a callback that will keep the element if it returns
true
. It receives:node
: the current node being iterated on.index
: the index of the current node in the matched list.list
: an array with all of the nodes that are being iterated over.
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");
Related
.not(selector)
: a method that returns a new collection only with nodes that pass the matcher.
.find()
.find(selector) -> $
Get all of the descendants of the nodes with an optional filter
Parameters
selector
: a string containing a selector that nodes must pass or a function that return a boolean. See .filter() for a complete explanation of how selectors work.
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()
.get(index) -> NodeElement
Get a native DOM Node given its index. Defaults to the first element:
const list = $(
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
</ul>
);
expect(list.find("li").get()).toHaveText("A");
expect(list.find("li").get(1)).toHaveText("B");
expect(list.find("li").get(-1)).toHaveText("D");
Parameters
index
: the index of the element to get. It defaults to 0
, and you can also set a negative indexes. Any overflowing index will be wrapped around. If there's only a single top-level element, that will be returned.
Return
A single NodeElement representing the content that was matched by the index.
Notes
This is very useful if you want to use the browser DOM API for testing different properties, like:
<input type="checkbox" />
can be tested withexpect(input.get().checked).toBe(true);
<input required ... />
can be tested withexpect(input.get().valid).toBe(true);
- Type of HTML element can be tested with
expect(component.get().nodeName).toBe("FORM")
- etc.
Basically any time that you are would like to fall back to the native DOM API for testing, you can use .get().
.
Technically if you want to wrap it again, you can simply do
$(form.get())
, though we wouldn't recommend to use this too much and see the ReactTest instance => Node as one way operation.
Examples
Get the form element to make assertions with FormData:
const form = $(<SignupForm />);
const data = new FormData(form.get());
expect(data.get("firstname")).toBe("");
Return a nested element of a list:
const list = $(
<ul>
<li>A</li>
<li>B</li>
</ul>
);
const first = list.get(0);
expect(first.textContent).toBe("A");
Related
.array()
: get ALL of the current nodes as a plain array.
.html()
.html() -> String
Retrieve the OuterHTML of the first element matched:
it("can extract the plain html", () => {
const card = $(<div className="card">Hello</div>);
expect(card.html()).toBe(`<div class="card">Hello</div>`);
});
Parameters
None
Return
A String with the HTML of the first element.
.is()
.is(selector) -> Boolean
Check whether all of the nodes match the selector:
it("can properly match the button", () => {
const button = $(<a className="button active">Click me</a>);
expect(button.is("a")).toBe(true);
expect(button.is(".active")).toBe(true);
expect(button.is(".inactive")).not.toBe(true);
});
Parameters
selector
: any one of these:
- a string containing the CSS selector that nodes must match
- a ReactTest instance containing a number of nodes. All the matched nodes must be in the ReactTest instance nodes
- a callback that will keep the element if it returns
true
. It receives:node
: the current node being iterated on.index
: the index of the current node in the matched list.list
: an array with all of the nodes that are being iterated over.
Return
A boolean, true
to indicate all of the selector matches all of the nodes, false
to indicate at least one (or more) fail the condition. An empty array will always return true
.
Notes
For a given selector, if you apply .filter(selector).is(selector)
(both being the same CSS selector) it will always return true.
Examples
TODO
// Check that the important class belongs to a direct list item
expect(list.find(".important").is(list.children())).toBe(true);
Related
.filter(selector)
: a method that returns a new collection only with nodes that pass the matcher.
.map()
.map(callback) -> $
Iterates over each of the nodes and returns a new collection with the nodes that were returned from the callback:
it("can get a new collection", () => {
const list = $(
<ul>
<li>A</li>
<li>B</li>
</ul>
);
// Same as .find('li')
const items = list.map((node) => node.querySelectorAll("li"));
expect(items.array((node) => node.nodeName)).toEqual(["LI", "LI"]);
});
Parameters
callback
: the function that receives each of the nodes currently matched. It receives (similarly to JS' .forEach()
):
node
: the current node being iterated on.index
: the index of the current node in the matched list.list
: an array with all of the nodes that are being iterated over.- returns the new nodes
Return
An instance of React-Test with the new collection of nodes. Nested arrays (or NodeLists) are flattened and empty and duplicates items are also removed.
Related
.each(callback)
: Similar to.map()
, but returns the original collection.
.not()
.not(selector) -> $
Remove the matched nodes from the collection. It's the opposite of .filter()
:
it("can get just the users", () => {
const list = $(
<ul>
<li className="user">John</li>
<li className="group">Ibiza</li>
<li className="user">Sarah</li>
</ul>
);
const people = list.children().not(".group");
expect(people.array((node) => node.textContent)).toEqual(["John", "Sarah"]);
});
Parameters
selector
: any one of these:
- a string containing the CSS selector that nodes must not match
- a ReactTest instance containing a number of nodes. All the matched nodes must not be in the ReactTest instance nodes
A callback is not allowed, instead use
.filter(node => true|false)
and negate the condition for better testing clarity.
Return
An instance of React Test with only the nodes that did not match the selector.
Examples
Get a list of non-important items:
it("can get all non-important list items", () => {
const List = () => (
<ul>
<li>A</li>
<li className="important">B</li>
<li>C</li>
</ul>
);
// Find the text of each element
const text = $(<List />)
.children()
.not(".important")
.array((item) => item.textContent);
expect(text).toEqual(["A", "C"]);
});
Get all the rows except the headers (first row):
it("can get all children except the first", () => {
const Table = () => (
<table>
<tbody>
<tr>
<th>Col A</th>
<th>Col B</th>
</tr>
<tr>
<td>A1</td>
<td>B1</td>
</tr>
<tr>
<td>A2</td>
<td>B2</td>
</tr>
</tbody>
</table>
);
// Find the text of each element
const text = $(<Table />)
.find("tr")
.not(":first-child")
.array((item) => item.textContent);
expect(text).toEqual(["A1B1", "A2B2"]);
});
Related
.filter(selector)
: a method that returns a new collection only with nodes that pass the matcher..is(selector)
: a method to determine whether all of the nodes match the selector.
.parent()
.parent() -> $
Return a new collection with the direct parent of the current nodes. It also removes duplicates:
it("can go down and up again", () => {
const list = $(
<ul>
<li>A</li>
<li>B</li>
</ul>
);
const items = list.children(); // <li>A</li>, <li>B</li>
const listB = items.parent(); // <ul>...</ul>
expect(listB.html()).toEqual(list.html());
});
Parameters
None.
TODO? an optional filter?
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);
.props()
.props(newProps) -> $
Rerender the component with the new props specified as a plain object:
const Demo = (props) => <div {...props}>world</div>;
it("can force-update the props on the root", () => {
const demo = $(<Demo className="hello" />);
expect(demo).toHaveHtml(`<div class="hello">world</div>`);
// Rerender with a new className on the top component:
demo.props({ className: "bye" });
expect(demo).toHaveHtml(`<div class="bye">world</div>`);
});
Parameters
newProps
: the props to pass to the component in a new re-render. It can be either a plain object, or a function that will receive the old props and should return the new props.
Return
An instance of React Test that has re-rendered with the new props.
Examples
Update the prop className:
const Demo = ({ className }) => <div className={className}>world</div>;
it("can inject new props", async () => {
const demo = $(<Demo className="hello" />);
expect(demo).toHaveHtml(`<div class="hello">world</div>`);
demo.props({ className: "bye" });
expect(demo).toHaveHtml(`<div class="bye">world</div>`);
});
it("can accept the old props", async () => {
const demo = $(<Demo className="hello" />);
expect(demo).toHaveHtml(`<div class="hello">world</div>`);
demo.props((p) => ({ className: p.className + "-bye" }));
expect(demo).toHaveHtml(`<div class="hello-bye">world</div>`);
});
.render()
.render(newComponent) -> $
Rerender the component as specified with the new value. If the component is different unmount+mount those respectively:
const Demo = (props) => <div {...props}>world</div>;
it("can force-update the props on the root", () => {
const demo = $(<Demo className="hello" />);
expect(demo).toHaveHtml(`<div class="hello">world</div>`);
// Rerender with a new className on the top component:
demo.render(<Demo className="bye" />);
expect(demo).toHaveHtml(`<div class="bye">world</div>`);
});
Note: if you only want to re-render changing the props, you might prefer using
.props()
.
Parameters
newComponent
: the new component to render in place of the old one. If it's the same component, it'll trigger a re-render, otherwise it'll unmount the old one and mount the new one.
Return
An instance of React Test that has re-rendered with the new component.
Examples
Rerender with a new prop:
const Demo = ({ className }) => <div className={className}>world</div>;
it("can inject new props", async () => {
const demo = $(<Demo className="hello" />);
expect(demo).toHaveHtml(`<div class="hello">world</div>`);
demo.render(<Demo className="bye" />);
expect(demo).toHaveHtml(`<div class="bye">world</div>`);
});
it("can render a different component", () => {
const demo = $(<div>Hello</div>);
expect(demo).toHaveHtml(`<div>Hello</div>`);
demo.render(<span>Bye</span>);
expect(demo).toHaveHtml(`<span>Bye</span>`);
});
Notes
Do not reuse a root node (by using this .render()
) more than necessary (e.g. testing rerenders); instead create a raw instance of a component with $()
as usual for testing new components.
.siblings()
.siblings(selector?) -> $
Return a new collection with the direct parent of the current nodes with an optional filter:
const list = $(<List />);
const items = list.find("li.active").siblings();
expect(items.array("className")).toEqual(["", ""]);
Parameters
selector
: a string containing a selector that nodes must pass or a function that return a boolean. See .filter() for a complete explanation of how selectors work.
Return
An instance of React Test with the new siblings as nodes
Examples
Find all the items in the list that are not active:
const list = $(<List />);
const items = list.find("li.active").siblings();
expect(items.array("className")).toEqual(["", ""]);
.submit()
.submit() -> Promise
Trigger a form submission on all the matched forms. It should be awaited for the side effects to run and the component to re-rendered:
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("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();
});
.click()
already wraps the call with act(), so there's no need for you to also wrap it.
onSubmit
should always calle.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()
.text() -> String
Get the textContent of the first matched node:
it("can get the simple text", () => {
const greeting = $(
<div>
Hello <br /> world
</div>
);
expect(greeting.text()).toBe("Hello world");
});
.trigger()
.trigger(name, extra?) -> Promise
Simulates an event happening on all the matched elements. It should be awaited for the side effects to run and the component to re-rendered:
it("can simulate clicking a div in a specific place", async () => {
const fn = jest.fn();
const canvas = $(<canvas onClick={fn}></canvas>);
await canvas.trigger("click", { clientX: 10, clientY: 20 });
const event = fn.mock.calls[0][0];
expect(event).toMatchObject({ clientX: 10, clientY: 20 });
});
Parameters
name
: the event name. It should be in lowercase and without anyon
. Examples:"click"
,"keypress"
,"mousedown"
,"pointermove"
, etc.extra = {}
: any data that you want to mock into theevent
that the event handler will receive. This is very useful to mock e.g.clientX+clientY
,target
, etc.
Returns
A promise that must be awaited before doing any assertion.
Examples
You can test window.addEventListerner()
the same way, since events bubble up to the global window by default:
const Demo = ({ onDown }) => {
useEffect(() => {
window.addEventListener("keydown", onDown);
return () => window.removeEventListener("keydown", onDown);
}, [onDown]);
return <div>Hello</div>;
};
it("can trigger clicks even from the window", async () => {
const onDown = jest.fn();
const demo = $(<Demo onDown={onDown} />);
expect(onDown).not.toBeCalled();
await demo.trigger("keydown", { key: "x" });
expect(onDown).toBeCalled();
const event = onDown.mock.calls[0][0];
expect(event.key).toBe("x");
});
But you might want to customize the event more to make it more realistic, like changing the target
manually (same Demo
as above):
it("can customize the target to window", async () => {
const onDown = jest.fn();
const demo = $(<Demo onDown={onDown} />);
expect(onDown).not.toBeCalled();
await demo.trigger("keydown", { key: "x", target: window });
expect(onDown).toBeCalled();
const event = onDown.mock.calls[0][0];
expect(event.key).toBe("x");
expect(event.target).toBe(window);
});
.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:
it("can simulate typing in an input", async () => {
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 want to use .change(text)
instead.
.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
act()
When you want to do some operation that will trigger a change in your React component, usually you need to wrap it in act()
to trigger the re-render. With React-Test, this is mostly not needed since all of our methods already wrap things with act() internally. But you might define your own e.g. delay() function, and if you expect some changes in your component during that timeout then you should wrap it with act():
import $, { act } from "react-test";
import CountDown from "./CountDown";
const delay = (time) => new Promise((done) => setTimeout(done, time));
it("will countdown from 3 to 0", async () => {
const down = $(<CountDown />);
expect(down).toHaveText("3");
await act(() => delay(4000));
expect(down).toHaveText("Done!");
});
If you do not wrap that custom delay with act(), you'll receive this warning:
Warning: An update to Countdown inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
at Countdown ([omitted]/Countdown.test.js:8:27)
Parameters
async function
: a function that can be either async or sync, that once is done executing might trigger a change in your React component.
Return
Promise
: a promise that must be awaited for to ensure the code has been updated properly.
Notes
You do not need this for any of the React methods, since we already wrap them with act()
internally. So all of these are wrong:
const counter = $(<Counter />);
const down = $(<CountDown />);
// WRONG; we already wrap these with act() internally
await act(() => counter.click());
await act(() => down.delay(4000));
await act(() => until(() => down.text() === "Done!"));
// RIGHT; this is how you should be using them instead
await counter.click();
await down.delay(4000);
await until(() => down.text() === "Done!");
Examples
If you want to define your own timer, then you can wrap it in act():
const delay = (time) => new Promise((done) => setTimeout(done, time));
it('waits properly with act()', () => {
const down = $(<CountDown />);
expect(down).toHaveText('3');
await act(() => delay(4000));
expect(down).toHaveText('Done!');
});
until()
Wait until the specified condition is fulfilled. There are multiple ways of specifying the conditions. For example, let's say that you have a timer that changes its class to active
after 3s:
import $, { until } from "react-test";
const timer = $(<Timer />);
expect(timer).not.toHaveClass("active");
await until(() => timer.is(".active")); // Wait until it becomes active
expect(timer).toHaveClass("active");
Parameters
checker
: it receives either a callback or a component and only resolves when:
- Receiving a callback, that callback returns
true
or atruthy
value. - Receiving a component, we call data methods like
.is('.active')
that will resolve when it's "true". - Receiving a component, we can execute actions like
.find('.active')
and it'll be truthy when it finds at least 1 node that matches the query.
Returns
It returns a composite response that behaves both as a React Instance and as a Promise. So it can be awaited straight away, concatenated until you use a "data" method which will behave as a promise or transversing the DOM in which case it'll resolve when it's not empty:
// Plain callback, resolves when it returns truthy
await until(() => button.text() === "Hello!");
// Wait until it has the class "active"
await until(button).is(".active");
// Wait until the collection returns a non-empty list
await until(list).find("li");
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("value", /^\d+$/);
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 and 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", "3");
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 "banana"
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'suseState()
.<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:
import Form from "form-mate";
export default function Signup({ onSubmit = () => {} }) {
return (
<Form onSubmit={onSubmit}>
<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" });
});