I'm writing tests for a React component Order
that renders a user's order and allows the user to view items on the order by clicking on a "View items" button, which triggers an API call.
import { useState, useEffect } from "react";
import Skeleton from "react-loading-skeleton";
import { Customer } from "../../../api/Server";
import capitalise from "../../../util/capitalise";
import renderOrderTime from "../../../util/renderOrderTime";
const Order = props => {
// Destructure props and details
const { details, windowWidth, iconHeight, cancelOrder } = props;
const { id, createdAt, status } = details;
// Define server
const Server = Customer.orders;
// Define order cancel icon
const OrderCancel = (
<svg className="iconOrderCancel" width={iconHeight} height={iconHeight} viewBox="0 0 24 24">
<path className="pathOrderCancel" style={{ fill:"#ffffff" }} d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5 15.538l-3.592-3.548 3.546-3.587-1.416-1.403-3.545 3.589-3.588-3.543-1.405 1.405 3.593 3.552-3.547 3.592 1.405 1.405 3.555-3.596 3.591 3.55 1.403-1.416z"/>
</svg>
)
// Set state and define fetch function
const [items, setItems] = useState([]);
const [fetchItems, setFetchItems] = useState(false);
const [isLoadingItems, setIsLoadingItems] = useState(false);
const [error, setError] = useState(null);
const fetchOrderItems = async() => {
setIsLoadingItems(true);
try {
let order = await Server.getOrders(id);
setItems(order.items);
setIsLoadingItems(false);
} catch (err) {
setError(true);
console.log(err);
}
}
useEffect(() => {
if (items.length === 0 && fetchItems) fetchOrderItems();
// eslint-disable-next-line
}, [fetchItems]);
// Define function to view order items
const viewItems = e => {
e.preventDefault();
setFetchItems(fetchItems ? false : true);
const items = document.getElementById(`items-${id}`);
items.classList.toggle("show");
}
// RENDERING
// Order items
const renderItems = () => {
// Return error message if error
if (error) return <p className="error">An error occurred loading order items. Kindly refresh the page and try again.</p>;
// Return skeleton if loading items
if (isLoadingItems) return <Skeleton containerTestId="order-items-loading" />;
// Get total cost of order items
let total = items.map(({ totalCost }) => totalCost).reduce((a, b) => a + b, 0);
// Get order items
let list = items.map(({ productId, name, quantity, totalCost}, i) => {
return (
<div key={i} className="item" id={`order-${id}-item-${productId}`}>
<p className="name">
<span>{name}</span><span className="times">×</span><span className="quantity">{quantity}</span>
</p>
<p className="price">
<span className="currency">Ksh</span><span>{totalCost}</span>
</p>
</div>
)
});
// Return order items
return (
<div id={`items-${id}`} className={`items${items.length === 0 ? null : " show"}`} data-testid="order-items">
{list}
{
items.length === 0 ? null : (
<div className="item total" id={`order-${id}-total`}>
<p className="name">Total</p>
<p className="price">
<span className="currency">Ksh</span><span>{total}</span>
</p>
</div>
)
}
</div>
)
};
// Component
return (
<>
<div className="order-body">
<div className="info">
<p className="id">#{id}</p>
<p className="time">{renderOrderTime(createdAt)}</p>
<button className="view-items" onClick={viewItems}>{ fetchItems ? "Hide items" : "View items"}</button>
{renderItems()}
</div>
</div>
<div className="order-footer">
<p className="status" id={`order-${id}-status`}>{capitalise(status)}</p>
{status === "pending" ? <button className="cancel-order" onClick={cancelOrder}>{windowWidth > 991 ? "Cancel order" : OrderCancel}</button> : null}
</div>
</>
)
}
export default Order;
Below are the tests I'm writing for Order
. I've run into some trouble writing tests on the API call.
import { render, fireEvent, screen } from "@testing-library/react";
import { act } from "react-dom/test-utils";
import Order from "../../../components/Primary/Orders/Order";
import { Customer } from "../../../api/Server";
import { orders } from "../../util/dataMock";
// Define server
const Server = Customer.orders;
// Define tests
describe("Order", () => {
describe("View items button", () => {
const mockGetOrders = jest.spyOn(Server, "getOrders");
beforeEach(() => {
const { getAllByRole } = render(<Order details={orders[2]} />);
let button = getAllByRole("button")[0];
fireEvent.click(button);
});
test("triggers API call when clicked", () => {
expect(mockGetOrders).toBeCalled();
});
test("renders loading skeleton during API call", () => {
let skeleton = screen.getByTestId("order-items-loading");
expect(skeleton).toBeInTheDocument();
});
///--- PROBLEMATIC TEST ---///
test("renders error message if API call fails", async() => {
await act(async() => {
mockGetOrders.mockRejectedValue("Error: An unknown error occurred. Kindly try again.");
});
// let error = await screen.findByText("An error occurred loading order items. Kindly refresh the page and try again.");
// expect(error).toBeInTheDocument();
});
});
test("calls cancelOrder when button is clicked", () => {
const clickMock = jest.fn();
const { getAllByRole } = render(<Order details={orders[2]} cancelOrder={clickMock} />);
let button = getAllByRole("button")[1];
fireEvent.click(button);
expect(clickMock).toBeCalled();
});
});
On the test that I've marked as problematic, I'm expecting the mocked rejected value Error: An unknown error occurred. Kindly try again.
to be logged to the console, but instead I'm getting a TypeError: Cannot read properties of undefined (reading 'items')
. This indicates that my mock API call isn't working properly and the component is attempting to act on an array of items it's supposed to receive from the API. How should I fix my test(s) to get the desired result?