I have an issue using jest and enzymes for my unit testing on react. I have a component connected to redux and i want to test a method in my component that update the state. heres's my component :
import React, {Component} from 'react';
import Form from 'react-jsonschema-form';
import pick from 'lodash/pick';
import {bindActionCreators} from 'redux';
import fields from './fields';
import widgets from './widgets';
import {initContainerActions} from '../../utils/actionHelper';
import {actions as modelActions} from './actions';
import {connect} from '../../utils/connect';
import reducerRegistry from '../ReducerRegistry';
import reducer from './reducer';
type StateProperties = {
roleModelSchema: Object,
roleUiSchema: Object
};
export type FormContainerProps = {
model: Object,
uiSchema: Object,
userModelLoad: Function,
isLoading: boolean,
idContainer: string
};
export class FormContainer extends Component<FormContainerProps, StateProperties> {
fields = {...fields};
widgets = {...widgets};
constructor(props) {
super(props);
const {idContainer} = props;
reducerRegistry.register(idContainer, reducer(idContainer));
this.state = {
roleModelSchema: {},
roleUiSchema: {}
};
}
componentDidMount = async () => {
const {userModelLoad} = this.props;
await userModelLoad();
this.renderRoleForm();
};
renderRoleForm = () => {
const {model, uiSchema} = this.props;
const modelProperties = Object.keys(model.properties);
const roleUiSchema = pick(uiSchema, modelProperties);
this.setState({
roleUiSchema,
roleModelSchema: model
});
};
render = () => {
const {roleModelSchema, roleUiSchema} = this.state;
const {isLoading} = this.props;
if (isLoading) {
return <div>...Loading</div>;
}
return (
<div className="container">
<div className="columns">
<div className="column col-12">
<Form schema={roleModelSchema} uiSchema={roleUiSchema} liveValidate fields={this.fields} widgets={this.widgets}>
<div>
<button type="submit" className="btn btn-primary">
Submit
</button>
<button type="button">Cancel</button>
</div>
</Form>
</div>
</div>
</div>
);
};
}
const mapDispatchToProps = (dispatch, props) => {
const initializedActions = initContainerActions(modelActions, props.idContainer);
return bindActionCreators(initializedActions, dispatch);
};
export default connect(
(state, props) => state[props.idContainer] || {},
mapDispatchToProps
)(FormContainer);
And the test :
const mockStore = configureMockStore([]);
const context = React.createContext();
describe('Check if renderRoleForm update state', () => {
it('renders without crashing', () => {
// Initialize mockstore with empty state
const initialState = {roleUiSchema: {}};
const store = mockStore(initialState);
const wrapper = shallow(
<Provider store={store} context={context}>
<FormContainer model={model} uiSchema={uiSchema} context={context}
/>
</Provider>
);
const instance = wrapper.instance();
instance.renderRoleForm();
expect(wrapper.state().roleUiSchema).not.toBeEmpty();
});
});
The error i have : TypeError: instance.renderRoleForm is not a function
I tried with mount, dive with shallow and same problem.
If you have any idea :)
Thanks
const wrapper = mount(
<Provider store={store} context={context}>
<FormContainer model={model} uiSchema={uiSchema} context={context} />
</Provider> );
//
wrapper.instance().renderRoleForm();
Related
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;
I am receiving this error on CodeSandbox: Cannot read properties of undefined (reading 'getState')
What is the proper way to use createSelectorHook()? Could someone please create a CodeSandbox to illustrate its usage?
index.js:
import React from "react";
import { createRoot } from "react-dom/client";
import { createStore } from "redux";
import reducer from "./reducer";
import App from "./App";
import myContext from "./context";
import {
Provider,
createStoreHook,
createDispatchHook,
createSelectorHook
} from "react-redux";
export const useStore = createStoreHook(myContext);
export const useDispatch = createDispatchHook(myContext);
export const useSelector = createSelectorHook(myContext);
const store = createStore(reducer, 0);
const root = createRoot(document.getElementById("root"));
root.render(
<Provider store={store} context={myContext}>
<App />
</Provider>
);
App.js:
import React, { useState, useRef } from "react";
import { useSelector, useDispatch } from "./";
import context from "./context";
const Counter = (props) => {
return (
<context.Provider value={props}>
<A />
</context.Provider>
);
};
const A = (props) => {
return <B />;
};
const B = (props) => {
return <C />;
};
const C = (props) => {
const v = useSelector((state) => state);
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch({ type: "DECREMENT" })}>-</button>
value:<span>{v}</span>
<button onClick={() => dispatch({ type: "INCREMENT" })}>+</button>
</div>
);
};
const App = () => {
const stepInput = useRef(1);
const [step, updateStep] = useState(1);
return (
<div>
step:
<input
ref={stepInput}
type="number"
onChange={() => updateStep(stepInput.current.value)}
/>
<Counter step={step} />
</div>
);
};
export default App;
I think react-redux v8.0.0 removed DefaultRootState (inferring the state):
Now that React-Redux itself is written in TS, we've opted to remove the DefaultRootState type entirely. State generics now default to unknown instead.
source: the release notes
So, createStoreHook appears to exist if you'd like to (a) bind to a custom context or, (b) ensure a call to something like useState().getStore() returns a properly typed redux store.
Note, I am not positive, if this is a correct or even suggested use case for (b). I also discovered that this works following the documented example for useAppDispatch:
export const useAppStore = () => useStore<RootState>();
So I have a context.
import React from "react";
const CartContext = React.createContext({
numberOfMeals: 0,
meals: [],
});
export default CartContext;
I then import this into my App.JS
import React from "react";
import MealsBase from "./Components/Meals/MealsBase";
import CartContext from "./Context/CartContext";
function App() {
return (
<CartContext.Provider value={{ numberOfMeals: 0, meals: [] }}>
<MealsBase/>
</CartContext.Provider>
);
}
export default App;
I use this context within two components.
Component 1 :
import React, {useContext} from "react";
import "./CartIcon.css";
import CartContext from "../../Context/CartContext";
const CartIcon = () => {
const cartContext = useContext(CartContext);
return (
<React.Fragment>
<button className="border">
<h3>Your Cart : {cartContext.numberOfMeals}</h3>
</button>
</React.Fragment>
);
};
export default CartIcon;
Component 2 :
import React,{ useState, useContext } from "react";
import CartContext from "../../Context/CartContext";
const MealForm = () => {
const [amount, setAmount] = useState(0);
const mealContext = useContext(CartContext);
const amountHandler = (event) => {
setAmount(event.target.valueAsNumber);
};
const formHandler = (event) => {
mealContext.numberOfMeals = mealContext.numberOfMeals + amount;
console.log(mealContext.numberOfMeals);
event.preventDefault();
}
return(
<React.Fragment>
<form onSubmit={formHandler}>
<label>Amount</label>
<input type="number" step={1} min={-1} max={50} value={amount} onChange={amountHandler}/>
<button type="submit"> + Add </button>
</form>
</React.Fragment>
);
};
export default MealForm
The context seems to update in component two but not in component one.
I have an understanding that if useContexts values change then it causes a component to re-render. So I am struggling to understand why the CartIcon.js file is not being re-rendered.
You aren't really updating the value the React way, you are mutating it. Move the context value into state and provide that out.
const CartContext = React.createContext({
numberOfMeals: 0,
setNumberOfMeals: () => {},
meals: [],
setMeals: () => {},
});
...
function App() {
const [numberOfMeals, setNumberOfMeals] = useState(0);
const [meals, setMeals] = useState([]);
return (
<CartContext.Provider value={{ numberOfMeals, setNumberOfMeals, meals, setMeals }}>
<MealsBase/>
</CartContext.Provider>
);
}
...
const MealForm = () => {
const [amount, setAmount] = useState(0);
const { setNumberOfMeals } = useContext(CartContext);
const amountHandler = (event) => {
setAmount(event.target.valueAsNumber);
};
const formHandler = (event) => {
event.preventDefault();
setNumberOfMeals(count => count + amount);
}
return(
<React.Fragment>
<form onSubmit={formHandler}>
<label>Amount</label>
<input type="number" step={1} min={-1} max={50} value={amount} onChange={amountHandler}/>
<button type="submit"> + Add </button>
</form>
</React.Fragment>
);
};
I am working on a site that has a piece a global state stored in a file using zustand. I need to be able to set that state in a class component. I am able to set the state in a functional component using hooks but I'm wondering if there is a way to use zustand with class components.
I've created a sandbox for this issue if that's helpful:
https://codesandbox.io/s/crazy-darkness-0ttzd
here I'm setting state in a functional component:
function MyFunction() {
const { setPink } = useStore();
return (
<div>
<button onClick={setPink}>Set State Function</button>
</div>
);
}
my state is stored here:
export const useStore = create((set) => ({
isPink: false,
setPink: () => set((state) => ({ isPink: !state.isPink }))
}));
how can I set state here in a class componet?:
class MyClass extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div>
<button
onClick={
{
/* setPink */
}
}
>
Set State Class
</button>
</div>
);
}
}
A class component's closest analog to a hook is the higher order component (HOC) pattern. Let's translate the hook useStore into the HOC withStore.
const withStore = BaseComponent => props => {
const store = useStore();
return <BaseComponent {...props} store={store} />;
};
We can access the store as a prop in any class component wrapped in withStore.
class BaseMyClass extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const { setPink } = this.props.store;
return (
<div>
<button onClick={setPink}>
Set State Class
</button>
</div>
);
}
}
const MyClass = withStore(BaseMyClass);
Seems that it uses hooks, so in class you can work with the instance:
import { useStore } from "./store";
class MyClass extends Component {
render() {
return (
<div>
<button
onClick={() => {
useStore.setState({ isPink: true });
}}
>
Set State Class
</button>
</div>
);
}
}
Create a React Context provider that both functional and class-based components can consume. Move the useStore hook/state to the context Provider.
store.js
import { createContext } from "react";
import create from "zustand";
export const ZustandContext = createContext({
isPink: false,
setPink: () => {}
});
export const useStore = create((set) => ({
isPink: false,
setPink: () => set((state) => ({ isPink: !state.isPink }))
}));
export const ZustandProvider = ({ children }) => {
const { isPink, setPink } = useStore();
return (
<ZustandContext.Provider
value={{
isPink,
setPink
}}
>
{children}
</ZustandContext.Provider>
);
};
index.js
Wrap your application with the ZustandProvider component.
...
import { ZustandProvider } from "./store";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<ZustandProvider>
<App />
</ZustandProvider>
</StrictMode>,
rootElement
);
Consume the ZustandContext context in both components
MyFunction.js
import React, { useContext } from "react";
import { ZustandContext } from './store';
function MyFunction() {
const { setPink } = useContext(ZustandContext);
return (
<div>
<button onClick={setPink}>Set State Function</button>
</div>
);
}
MyClass.js
import React, { Component } from "react";
import { ZustandContext } from './store';
class MyClass extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div>
<button
onClick={this.context.setPink}
>
Set State Class
</button>
</div>
);
}
}
MyClass.contextType = ZustandContext;
Swap in the new ZustandContext in App instead of using the useStore hook directly.
import { useContext} from 'react';
import "./styles.css";
import MyClass from "./MyClass";
import MyFunction from "./MyFunction";
import { ZustandContext } from './store';
export default function App() {
const { isPink } = useContext(ZustandContext);
return (
<div
className="App"
style={{
backgroundColor: isPink ? "pink" : "teal"
}}
>
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<MyClass />
<MyFunction />
</div>
);
}
If you aren't able to set any specific context on the MyClass component you can use the ZustandContext.Consumer to provide the setPink callback as a prop.
<ZustandContext.Consumer>
{({ setPink }) => <MyClass setPink={setPink} />}
</ZustandContext.Consumer>
MyClass
<button onClick={this.props.setPink}>Set State Class</button>
This worked out pretty well for me.
:
import React, { Component } from "react";
import { useStore } from "./store";
class MyClass extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div>
<button
onClick={
useStore.getState().setPink() // <-- Changed code
}
>
Set State Class
</button>
</div>
);
}
}
export default MyClass;
I like to create a high order component similar to redux connect:
function connectZustand(useStore, selector) {
return (Component) =>
React.forwardRef((props, ref) => <Component ref={ref} {...props} {...useStore(selector, shallow)} />);
}
eg:
import React, { Component } from 'react';
import create from 'zustand';
import shallow from 'zustand/shallow';
function connectZustand(useStore, selector) {
return (Component) =>
React.forwardRef((props, ref) => <Component ref={ref} {...props} {...useStore(selector, shallow)} />);
}
const useStore = create((set) => ({
isPink: false,
setPink: () => set((state) => ({ isPink: !state.isPink })),
}));
class MyClass extends Component {
render() {
const { setPink } = this.props;
return (
<div>
<button onClick={() => setPink()}>Set State Class</button>
</div>
);
}
}
const MyClassWithZustand = connectZustand(useStore, (state) => ({ setPink: state.setPink }))(MyClass);
export default function Test() {
const isPink = useStore((state) => state.isPink);
return (
<>
<MyClassWithZustand />
{isPink ? 'Is Pink' : 'Is Not Pink'}
</>
);
}
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>
);