react-integration-test-engine
v1.5.0
Published
Integration test utils for react components
Downloads
10
Maintainers
Readme
react-integration-test-engine
Unit test utils for react components
Examples
Installation
npm install react-integration-test-engine @testing-library/react --save-dev
or
yarn add react-integration-test-engine @testing-library/react --dev
Quickstart
Let's test a component. I'm using vitest
, but you can use your favourite test framework
import type {
ReactElement,
} from "react";
type Props = {
callback: (foo: number, bar: string) => void;
};
function Component({
callback,
}: Props): ReactElement | null {
const onClick = useCallback(() => {
callback(1, "2");
}, [callback]);
return (
<div className="my-wrapper">
<button type="button" onClick={onClick}>
Click me
</button>
</div>
);
}
At first, we have to define stubs for required props of the component
import { vi } from "vitest";
const defaultProps: Props = {
callback: vi.fn(),
};
Then let's describe accsessors of rendered components. In this case, only button
is needed. Let's call it "targetButton"
import { create, AccessorQueryType } from "react-integration-test-engine";
const render = create(Component, defaultProps, {
queries: {
button: {
query: AccessorQueryType.Text,
parameters: ["Click me"],
},
},
});
A boilerplate is ready. Let's write a test that checks for the correct render of the content of the button
import { expect, test } from "vitest";
test("should render children correctly", () => {
const engine = render({});
expect(engine.accessors.button.get().textContent).toBe("Click me");
});
A method get
is used here, but you can use other methods. The full list:
getAll
- returns all matched elements getAll: () => HTMLElement[];get
- returns a single matched element or throws if there are no matched elements or throws if there are more than one matched elements get: () => HTMLElement;queryAll
- returns all matched elements queryAll: () => HTMLElement[];query
- returns a single matched element ornull
if there are no matched elements or throws if there are more than one matched elements query: () => HTMLElement | null;findAll
- similar togetAll
, but waits for matched elements findAll: () => Promise<HTMLElement[]>;find
- similar toget
, but waits for matched elements find: () => Promise;
testing-library is used.
Then let's test a callback. There's an easy way to do it. Let's change definition a little
import { create, AccessorQueryType } from "react-integration-test-engine";
const render = create(Component, defaultProps, {
queries: {
button: {
query: AccessorQueryType.Text,
parameters: ["Click me"],
},
},
// !!!!!!!!!!!!!!!
// ADDED `fireEvents` SECTION
fireEvents: {
buttonClick: ["button", "click"],
},
});
The first value of the tupple is the key of queries
. The second value is the type of the event
Let's write a test for the callback:
import type { MouseEvent } from "react";
import { expect, test, vi } from "vitest";
test("should call callback correctly", () => {
const callback = vi.fn();
const engine = render({
callback,
});
const event = {};
engine.fireEvent("buttonClick");
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith(1, "2");
});
Scenarios
Execution of complex actions
fireEvent
is not enough for actions that required several user interactions, e.g. selecting of value from dropdown or date picker etc.
There is a property scenarios
in the constuctor of engine. You can call the scenario with the run
method of the engine.
Differences from events
:
Allow multiple interactions;
Doesn't invoke
act
automatically;Can return something.
Let's write an example test with selecting the value of react-datepicker
(version 4.21.0
):
At first, let's write a component for testing:
import { type ReactElement, useState } from "react";
import ReactDatePicker from "react-datepicker";
function Component(): ReactElement {
const [date, setDate] = useState<Date | null>(() => new Date(2023, 9, 20));
return (
<ReactDatePicker selected={date} onChange={setDate} />
);
}
Then let's define the engine constructor:
import {
act,
fireEvent,
screen,
within,
} from "@testing-library/react";
import { AccessorQueryType, create } from "react-integration-test-engine";
const render = create(
Component,
{},
{
queries: {
dateInput: {
query: AccessorQueryType.QuerySelector,
parameters: [".react-datepicker__input-container input"],
},
},
scenarios: {
changeDatepicker: [
"dateInput",
async (element, day: number) => {
act(() => {
fireEvent.focus(element);
});
const listbox = await screen.findByRole("listbox");
const dayButton = within(listbox).getByText(`${day}`, {
ignore: ".react-datepicker__day--outside-month",
});
act(() => {
fireEvent.click(dayButton);
});
},
],
},
},
);
Then let's write a test to change the date:
test("date change", async () => {
const engine = render({});
await engine.run("changeDatepicker", 1);
expect(engine.accessors.dateInput.get()).toHaveProperty(
"value",
"10/01/2023",
);
});
Complex data collection
Let's test a table.
import type { ReactElement, ReactNode } from "react";
type RowType = Readonly<{
id: number;
foo: ReactNode;
bar: ReactNode;
baz: ReactNode;
}>;
type Props = Readonly<{
rows: readonly RowType[];
}>;
function Component({ rows }: Props): ReactElement {
return (
<table>
<tbody>
{rows.map(({ id, foo, bar, baz }) => (
<tr key={id}>
<td>{foo}</td>
<td>{bar}</td>
<td>{baz}</td>
</tr>
))}
</tbody>
</table>
);
}
Suppose it's needed to make an array of the text contents of a certain row of the table. Let's write this scenario:
import { AccessorQueryType, create } from "react-integration-test-engine";
const defaultProps: Props = {
rows: [],
};
const render = create(Component, defaultProps, {
queries: {
table: {
query: AccessorQueryType.QuerySelector,
parameters: ["table"],
},
},
scenarios: {
getRenderedRow: [
"table",
(tableNode, index: number) => {
const tableRow = tableNode.querySelector(`tr:nth-child(${index})`);
if (!tableRow || !(tableRow instanceof HTMLElement)) {
throw new Error("row is not rendered");
}
return [...tableRow.childNodes].map((node) => node.textContent);
},
],
},
});
All that's left to do is run this scenario:
test("render rows", () => {
const engine = render({
rows: [
{
id: 1,
foo: "foo 1",
bar: "bar 1",
baz: "baz 1",
},
{
id: 2,
foo: "foo 2",
bar: "bar 2",
baz: "baz 2",
},
],
});
expect(engine.run("getRenderedRow", 1)).toEqual(["foo 1", "bar 1", "baz 1"]);
expect(engine.run("getRenderedRow", 2)).toEqual(["foo 2", "bar 2", "baz 2"]);
});