Testing of Nested React components using React-Test-Renderer - reactjs

I have a pure React-Redux application and it is working as expected.
The App.js
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { Router, Route, Switch, Redirect } from "react-router-dom";
import history from "../history";
import LandingPage from "./home/LandingPage";
import { displayModules } from "../actions";
import Cart from "./home/Cart";
const App = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(displayModules());
}, [dispatch]);
return (
<Router history={history}>
<Switch>
<Route path="/" exact component={LandingPage}></Route>
<Route path="/cart" exact component={Cart}></Route>
<Route render={() => <Redirect to="/" />} />
</Switch>
</Router>
);
};
export default App;
The LandingPage has a nested component called Tile.
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import Tile from "../common/Tile";
import { addItemToCart, displayCartContents } from "../../actions";
import "./LandingPage.css";
const LandingPage = () => {
const modules = useSelector(state => state.data.modules);
const cart = useSelector(state => state.data.cart);
const dispatch = useDispatch();
const addToCart = item => {
dispatch(addItemToCart(item));
};
return (
<div className="app">
<div className="header">
<div className="text">Insurance modules</div>
<i
className="shopping cart icon"
onClick={() => {
dispatch(displayCartContents());
}}
>
<span className="badge">{cart.length}</span>
</i>
</div>
<div className="body">
{modules.map(module => (
<Tile key={module.id} module={module} addToCart={addToCart}></Tile>
))}
</div>
</div>
);
};
export default LandingPage;
Tile.js has a button which I want to test.
import React, { useState } from "react";
import "./Tile.css";
const Tile = props => {
const { module, addToCart } = props;
const [coverage, setCoverage] = useState(parseInt(module.coverageMax - module.coverageMin) / 2);
const [price, setPrice] = useState((coverage * module.risk) / 100);
return (
<div className="tile">
<div className="tile-description">
<div>
<i className={`${module.icon} icon`}></i>
</div>
<div className="tile-name">{module.name}</div>
<div className="tile-risk">Risk(%): {module.risk}</div>
</div>
<div className="tile-footer">
<div className="tile-range">
<div className="field-label">
Select Coverage: <span className="coverage-display">{coverage}</span>
</div>
<div className="slidecontainer">
<span className="slider-step">{module.coverageMin}</span>
<input
type="range"
min={module.coverageMin}
max={module.coverageMax}
value={coverage}
className="slider"
onChange={e => {
setCoverage(e.target.value);
setPrice((e.target.value * module.risk) / 100);
}}
></input>
<span className="slider-step">{module.coverageMax}</span>
</div>
</div>
<div>
PRICE at this Coverage:<span className="tile-price">{price}</span>
</div>
<button
className="tile-button"
onClick={() => {
addToCart({
id: module.id,
name: module.name,
coverage: coverage,
price: price,
timeStamp: Math.ceil(new Date().getTime() * Math.random() * Math.random())
});
}}
>
Add module to cart
</button>
</div>
</div>
);
};
export default Tile;
App.test.js works fine and I am able to find the nested Landing Page div by className prop.
import React from "react";
import configureStore from "redux-mock-store";
import { Provider } from "react-redux";
import renderer from "react-test-renderer";
import App from "../components/App";
import history from "../history";
import { displayModules } from "../actions";
import { DISPLAY_MODULES } from "../actions/types";
const mockStore = configureStore([]);
describe("App Component test", () => {
let store = {};
let wrappedComponent = {};
const expectedActions = {
type: DISPLAY_MODULES,
payload: [
{
id: 0,
icon: "bicycle",
name: "Bike",
coverageMin: 0,
coverageMax: 3000,
risk: 30
},
{
id: 1,
icon: "gem",
name: "Jewelry",
coverageMin: 500,
coverageMax: 10000,
risk: 5
},
{
id: 2,
icon: "microchip",
name: "Electronics",
coverageMin: 500,
coverageMax: 6000,
risk: 35
},
{
id: 3,
icon: "football ball",
name: "Sports Equipment",
coverageMin: 0,
coverageMax: 20000,
risk: 30
}
]
};
beforeEach(() => {
store = mockStore({
data: {
modules: [],
cart: [],
total: 0
}
});
store.dispatch = jest.fn(displayModules);
wrappedComponent = renderer.create(
<Provider store={store}>
<App />
</Provider>
);
});
it("should render with given state from Redux store", () => {
expect(wrappedComponent.toJSON()).toMatchSnapshot();
});
it("should have an app from Landing Page", () => {
expect(wrappedComponent.root.findByProps({ className: "app" })).toBeDefined();
});
it("should show landing page for default route", () => {
*debugger;
expect(wrappedComponent.root.findByProps({ className: "shopping cart icon" })).toBeDefined();*
});
it("should show cart page for /cart route", () => {
history.push("/cart");
expect(wrappedComponent.root.findByProps({ className: "backward icon" })).toBeDefined();
});
it("should redirect to landing page for unmatched 404 routes", () => {
history.push("/someRandomRoute");
expect(wrappedComponent.root.findByProps({ className: "shopping cart icon" })).toBeDefined();
});
it("should dispatch displayModules action on app mount", async () => {
const actualAction = await store.dispatch();
expect(actualAction).toEqual(expectedActions);
});
});
But If you see the test debugger
The children of div with className: body has no children.
That is why it is not able to find the Tile component.
Can you suggest why the children are null for the body?
I have seen this before, even i tried with Enzyme i faced this issue.
Since it is a Redux wrapped component the , i cant directly create the Landing page or Tile component for testing.
How to test the nested items?

You are providing an empty array to modules inside redux state:
store = mockStore({
data: {
modules: [], // your modules is empty so no tiles will render
cart: [],
total: 0
}
});
Another issue is that you mock store.dispatch so it no longer changes the redux store even if some action is dispatched:
store.dispatch = jest.fn(displayModules);
If you want to test that an action was dispatched you can use:
const actions = store.getActions()
Which will give you all actions which were dispatched.
If you want to test how your app renders based on your store data you can either:
Setup the store in the test:
const existingModules = [ ... ]; // list of modules
store = mockStore({
data: {
modules: existingModules,
cart: [],
total: 0
}
});
You can mock useSelector in your test:
const existingModules = [ ... ]; // list of modules
const spy = jest.spyOn(redux, 'useSelector')
spy.mockReturnValue(existingModules)

Related

Why my state returns undefined when i click on add category button?

I have a context api in my application. At the moment I only keep the categories and i have 1 initial category. I print the categories in App.js with the map function. I have defined a function called addCategoryHandler in context api and I want to update my state by calling it in AddCategory component. But when I click the button state.categories returns undefined. I guess I'm missing something about lifecyle but I couldn't quite understand. Can you help?
Here is the codesandbox link: https://codesandbox.io/s/hungry-zeh-kwolr8
App.js
import AvailableProducts from './components/AvailableProducts.js';
import Category from './components/Category.js';
import Review from './components/Review.js';
import AddCategory from './components/AddCategory';
import { useAppContext } from './context/appContext';
import './assets/styles/App.scss';
export default function App() {
const { categories } = useAppContext();
return (
<main>
<h1>Initial Screen</h1>
<div className='container'>
<div className='container__left-side'>
<AvailableProducts />
<Review />
</div>
<div className='container__right-side'>
{categories.map((category) => (
<Category
key={category.id}
id={category.id}
title={category.title}
/>
))}
<AddCategory />
</div>
</div>
</main>
);
}
Context Api
import React, { useContext, useState } from "react";
import generateCategoryTitle from "../utils/GenerateCategoryTitle";
const AppContext = React.createContext();
const initialState = {
categories: [{ id: 1, title: "Category 1", products: [] }]
};
const AppProvider = ({ children }) => {
const [state, setState] = useState(initialState);
console.log(state);
const addCategoryHandler = () => {
// const { newId, newCategoryTitle } = generateCategoryTitle(state.categories);
// // const newCategory = [{ id: newId, title: newCategoryTitle, products: [] }];
// setState((prevState) => {
// console.log([...prevState.categories,...newCategory]);
// });
console.log("add category clicked");
};
return (
<AppContext.Provider value={{ ...state, addCategoryHandler }}>
{children}
</AppContext.Provider>
);
};
const useAppContext = () => useContext(AppContext);
export { AppProvider, useAppContext };
Add Category Component
import "../assets/styles/AddCategory.scss";
import { useAppContext } from "../context/appContext";
const AddCategory = () => {
const { addCategoryHandler } = useAppContext();
return (
<button
className="add-categoryn-btn"
type="button"
onClick={addCategoryHandler}
>
Add Category
</button>
);
};
export default AddCategory;

React - pass context to SweetAlert popup

My context is as follows:
import React, {createContext, useEffect, useState} from "react";
export const CartContext = createContext();
const CartContextProvider = (props) => {
const [cart, setCart] = useState(JSON.parse(localStorage.getItem('cart')) || []);
useEffect(() => {
localStorage.setItem('cart', JSON.stringify(cart));
}, [cart]);
const updateCart = (productId, op) => {
let updatedCart = [...cart];
if (updatedCart.find(item => item.id === productId)) {
let objIndex = updatedCart.findIndex((item => item.id === productId));
if (op === '-' && updatedCart[objIndex].qty > 1) {
updatedCart[objIndex].qty -= 1;
} else if (op === '+') {
updatedCart[objIndex].qty += 1;
}
} else {
updatedCart.push({id: productId, qty: 1})
}
setCart(updatedCart);
}
const removeItem = (id) => {
setCart(cart.filter(item => item.id !== id));
};
return (
<CartContext.Provider value={{cart, updateCart, removeItem}}>
{props.children}
</CartContext.Provider>
)
};
export default CartContextProvider;
App.js:
import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import NavigationBar from "./components/layout/navigationBar/NavigationBar";
import Homepage from "./pages/homepage/Homepage";
import AboutUsPage from "./pages/aboutUs/AboutUsPage";
import ContactPage from "./pages/contact/ContactPage";
import SearchPage from "./pages/search/SearchPage";
import ShoppingCart from "./components/layout/shoppingCart/ShoppingCart";
import CartContextProvider from "./context/CartContext";
function App() {
return (
<div>
<CartContextProvider>
<Router>
<NavigationBar/>
<ShoppingCart/>
<Routes>
<Route exact path="/" element={<Homepage/>}/>
<Route path="/a-propos" element={<AboutUsPage/>} />
<Route path="/contact" element={<ContactPage/>}/>
<Route path="/recherche" element={<SearchPage/>}/>
</Routes>
</Router>
</CartContextProvider>
</div>
);
}
export default App;
In the component ShoppingCart I am using another component ShoppingCartQuantity which in turn makes use of the context. It works as it should.
Here's the ShoppingCartQuantity component:
import React, {useContext} from "react";
import {CartContext} from "../../../context/CartContext";
import styles from './ShoppingCartQuantity.module.css'
const ShoppingCartQuantity = ({productId}) => {
const {cart, updateCart} = useContext(CartContext);
let qty = 0;
if (cart.find((item => item.id === productId))) {
let objIndex = cart.findIndex((item => item.id === productId));
qty = cart[objIndex].qty;
}
return (
<div>
<span>
<span className={`${styles.op} ${styles.decrementBtn}`} onClick={() => updateCart(productId, '-')}>-</span>
<span className={styles.qty}>{qty}</span>
<span className={`${styles.op} ${styles.incrementBtn}`} onClick={() => updateCart(productId, '+')}>+</span>
</span>
</div>
)
}
export default ShoppingCartQuantity;
Now I am trying to use the ShoppingCartQuantity component in the Homepage component which is a route element (refer to App.js) but getting the error Uncaught TypeError: Cannot destructure property 'cart' of '(0 , react__WEBPACK_IMPORTED_MODULE_0__.useContext)(...)' as it is undefined.
So the context is working for components outside the router but not for those inside it. If I have wrapped the router within the provider, shouldn't all the route elements get access to the context or am I missing something?
UPDATE
As user Build Though suggested in the comments, I tried using the ShoppingCartQuantity component in another route element and it works fine; so the problem is not with the router!
Below is the code of how I am using the ShoppingCartQuantity component in the Homepage component:
import React, { useState, useEffect, useRef } from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import Subcat from "../../components/subcat/Subcat";
import CategoryService from "../../services/api/Category";
import SubCategoryService from "../../services/api/SubCategory";
import CategoriesLayout from "../../utils/CategoriesLayout";
import CategoryCard from "../../components/category/CategoryCard";
import { Triangle } from 'react-loader-spinner'
import ScrollIntoView from 'react-scroll-into-view'
import ProductService from "../../services/api/Product";
import Swal from 'sweetalert2'
import withReactContent from 'sweetalert2-react-content';
import YouTube from 'react-youtube';
import FavoriteBtn from "../../components/favorite/FavoriteBtn";
import ShoppingCartQuantity from "../../components/layout/shoppingCart/ShoppingCartQuantity";
import "./Homepage.css";
import "../../components/product/ProductModal.css"
import "react-loader-spinner";
import modalStyles from "../../components/product/ProductModal.module.css"
function Homepage() {
const [categories, setCategories] = useState([]);
const [subCats, setSubCats] = useState([]);
const [loader, setLoader] = useState(false);
const ResponsiveGridLayout = WidthProvider(Responsive);
const scrollRef = useRef();
const productModal = withReactContent(Swal);
const opts = {
// height: '390',
// width: '640',
playerVars: {
autoplay: 1,
}
};
useEffect(() => {
CategoryService.get().then((response) => {
setCategories(response);
});
}, []);
function showSubCatsHandler(catId) {
setLoader(true);
setSubCats([]);
SubCategoryService.get(catId).then((response) => {
setSubCats(response.data);
setLoader(false);
scrollRef.current.scrollIntoView({ behavior: "smooth" });
});
}
function showProductPopupHandler(productId) {
ProductService.get(productId).then((response) => {
const product = response.data;
return productModal.fire({
html:
<div>
<h3 className={modalStyles.header}>{product.AMP_Title}</h3>
<h4 className={`${modalStyles.price} ${modalStyles.header}`}>{"CHf " + product.AMP_Price}</h4>
<img className={modalStyles.image} src={process.env.REACT_APP_BACKEND_BASE_URL + 'images/products/' + product.AMP_Image} />
{
product.descriptions.map((desc, _) => (
<div key={desc.AMPD_GUID}>
{
desc.AMPD_Title === '1' && <h4 className={modalStyles.header}>{product.AMP_Title}</h4>
}
{
desc.AMPD_Image !== '' && <img src={process.env.REACT_APP_BACKEND_BASE_URL + 'images/descriptions/' + desc.AMPD_Image} className={desc.AMPD_Alignment === 'left' ? modalStyles.descImageLeft : modalStyles.descImageRight} />
}
<p className={modalStyles.description}>{desc.AMPD_Description}</p>
</div>
))
}
<br/>
<div>
<FavoriteBtn productId={product.AMP_GUID}/>
<ShoppingCartQuantity productId={product.AMP_GUID} />
</div>
<br/>
{
product.AMP_VideoId !== '' &&
<YouTube
videoId={product.AMP_VideoId}
opts={opts}
/>
}
</div>,
showConfirmButton: false,
showCloseButton: true
});
});
}
return (
<div>
<div className="categories-container">
<ResponsiveGridLayout
className="layout"
layouts={ CategoriesLayout }
breakpoints={ { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 } }
cols={ { lg: 8, md: 8, sm: 6, xs: 4, xxs: 2 } }
isDraggable={ false }
>
{
categories.map((cat, index) => (
<div key={index}>
<CategoryCard
category_id = {cat.AMC_GUID}
image = {cat.AMC_Image}
showSubCatsHandler = {showSubCatsHandler}
/>
</div>
))
}
</ResponsiveGridLayout>
{
loader &&
<Triangle
height="100"
width="100"
color='#bcad70'
ariaLabel='loading'
wrapperClass="loader"
/>
}
<div ref={scrollRef}>
{
Object.keys(subCats).map((keyName, _) => (
<Subcat
key={subCats[keyName].AMSC_GUID}
title={ subCats[keyName].AMSC_Title }
products={ subCats[keyName].products }
showProductPopupHandler = {showProductPopupHandler}
/>
))
}
</div>
</div>
</div>
);
}
export default Homepage;
I am using the component in a SweetAlert popup. I guess it's the SweetAlert component that is not getting access to the context. Does anyone have an idea how to pass the context to the SweetAlert component?
UPDATE 2
The accepted solution works great except for 1 small issue: the ShoppingCartQuantity component was not re-rendering inside the SweetAlert popup and the qty would not change visually.
I updated the component by using the qty as a state.
const ShoppingCartQuantity = ({ qty, productId, updateCart }) => {
const [quantity, setQuantity] = useState(qty);
const updateCartHandler = (productId, amount) => {
updateCart(productId, amount);
setQuantity(Math.max(quantity + amount, 1));
}
return (
<div>
<span>
<span
className={`${styles.op} ${styles.decrementBtn}`}
onClick={() => updateCartHandler(productId, -1)}
>
-
</span>
<span className={styles.qty}>{quantity}</span>
<span
className={`${styles.op} ${styles.incrementBtn}`}
onClick={() => updateCartHandler(productId, 1)}
>
+
</span>
</span>
</div>
)
}
Issue
It's very likely that the sweet alert component is rendered outside your app, and thus, outside the CartContextProvider provider. I just searched the repo docs if there is a way to specify a root element, but this doesn't seem possible since this sweet alert code isn't React specific.
See this other similar issue regarding accessing a Redux context in the alert.
Solution
It doesn't seem possible ATM to access the context value from within the modal, so IMHO a workaround could be to refactor your ShoppingCartQuantity component into a wrapper container component to access the context and a presentation component to receive the context values and any callbacks.
I suggest also just passing the amount you want to increment/decrement the quantity by to updateCart instead of passing a "+"/"-" string and operator comparison.
Example:
export const withShoppingCartContext = Component => props => {
const { cart, removeItem, updateCart } = useContext(CartContext);
return <Component {...props} {...{ cart, removeItem, updateCart }} />;
}
const ShoppingCartQuantity = ({ cart, productId, updateCart }) => {
const qty = cart.find(item => item.id === productId)?.qty ?? 0;
return (
<div>
<span>
<span
className={`${styles.op} ${styles.decrementBtn}`}
onClick={() => updateCart(productId, -1)}
>
-
</span>
<span className={styles.qty}>{qty}</span>
<span
className={`${styles.op} ${styles.incrementBtn}`}
onClick={() => updateCart(productId, 1)}
>
+
</span>
</span>
</div>
)
}
export default ShoppingCartQuantity;
In places in your app where ShoppingCartQuantity component is used within the CartContextProvider decorate it with the withShoppingCartContext HOC and use normally.
ShoppingCart
import ShoppingCartQuantityBase, {
withShoppingCartContext
} from "../../components/layout/shoppingCart/ShoppingCartQuantity";
const ShoppingCartQuantity = withShoppingCartContext(ShoppingCartQuantityBase);
const ShoppingCart = (props) => {
...
return (
...
<ShoppingCartQuantity productId={....} />
...
);
};
In places where ShoppingCartQuantity component is used outside the context, like in the sweet modal, access the context within the React code and pass in the context values and callbacks.
...
import ShoppingCartQuantity from "../../components/layout/shoppingCart/ShoppingCartQuantity";
...
function Homepage() {
...
const { cart, updateCart } = useContext(CartContext);
const productModal = withReactContent(Swal);
...
function showProductPopupHandler(productId) {
ProductService.get(productId)
.then((response) => {
const product = response.data;
return productModal.fire({
html:
<div>
...
<div>
<FavoriteBtn productId={product.AMP_GUID}/>
<ShoppingCartQuantity
productId={product.AMP_GUID}
{...{ cart, updateCart }}
/>
</div>
...
</div>,
showConfirmButton: false,
showCloseButton: true
});
});
}
return (...);
}
export default Homepage;
Additional Issues
Your context provider is mutating state when updating quantities. When updating nested state you should still create a shallow copy of the array elements that are being updated.
Example:
const CartContextProvider = (props) => {
...
const updateCart = (productId, amount) => {
// only update if item in cart
if (cart.some(item => item.id === productId)) {
// use functional state update to update from previous state
// cart.map creates shallow copy of previous state
setCart(cart => cart.map(item => item.id === productId
? {
...item, // copy item being updated into new object reference
qty: Math.max(item.qty + amount, 1), // minimum quantity is 1
}
: item
));
}
}
const removeItem = (id) => {
setCart(cart => cart.filter(item => item.id !== id));
};
return (
<CartContext.Provider value={{ cart, updateCart, removeItem }}>
{props.children}
</CartContext.Provider>
);
};
You did't show where you are using the ShoppingCart component or the ShoppingCartQuantity component.
Anyway, when you declare a route, you must pass the component, not the root element. So, this line:
<Route exact path="/" element={<Homepage/>}/>
must be
<Route exact path="/" component={Homepage}/>

How to mock data from service as hook with React Testing Library?

I'm starting using React Testing Library to make tests for a React Application, but i'm struggling to mock the data in a component that makes API calls using a Hook as service.
My component is just a functional component, with nothing extraordinary:
import React, { useEffect, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import Skeleton from '#material-ui/lab/Skeleton'
import usePanelClient from '../../../clients/PanelClient/usePanelClient'
import useUtils from '../../../hooks/useUtils'
import IconFile from '../../../assets/img/panel/ico-file.svg'
import IconExclamation from '../../../assets/img/panel/ico-exclamation.svg'
import IconPause from '../../../assets/img/panel/awesome-pause-circle.svg'
import './PendingAwards.scss'
const PendingAwards = () => {
const pendingAwardsRef = useRef(null)
const location = useLocation()
const [loading, setLoading] = useState(true)
const [pendingAwards, setPendingAwards] = useState({})
const panelClient = usePanelClient()
const { formatCurrency } = useUtils()
useEffect(() => {
panelClient()
.getPendingAwards()
.then((response) => {
setLoading(false)
setPendingAwards(response.data)
})
}, [panelClient])
useEffect(() => {
const searchParams = new URLSearchParams(location.search)
if (searchParams.get('scrollTo') === 'pendingAwards') {
window.scrollTo({
top: 0,
behavior: 'smooth',
})
}
}, [location])
return (
<div
id="pendingAwards"
className="pending-awards-container"
ref={pendingAwardsRef}
>
<span className="pending-awards-container__title">Prêmios Pendentes</span>
{loading && (
<div className="skeleton-box">
<Skeleton width="100%" height="70px" />
<Skeleton width="100%" height="70px" />
</div>
)}
{!loading && (
<div className="pending-awards-values">
<div className="pending-awards-container__quantity">
<div className="pending-awards-container__quantity-container">
<span className="pending-awards-container__quantity-container-title">
Quantidade
</span>
<span className="pending-awards-container__quantity-container-content">
<div className="pending-awards-container__quantity-container-content__icon-box">
<img src={IconFile} alt="Ícone de arquivo" />
</div>
{pendingAwards.quantity ? pendingAwards.quantity : '0'}
</span>
</div>
</div>
<div className="pending-awards-container__amount">
<div className="pending-awards-container__amount-container">
<span className="pending-awards-container__amount-container-title">
Valor Pendente
</span>
<span className="pending-awards-container__amount-container-content">
<div className="pending-awards-container__amount-container-content__icon-box">
<img src={IconPause} alt="Ícone Pause" />
</div>
{pendingAwards.amount
? formatCurrency(pendingAwards.amount)
: 'R$ 0,00'}
</span>
</div>
</div>
<div className="pending-awards-container__commission">
<div className="pending-awards-container__commission-container">
<span className="pending-awards-container__commission-container-title">
Comissão Pendente
</span>
<span className="pending-awards-container__commission-container-content">
<div className="pending-awards-container__commission-container-content__icon-box">
<img src={IconExclamation} alt="Ícone exclamação" />
</div>
{pendingAwards.commission
? formatCurrency(pendingAwards.commission)
: 'R$ 0,00'}
</span>
</div>
</div>
</div>
)}
</div>
)
}
export default PendingAwards
My service that makes the API calls it is written like this:
import { useCallback } from 'react'
import axios from 'axios'
const usePanelClient = () => {
const getQuotationCard = useCallback(() => axios.get('/api/cards/quotation'), [])
const getCommissionCard = useCallback(() => axios.get('/api/cards/commission'), [])
const getPendingAwards = useCallback(() => axios.get('/api/premium/pending'), [])
return useCallback(() => ({
getQuotationCard,
getCommissionCard,
getPendingAwards,
}), [
getQuotationCard,
getCommissionCard,
getPendingAwards,
])
}
export default usePanelClient
In my current test I've tried mocking the hook like this, but I did not have success:
import React from 'react'
import { render } from '#testing-library/react'
import { Router } from 'react-router-dom'
import { createMemoryHistory } from 'history'
import PendingAwards from './PendingAwards'
describe('PendingAwards Component', () => {
beforeEach(() => {
jest.mock('../../../clients/PanelClient/usePanelClient', () => {
const mockData = {
quantity: 820,
amount: 26681086.12,
commission: 5528957.841628,
}
return {
getPendingAwards: jest.fn(() => Promise.resolve(mockData)),
}
})
})
it('should render the PendingAwards', () => {
const history = createMemoryHistory()
history.push = jest.fn()
const { container } = render(
<Router history={history}>
<PendingAwards />
</Router>,
)
expect(container).toBeInTheDocument()
})
it('should render the PendingAwards', () => {
const history = createMemoryHistory()
history.push({
search: '&scrollTo=pendingAwards',
})
window.scrollTo = jest.fn()
render(
<Router history={history}>
<PendingAwards />
</Router>,
)
expect(window.scrollTo).toHaveBeenCalledWith({ behavior: 'smooth', top: 0 })
})
})
May someone help me resolving this? I don't feel like this is something hard, but I've tried several things, and nothing seems to resolve it.
Thanks in advance.
You must call jest.mock at the top level of the module and for mocking ES6 modules with a default export you should use __esModule: true
jest.mock('../../../clients/PanelClient/usePanelClient', () => {
const mockData = {
quantity: 820,
amount: 26681086.12,
commission: 5528957.841628,
}
return {
__esModule: true,
default: ()=> ({
getPendingAwards: jest.fn(() => Promise.resolve({data: mockData})),
}),
}});

React, pass function as prop in different component

I have a demo here
I have a simple list of products and a cart that I would like to add the products to.
The Products and Cart are separate components in the index file.
I have the function to add the products to the cart in the Products components but how do I pass this to the Cart component that is outside the Products component.
import React, { useState } from "react";
import { render } from "react-dom";
import Cart from "./Cart";
import Products from "./Products";
import "./style.css";
const App = () => {
return (
<div>
<Products />
<Cart />
</div>
);
};
render(<App />, document.getElementById("root"));
https://stackblitz.com/edit/react-ts-txpsds
// index.tsx
import React from "react";
import { render } from "react-dom";
import Cart from "./Cart";
import { CartProvider } from "./context";
import Products from "./Products";
import "./style.css";
const App = () => {
return (
<CartProvider>
<div>
<Products />
<Cart />
</div>
</CartProvider>
);
};
render(<App />, document.getElementById("root"));
// Products.tsx
import React, { createContext, useCallback, useContext, useState } from "react";
import { AddCartContext } from "./context";
import { IProduct } from "./interface";
const Products = () => {
const addItems = useContext(AddCartContext);
const items = [
{
id: 1,
name: "Product One",
price: 20
},
{
id: 2,
name: "Product Two",
price: 56
},
{
id: 3,
name: "Product Three",
price: 13
}
];
const handleClick = (
e: React.MouseEvent<HTMLInputElement, MouseEvent>,
item: IProduct
) => {
e.preventDefault();
addItems(item);
};
const listItems = items.map(item => (
<div key={item.id}>
{`${item.name}: £${item.price}`}
<input type="submit" value="+" onClick={e => handleClick(e, item)} />
</div>
));
return (
<div>
<div>
<h2>Products</h2>
{listItems}
</div>
</div>
);
};
export default Products;
const Cart = () => {
const items = useContext(CartContext);
const cartItems = items.map((item, index) => (
<div key={index}>{`${item.name}: £${item.price}`}</div>
));
return (
<div>
<h2>Cart</h2>
{cartItems}
</div>
);
};
// context.tsx
import React, { createContext, useCallback, useRef, useState } from "react";
export const CartContext = createContext([]);
export const AddCartContext = createContext(item => {});
export function CartProvider(props) {
const [items, setItems] = useState([]);
const itemsRef = useRef(items);
itemsRef.current = items;
return (
<AddCartContext.Provider
value={useCallback(item => {
setItems([...itemsRef.current, item]);
}, [])}
>
<CartContext.Provider value={items}>
{props.children}
</CartContext.Provider>
</AddCartContext.Provider>
);
}
There are 2 work-arounds for your problem.
You can make the Cart component as the child component of Products through which you can pass the addToCart() as Props to Cart. [but it is not meaningful]
You can bring the state from Product Component to App i.e make the App as a stateful component and for products and Cart, make them as statelesss. Pass the data and methods as props.
For the second option, check the link.
If you want to share a property or function between multiple components you need to put that property or function in closest parent of those components so you can pass them as props.
In your case try to add your function to your App Component and then pass the function to both Products and Cart Components
Take a look at the react docs for Lifting state up.
Move your cart state up into the closest common ancestor - App.
From App, pass cart and setCart as props into both Products and Cart as needed.
import React, { useState, Dispatch, SetStateAction } from "react";
import { render } from "react-dom";
interface IProduct {
id: number;
name: string;
price: number;
}
const App = () => {
const [cart, setCart] = useState<IProduct[]>([]);
return (
<div>
<Products cart={cart} setCart={setCart} />
<Cart cart={cart} />
</div>
);
};
function Cart({ cart = [] }: { cart: IProduct[] }) {
return (
<div>
<h2>Cart</h2>
{cart.map(item => (
<div>{`${item.name}: £${item.price}`}</div>
))}
</div>
);
}
function Products({
cart,
setCart
}: {
cart: IProduct[];
setCart: Dispatch<SetStateAction<IProduct[]>>;
}) {
const items: IProduct[] = [{id: 1,name: "Product One",price: 20},{id: 2,name: "Product Two",price: 56},{id: 3,name: "Product Three",price: 13}];
const handleClick = (
e: React.MouseEvent<HTMLInputElement, MouseEvent>,
item: IProduct
) => {
e.preventDefault();
setCart([...cart, item]);
};
return (
<div>
<div>
<h2>Products</h2>
{items.map(item => (
<div>
{`${item.name}: £${item.price}`}
<input
type="submit"
value="+"
onClick={e => setCart([...cart, item])}
/>
</div>
))}
</div>
</div>
);
}
render(<App />, document.getElementById("root"));
Stackblitz
Keep a common items variable and a function addItems in App.tsx and you need to pass this function as prop to Product component which when adds a product will call this same function in App.tsx file and update the items list.And this items list can be passed to the Cart component.
Here check this live demo:https://stackblitz.com/edit/react-ts-qq5cea?file=index.tsx
I improved your code a little, the main thing that needed to be done was to move the state to a higher level
https://stackblitz.com/edit/react-ts-iicy7v?file=Shop.tsx
const App = () => {
return (
<div>
<Shop Products={Products} Cart={Cart} />
</div>
);
};
move logic to Shop:
const Shop = ({ Products, Cart }) => {
const [cart, setCart] = useState([]);
const addToCart = (item: IProduct) => {
setCart([...cart, item]);
};
const removeFromCart = (item: IProduct) => {
const itemWillBeRemoved = cart.find(e => e.id === item.id);
const index = cart.indexOf(itemWillBeRemoved);
const newCart = [...cart];
newCart.splice(index, 1);
setCart(newCart);
};
const items = [
{
id: 1,
name: "Product One",
price: 20
},
{
id: 2,
name: "Product Two",
price: 56
},
{
id: 3,
name: "Product Three",
price: 13
}
];
return (
<div>
<Products items={items} addToCart={addToCart} />
<Cart items={cart} removeFromCart={removeFromCart} />
</div>
);
};
But the best way - use State Management
Redux

How to mock stateless child component event when testing parent component

as mentioned in the title I'm trying to set up some test for <Search /> component, in particular I want to test the useState hooks.
After mocking the Redux store and creating a shallow wrapper I tried to simulate an input from the child component DisplaySearchBar but apparently I cannot even mamage to select it.
That's the error I get:
Method “props” is meant to be run on 1 node. 0 found instead.
Here's Search.js
import React, { useState } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { handleScriptLoad } from '../../../helpers/Autocomplete';
import { getRestaurants, setAlert } from '../../../actions/restaurantAction';
import DisplaySearchBar from '../../layout/DisplaySearchBar/DisplaySearchBar';
import styles from './Search.module.scss';
const Search = ({ getRestaurants, setAlert }) => {
const [where, setWhere] = useState('');
const [what, setWhat] = useState('');
const [sortBy, setSortBy] = useState('rating');
const sortByOptions = {
'Highest Rated': 'rating',
'Best Match': 'best_match',
'Most Reviewed': 'review_count',
};
// give active class to option selected
const getSortByClass = (sortByOption) => {
if (sortBy === sortByOption) {
return styles.active;
} else {
return '';
}
};
// set the state of a sorting option
const handleSortByChange = (sortByOption) => {
setSortBy(sortByOption);
};
//handle input changes
const handleChange = (e) => {
if (e.target.name === 'what') {
setWhat(e.target.value);
} else if (e.target.name === 'where') {
setWhere(e.target.value);
}
};
const onSubmit = (e) => {
e.preventDefault();
if (where && what) {
getRestaurants({ where, what, sortBy });
setWhere('');
setWhat('');
setSortBy('best_match');
} else {
setAlert('Please fill all the inputs');
}
};
// displays sort options
const renderSortByOptions = () => {
return Object.keys(sortByOptions).map((sortByOption) => {
let sortByOptionValue = sortByOptions[sortByOption];
return (
<li
className={getSortByClass(sortByOptionValue)}
key={sortByOptionValue}
onClick={() => handleSortByChange(sortByOptionValue)}
>
{sortByOption}
</li>
);
});
};
return (
<DisplaySearchBar
onSubmit={onSubmit}
handleChange={handleChange}
renderSortByOptions={renderSortByOptions}
where={where}
what={what}
handleScriptLoad={handleScriptLoad}
/>
);
};
Search.propTypes = {
getRestaurants: PropTypes.func.isRequired,
setAlert: PropTypes.func.isRequired,
};
export default connect(null, { getRestaurants, setAlert })(Search);
DisplaySearchBar.js
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { clearSearch } from '../../../actions/restaurantAction';
//Import React Script Libraray to load Google object
import Script from 'react-load-script';
import Fade from 'react-reveal/Fade';
import Alert from '../Alert/Alert';
import styles from './DisplaySearchBar.module.scss';
const DisplaySearchBar = ({
renderSortByOptions,
onSubmit,
where,
handleChange,
what,
handleScriptLoad,
restaurants,
clearSearch,
}) => {
const googleUrl = `https://maps.googleapis.com/maps/api/js?key=${process.env.REACT_APP_GOOGLE_API_KEY}&libraries=places`;
// {googleUrl && <Script url={googleUrl} onLoad={handleScriptLoad} />}
return (
<section className={styles.searchBar}>
<form onSubmit={onSubmit} className={styles.searchBarForm}>
<legend className="title">
<Fade left>
<h1>Where are you going to eat tonight?</h1>
</Fade>
</legend>
<Fade>
<fieldset className={styles.searchBarInput}>
<input
type="text"
name="where"
placeholder="Where do you want to eat?"
value={where}
onChange={handleChange}
id="autocomplete"
/>
<input
type="text"
name="what"
placeholder="What do you want to eat?"
onChange={handleChange}
value={what}
/>
<div className={styles.alertHolder}>
<Alert />
</div>
</fieldset>
<fieldset className={styles.searchBarSubmit}>
<input
id="mainSubmit"
className={`${styles.myButton} button`}
type="submit"
name="submit"
value="Search"
></input>
{restaurants.length > 0 && (
<button
className={`${styles.clearButton} button`}
onClick={clearSearch}
>
Clear
</button>
)}
</fieldset>
</Fade>
</form>
<article className={styles.searchBarSortOptions}>
<Fade>
<ul>{renderSortByOptions()}</ul>
</Fade>
</article>
</section>
);
};
DisplaySearchBar.propTypes = {
renderSortByOptions: PropTypes.func.isRequired,
where: PropTypes.string.isRequired,
handleChange: PropTypes.func.isRequired,
what: PropTypes.string.isRequired,
handleScriptLoad: PropTypes.func.isRequired,
restaurants: PropTypes.array.isRequired,
clearSearch: PropTypes.func.isRequired,
};
const mapStatetoProps = (state) => ({
restaurants: state.restaurants.restaurants,
});
export default connect(mapStatetoProps, { clearSearch })(DisplaySearchBar);
And Search.test.js
import React from 'react';
import { mount } from 'enzyme';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import Search from '../Search';
import DisplaySearchBar from '../../../layout/DisplaySearchBar/DisplaySearchBar';
const mockStore = configureStore();
const initialState = {
restaurants: { restaurants: ['foo'], alert: null },
};
describe('Search', () => {
test('renders withut errors', () => {
const store = mockStore(initialState);
const wrapper = mount(
<Provider store={store}>
<Search setAlert={jest.fn()} getRestaurants={jest.fn()} />
</Provider>
);
wrapper.find(DisplaySearchBar).props();
});
});
Thanks for your help!
shallow doesn't work for react-redux new versions (>= 6).
Use mount instead:
const wrapper = mount( // <-- changed shallow to mount.
<Provider store={store}>
<Search {...props} />
</Provider>
);
Run It On Sandbox (Use tests tab to run tests.)
Try to mount it like this:
const wrapper = shallow(
<Provider store={store} />
<Search setAlert=jest.fn() getRestaurants=jest.fn() />
</Provider>
);

Resources