Test a react component using test and react-testing-library - javascript

I just joined a team where we use react, redux, recompose to construct components to build UI. There aren't any unit tests in the application and there isn't consistent architecture for the application. I decided to take it upon myself to add unit tests using jest and react-testing-library. I succeed with few snapshot tests but I am struggling with unit testing. I am still learning react and pretty new to redux. I would love some suggestion. I am going to share a component which renders a table with column and row. I would love a feedback.
import React, { useEffect, useState } from 'react';
import { compose } from 'recompose';
import { connect } from 'react-redux';
import { clearAll, fetchContacts } from '~/store/resources/contacts/actions';
import { isDevEnv } from '~/utils';
import Sidebar from './Sidebar';
import Table from './Table';
import Toolbar from './Toolbar';
const Contacts = ({ clearAll, fetchContacts, ...props }) => {
const [searchValue, setSearchValue] = useState('');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [canonicalFormValues, setCanonicalFormValues] = useState({ active: true });
useEffect(() => {
fetchContacts();
return () => {
clearAll();
};
}, []);
const closeSidebar = () => {
if (isDevEnv) {
console.log('hit close function');
}
setIsSidebarOpen(false);
};
return (
<div>
<Toolbar
searchValue={searchValue}
setSearchValue={setSearchValue}
setIsSidebarOpen={setIsSidebarOpen}
/>
<Table setCanonicalFormValues={setCanonicalFormValues} />
<Sidebar
isSidebarOpen={isSidebarOpen}
closeSidebar={closeSidebar}
canonicalFormValues={canonicalFormValues}
/>
{isDevEnv && (
<div>
This is coming from the contact folder
<br />
state values:
<br />
{JSON.stringify({ searchValue })}
<br />
{JSON.stringify({ isSidebarOpen })}
<br />
{JSON.stringify({ canonicalFormValues })}
</div>
)}
</div>
);
};
const mapDispatchToProps = {
clearAll,
fetchContacts,
};
export default compose(
connect(
null,
mapDispatchToProps,
),
)(Contacts);

I generally start out with a simple "should render without crashing" test. I prefer to export and test the undecorated component, in your case Contacts.
export const Contacts = ({ clearAll, fetchContacts, ...props }) => { ...
In the test file
import React from 'react';
import { render } from '#testing-library/react';
import { Contacts } from '.';
// mock the other imported components, they should already be tested alone, right?
jest.mock('./Sidebar');
jest.mock('./Table');
jest.mock('./Toolbar');
describe('Contacts', () => {
it('should render without crashing', () = {
render(
<Contacts
// pass all the props necessary for a basic render
clearAll={jest.fn()}
fetchContacts={jest.fn()}
/>
);
});
});
At this point I run a code coverage report to see how much I have, then add more tests with varying prop values and/or using the react-testing-library's matchers to target buttons or elements to assert text is visible or trigger callbacks, etc, until I have the coverage I want.
Sometimes some of your components may rely on context provider, and in this case RTL allows you to specify wrappers. For example if your component gets decorated with react-intl for string localization, you can provide a wrapper.
export const Contacts = ({ clearAll, fetchContacts, intl }) => { ...
...
export default compose(
connect(
null,
mapDispatchToProps,
),
injectIntl,
)(Contacts);
Create a wrapper
import { IntlProvider } from 'react-intl';
const IntlWrapper = ({ children }) => (
<IntlProvider locale="en">{children}</IntlProvider>
);
const intlMock = {
...
formatMessage: message => message,
...
};
and to test, specify the wrapper in the render options argument
render(
<Contacts
// pass all the props necessary for a basic render
clearAll={jest.fn()}
fetchContacts={jest.fn()}
intl={intlMock}
/>,
{
wrapper: IntlWrapper
}
);
react-testing-library has a lot of documentation, but it is worth reading through. Hope this helps you get going.

Related

Connecting Redux Stores using Redux Toolkit

for various reasons, my workplace is currently trying to introduce React/Redux into our project. Our project utilizes Shopify and it's Liquid templates along with jQuery. They want to migrate towards React, and we have thus far been injecting React by looking for specific ID's and injecting React Components in that way.
Due to this, and because we are now needing a store to keep and render data, I've come to a strange issue. If I wrap each one of these injected components with a Provider and store, I can essentially have each component with it's own store, but that doesn't help at all as it's pretty much mimicking local state.
Is there a way for me to 'connect' and share the store with multiple components?
I thought about wrapping the whole project, as is usually the case, but doing so would/should render the Liquid useless.
Here's an example of what's going on:
import ReactDOM from "react-dom";
import React from "react";
import attrToProps from "../../../partials/data-attribute-to-props.js";
import FavoritesToggler from "./index.js";
import store from "./../../Store/store"
import { Provider } from "react-redux"
const rootEl = document.getElementById("fav-container-react");
if(rootEl) {
const props = attrToProps(rootEl.attributes);
rootEl && ReactDOM.render(<Provider store={store}><FavoritesToggler {...props} /></Provider>, rootEl);
}
This is injected into a div that contains the 'fav-container-react' id.
But we have several of these at the moment, and I'm wondering how to get them to connect all to the same store.
Any ideas would be appreciated, including perhaps a possible change in architecture (we're required to 'show progress' in order to continue getting funded, so starting a new project isn't an option. Meaning I need a solution that is able to continuously update legacy code to React)
You don't need a provider if the component has a store prop and you use connect. The hooks won't work so I'd advise using a containers that can be refactored using the hooks when all of the project is converted to React.
const { connect } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const initialState = { counter: 0 };
//action types
const UP = 'UP';
//action creators
const up = () => ({
type: UP,
});
const reducer = (state, { type }) => {
if (type === UP) {
return { ...state, counter: state.counter + 1 };
}
return state;
};
//selectors
const selectCounter = (state) => state.counter;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
() => (next) => (action) => next(action)
)
)
);
const App = ({ count, up }) => {
return <button onClick={up}>{count}</button>;
};
const AppContainer = connect(
(state) => ({
count: selectCounter(state),
}),
{ up }
)(App);
const OtherContainer = connect(
(state) => ({
count: selectCounter(state),
})
)(({count})=><h1>{count}</h1>);
ReactDOM.render(
<AppContainer store={store} />,
document.getElementById('root-1')
);
ReactDOM.render(
<AppContainer store={store} />,
document.getElementById('root-2')
);
ReactDOM.render(
<OtherContainer store={store} />,
document.getElementById('root-3')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root-1"></div>
<div id="root-2"></div>
<div id="root-3"></div>

Combine hooks without using Provider

I'm trying to combine several contexts that are feed with some async operations, in my app's pages.
I would like to combine these contexts without using the Context.Provider because it could be verbose. For example,
<Route path="/discover">
<MainContainer extraClass="discover-container" hasHeader={true}>
<UserContext>
<ContentContextProvider>
<NotificationContext>
<Discover />
</NotificationContext>
</ContentContextProvider>
</UserContext>
</MainContainer>
</Route>
In each of these Context I wrapper the child with the context. Fe,
import React from "react";
import useAllContent from "utils/hooks/useAllContent";
const ContentContext = React.createContext({});
export const ContentContextProvider = ({ children }) => {
const { allContent, setAllContents } = useAllContent([]);
return (
<ContentContext.Provider value={{ allContent, setAllContents }}>
{children}
</ContentContext.Provider>
);
};
export default ContentContext;
This works, but as I mentioned before is very verbose so i would like to use the Contexts like an objetcs to combine between them.
I tried:
import { useState, useEffect, useCallback } from "react";
import { DataStore, Predicates } from "#aws-amplify/datastore";
import { Content } from "models";
const useAllContent = (initialValue) => {
const [allContent, setContent] = useState(initialValue);
const setAllContents = useCallback(async () => {
const contents = await DataStore.query(Content, Predicates.ALL);
setContent(contents);
}, []);
useEffect(() => {
if (allContent === 0) setAllContents();
}, [allContent, setAllContents]);
return { allContent, setAllContents };
};
export default useAllContent;
import React from "react";
import useAllContent from "utils/hooks/useAllContent";
const { allContent, setAllContents } = useAllContent([]);
const ContentContext = React.createContext({ allContent, setAllContents });
export default ContentContext;
But I break the rule × Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
How could i achieve it?
Combining instances of React.Context in the way you describe would require manipulating the values they contain. But there is no way to access the value of a Context without using its corresponding Provider. I sympathize with the dislike of verboseness, but it is unavoidable in this case.

Sharing data between React components

I have a search form in React that performs an API call and saves the data called nameservers in-state using the useState() React hook.
I'm trying to determine how I can pass this data to another component so the data can be used for rendering elsewhere on the page.
Currently, I'm just outputting the data in my return block which appears below the search form.
import React, { useState } from "react";
import { Spinner, Form, Button, Container, Col } from "react-bootstrap";
import { FaSearch } from "react-icons/fa";
const SearchForm: React.FC = () => {
const [domain, setDomain] = useState("");
const [loading, setLoading] = useState(false);
const [nameservers, setNameservers] = useState([]);
const [submitted, setSubmitted] = useState(false);
const formText = "Search for a domains";
const handleChange = (event: any) => {
setDomain(event.target.value);
};
const handleSubmit = (event: any) => {
event.preventDefault();
setLoading(true);
setSubmitted(true);
console.log(domain);
fetch(`https://dns.google.com/resolve?name=${domain}&type=NS`)
.then(results => results.json())
.then(data => {
setLoading(false);
if (data && data.Answer) {
data.Answer.sort((a: any, b: any) => a.data.localeCompare(b.data));
setNameservers(data.Answer);
}
});
};
return (
<>
<Form onSubmit={handleSubmit}>
<Form.Row>
<Col />
<Col>
<Form.Control
type="text"
placeholder={formText}
value={domain}
onChange={handleChange}
autoFocus
required
/>
</Col>
<Col>
<Button variant="primary" type="submit">
<FaSearch /> Search
</Button>
</Col>
</Form.Row>
</Form>
<hr />
{submitted &&
nameservers.map((server: any, index: number) => (
<li key={index}>{server.data}</li>
))}
{submitted && nameservers.length === 0 && <small>Error</small>}
{loading && (
<Container className="text-center">
<Spinner animation="grow" size="sm" />
<Spinner animation="grow" size="sm" />
<Spinner animation="grow" size="sm" />
</Container>
)}
</>
);
};
export default SearchForm;
There are multiple ways to share data between components. You can use one of the following options:
If the component to which you want to pass the data is a child of SearchForm component, then you can pass it as a prop.
If you are managing state via redux, you can connect components to the redux store and get the required data from the redux store in your components.
You can also use React's Context API to share common data between components that may or may not have a parent-child relationship.
If the component that needs the data from SearchForm component, is a parent component of SearchForm component, you can pass a callback function to the SearchForm component as a prop and when data is available in SearchForm component, call the callback function, received as a prop, and pass the data as an argument to that callback function.
Ciao, when I want to share data between components I use React-Redux.
Lets make an example:
Suppose that you want to share data received by server (nameservers).
At first install react-redux:
npm install react-redux
npm install redux
npm install #reduxjs/toolkit
Now we have to create the reducer and the action:
Lets say you have your component in a folder called "/components/MyComponent". Create a file called MyReducer.js.
/components/MyComponent/MyReducer.js
import { createReducer } from '#reduxjs/toolkit';
const initialState = {
nameservers: undefined,
};
const LoginReducer = createReducer(initialState, {
["SET_NAMESERVERS"]: (state, action) => {
state.nameservers= action.payload.nameservers;
},
})
export default MyReducer;
Now, on the same folder, createa file called MyAction.js
/components/MyComponent/MyAction.js
export const setNameServers = data => ({
type: "SET_NAMESERVERS",
payload: { nameservers: data }
});
Then create the store:
On your project root create a folder callled redux. Inside this create a folder called store. Then on this folder create a file called index.js.
redux/store/index.js
import { createStore, combineReducers } from "redux";
import MyReducer from '../../components/MyComponent/MyReducer';
const reducers = combineReducers({
MyReducer,
});
const store = createStore(reducers);
export default store;
Now on index.js file on root folder lets pass the store already created:
index.js
...
import { Provider } from 'react-redux';
import store from "./redux/store";
ReactDOM.render((
<Provider store={store}>
<App />
</Provider>
), document.getElementById('root') || document.createElement('div'));
We have almost done. On your component (MyComponent) you retrieve data from server. Once you have data, lets dispatch data to share into the store:
/components/MyComponent/MyComponent.js
...
import { useDispatch } from 'react-redux';
import { setNameServers } from './MyComponentAction';
const MyComponent: React.FC = () => {
const [nameservers, setNameservers] = useState([]);
const dispatch = useDispatch();
....
const handleSubmit = (event: any) => {
...
fetch(`https://dns.google.com/resolve?name=${domain}&type=NS`)
.then(results => results.json())
.then(data => {
setLoading(false);
if (data && data.Answer) {
data.Answer.sort((a: any, b: any) => a.data.localeCompare(b.data));
setNameservers(data.Answer);
dispatch(setNameServers(data.Answer)); // here the magic
}
});
};
};
Done! now you have nameservers on your react redux store and you can easly get it from another component like this:
OtherComponent.js
import { useSelector } from 'react-redux';
const OtherComponent: React.FC = () => {
const nameservers = useSelector(state => state.MyReducer.nameservers);
};
And if you log nameservers somewhere in OtherComponent you will see data retrieved in MyComponent. Awesome!
I’d do what Yousaf suggested.
SearchForm should have two props:
nameservers
handleSubmit
(This will make it easy to write a Storybook story and any tests).
Then make a wrapper for SearchForm that does the API call (handleSubmit) and sets nameservers in context. Now nameservers can be accessed elsewhere in the app.

React context not updating

I have set a basic sample project that use Context to store the page title, but when I set it the component is not rerendered.
Principal files:
Context.js
import React from 'react'
const Context = React.createContext({})
export default Context
AppWrapper.js
import React from 'react'
import App from './App'
import Context from './Context'
function AppWrapper () {
return (
<Context.Provider value={{page: {}}}>
<App />
</Context.Provider>
)
}
export default AppWrapper
App.js
import React, { useContext } from 'react';
import Context from './Context';
import Home from './Home';
function App() {
const { page } = useContext(Context)
return (
<>
<h1>Title: {page.title}</h1>
<Home />
</>
);
}
export default App;
Home.js
import React, { useContext } from 'react'
import Context from './Context'
function Home () {
const { page } = useContext(Context)
page.title = 'Home'
return (
<p>Hello, World!</p>
)
}
export default Home
full code
What am I doing wrong?
Think about React context just like you would a component, if you want to update a value and show it then you need to use state. In this case your AppWrapper where you render the context provider is where you need to track state.
import React, {useContext, useState, useCallback, useEffect} from 'react'
const PageContext = React.createContext({})
function Home() {
const {setPageContext, page} = useContext(PageContext)
// essentially a componentDidMount
useEffect(() => {
if (page.title !== 'Home')
setPageContext({title: 'Home'})
}, [setPageContext])
return <p>Hello, World!</p>
}
function App() {
const {page} = useContext(PageContext)
return (
<>
<h1>Title: {page.title}</h1>
<Home />
</>
)
}
function AppWrapper() {
const [state, setState] = useState({page: {}})
const setPageContext = useCallback(
newState => {
setState({page: {...state.page, ...newState}})
},
[state, setState],
)
const getContextValue = useCallback(
() => ({setPageContext, ...state}),
[state, updateState],
)
return (
<PageContext.Provider value={getContextValue()}>
<App />
</PageContext.Provider>
)
}
Edit - Updated working solution from linked repository
I renamed a few things to be a bit more specific, I wouldn't recommend passing setState through the context as that can be confusing and conflicting with a local state in a component. Also i'm omitting chunks of code that aren't necessary to the answer, just the parts I changed
src/AppContext.js
export const updatePageContext = (values = {}) => ({ page: values })
export const updateProductsContext = (values = {}) => ({ products: values })
export const Pages = {
help: 'Help',
home: 'Home',
productsList: 'Products list',
shoppingCart: 'Cart',
}
const AppContext = React.createContext({})
export default AppContext
src/AppWrapper.js
const getDefaultState = () => {
// TODO rehydrate from persistent storage (localStorage.getItem(myLastSavedStateKey)) ?
return {
page: { title: 'Home' },
products: {},
}
}
function AppWrapper() {
const [state, setState] = useState(getDefaultState())
// here we only re-create setContext when its dependencies change ([state, setState])
const setContext = useCallback(
updates => {
setState({ ...state, ...updates })
},
[state, setState],
)
// here context value is just returning an object, but only re-creating the object when its dependencies change ([state, setContext])
const getContextValue = useCallback(
() => ({
...state,
setContext,
}),
[state, setContext],
)
return (
<Context.Provider value={getContextValue()}>
...
src/App.js
...
import AppContext, { updateProductsContext } from './AppContext'
function App() {
const [openDrawer, setOpenDrawer] = useState(false)
const classes = useStyles()
const {
page: { title },
setContext,
} = useContext(Context)
useEffect(() => {
fetch(...)
.then(...)
.then(items => {
setContext(updateProductsContext({ items }))
})
}, [])
src/components/DocumentMeta.js
this is a new component that you can use to update your page names in a declarative style reducing the code complexity/redundancy in each view
import React, { useContext, useEffect } from 'react'
import Context, { updatePageContext } from '../Context'
export default function DocumentMeta({ title }) {
const { page, setContext } = useContext(Context)
useEffect(() => {
if (page.title !== title) {
// TODO use this todo as a marker to also update the actual document title so the browser tab name changes to reflect the current view
setContext(updatePageContext({ title }))
}
}, [title, page, setContext])
return null
}
aka usage would be something like <DocumentMeta title="Whatever Title I Want Here" />
src/pages/Home.js
each view now just needs to import DocumentMeta and the Pages "enum" to update the title, instead of pulling the context in and manually doing it each time.
import { Pages } from '../Context'
import DocumentMeta from '../components/DocumentMeta'
function Home() {
return (
<>
<DocumentMeta title={Pages.home} />
<h1>WIP</h1>
</>
)
}
Note: The other pages need to replicate what the home page is doing
Remember this isn't how I would do this in a production environment, I'd write up a more generic helper to write data to your cache that can do more things in terms of performance, deep merging.. etc. But this should be a good starting point.
Here is a working version of what you need.
import React, { useState, useContext, useEffect } from "react";
import "./styles.css";
const Context = React.createContext({});
export default function AppWrapper() {
// creating a local state
const [state, setState] = useState({ page: {} });
return (
<Context.Provider value={{ state, setState }}> {/* passing state to in provider */}
<App />
</Context.Provider>
);
}
function App() {
// getting the state from Context
const { state } = useContext(Context);
return (
<>
<h1>Title: {state.page.title}</h1>
<Home />
</>
);
}
function Home() {
// getting setter function from Context
const { setState } = useContext(Context);
useEffect(() => {
setState({ page: { title: "Home" } });
}, [setState]);
return <p>Hello, World!</p>;
}
Read more on Hooks API Reference.
You may put useContext(yourContext) at wrong place.
The right position is inner the <Context.Provider>:
// Right: context value will update
<Context.Provider>
<yourComponentNeedContext />
</Context.Provider>
// Bad: context value will NOT update
<yourComponentNeedContext />
<Context.Provider>
</Context.Provider>

How to get event from components to container in React Redux

I'm new to Redux.
I handled the basic Facebook Flux architecture very easily and made some nice app with it.
But I struggle very hard to get very simple Redux App to work.
My main concern is about containers and the way they catch events from components.
I have this very simple App :
CONTAINER
import { connect } from 'react-redux'
import {changevalue} from 'actions'
import App from 'components/App'
const mapStateToProps = (state) => {
return {
selector:state.value
}
}
const mapDispatchToProps = (dispatch) => {
return {
onClick: (e) => {
console.log(e)
dispatch(changeValue())
}
}
}
const AppContainer = connect(
mapStateToProps,
mapDispatchToProps
)(App)
export default AppContainer;
Component
import React, {Component} from 'react'
import Selector from 'components/Selector'
import Displayer from 'components/Displayer'
const App = (selector, onClick) => (
<div>
<Selector onClick={(e) => onClick}/>
<Displayer />
</div>
)
export default App;
CHILD COMPONENT
import React, {Component} from 'react'
const Selector = ({onClick}) => (
<div onClick={onClick}>click me</div>
)
export default Selector;
onClick event does not reach the container's mapDispatchToProps.
I feel that if I get this work, I get a revelation, and finally get the Redux thing! ;)
Can anybody help me get this, please ? (The Redux doc is TOTALLY NOT helpfull...)
The problem is in the App component. In the onClick property of the Selector component, you're passing a function which returns the definition of a function, not the result.
const App = (selector, onClick) => (
<div>
<Selector onClick={(e) => onClick}/> // here is the problem
<Displayer />
</div>
)
You should simply do this instead:
const App = (selector, onClick) => (
<div>
<Selector onClick={(e) => onClick(e)}/>
<Displayer />
</div>
)
Or even simpler:
const App = (selector, onClick) => (
<div>
<Selector onClick={onClick}/>
<Displayer />
</div>
)

Categories

Resources