How to test react component with hooks using react testing library - reactjs

I am trying to test a component which use useTheme hook provided by emotion.js. The theme is set during the app initialization.
Now when I write my test cases, the useTheme hook is unable to fetch the styles data as only header component is getting mounted for testing. How to mock the data provided by hooks.
app.js
import { theme } from '../src/utils/styles/themes/default';
const AppRoot = ({ path, Router }) => {
const routeRenderFunction = (props) => <RouteHandler route={props} />;
return (
<ThemeProvider theme={theme}>
<Router location={path} context={{}}>
<Switch>
{routePatterns.map((routePattern) => (
<Route key={routePattern} path={routePattern} render={routeRenderFunction} />
))}
</Switch>
</Router>
</ThemeProvider>
);
};
header.js
import React from "react";
import { css } from "emotion";
import { useTheme } from "emotion-theming";
import * as styles from "./Header.style";
const Header = ({userName = 'Becky Parsons', clinicName = 'The University of Southampton'}) => {
const theme = useTheme();
return (
<div className={css(styles.accountDetails(theme))}>
<div className={css(styles.accountContainer)}>
<div className={css(styles.accountSpecifics)}>
<h5 className={css(styles.accountDetailsH5(theme))} data-testid="user-name">{userName}</h5>
<h6 className={css(styles.accountDetailsH6(theme))} data-testid="clinic-name">
{clinicName}
</h6>
</div>
<div className={css(styles.avatar)} />
</div>
</div>
);
};
export default Header;
header.test.js
import React from 'react'
import {render} from '#testing-library/react';
import Header from './Header';
test('Check if header component loads', () => {
const { getByTestId } = render(<Header userName='Becky Parsons' clinicName='The University of Southampton'/>);
expect(getByTestId('user-name').textContent).toBe('Becky Parsons');
expect(getByTestId('clinic-name').textContent).toBe('The University of Southampton');
})
header.style.js
export const accountDetails = theme => ({
height: '70px',
backgroundColor: theme.header.backgroundColor,
textAlign: 'right',
padding: '10px 40px'
});
export const accountContainer = {
display: 'flex',
justifyContent: 'flex-end'
};
Error: Uncaught [TypeError: Cannot read property 'backgroundColor' of undefined]

You should wrap your component with ThemeProvider, try this;
test('Check if header component loads', () => {
const { getByText } = render(
<ThemeProvider theme={your theme object...}>
<Header userName='Becky Parsons' clinicName='The University of Southampton'/>
</ThemeProvider >
);
...
});
and you can look here: https://github.com/emotion-js/emotion/blob/master/packages/emotion-theming/tests/use-theme.js

Related

React error- Uncaught TypeError: Cannot destructure property 'children' of '_ref' as it is undefined

I have used useEffect to fetch API & dispatch an action from inside it to update state. I am using context API, useReducer for state management and also react-router in this project.
In Header.js as soon as i import/use CartState and state variable (e.g. cart.length) then the app breaks down and items from api are not rendered and i get this error. In home.js also i am using state, but with only that part it does not give any error.
Below i am attaching two images, one with error and one where it is rendered when i remove CartState from header.js
Or you can see the codesandbox : https://codesandbox.io/s/serene-fast-297j3h?from-embed=&file=/
Website picture when CartState removed from header.js
Error message - website picture
Please tell me what is the issue here and what should be done.
index.js file:
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import "bootstrap/dist/css/bootstrap.min.css";
import { BrowserRouter } from "react-router-dom";
import GlobalContext from './context/GlobalContext'
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<BrowserRouter>
<GlobalContext>
<App />
</GlobalContext>
</BrowserRouter>
</React.StrictMode>
);
App.js:
import "./App.css";
import Header from "./Components/Header";
import { Routes, Route } from "react-router-dom";
import Home from './Components/Home/Home'
import Cart from './Components/Cart'
function App() {
return (
<>
<Header />
<Routes>
<Route path="/" exact element={<Home/>}></Route>
<Route path="/cart" exact element={<Cart/>}></Route>
</Routes>
</>
);
}
export default App;
GlobalContext.js:
const CartContext = createContext();
const GlobalContext = ({children}) => {
const initialState ={
products: [],
cart: []
};
useEffect( ()=>{
const fetchData = async ()=> {
const res = await axios('https://fakestoreapi.com/products');
const resAdd = res.data.map((item)=> ({...item, inStock: faker.helpers.arrayElement([0,2,5,9,20]), fastDelivery: faker.datatype.boolean() }));
dispatch({type: 'API_CALL_SUCCESS', payload: resAdd});
// console.log(initialState);
}
fetchData();
}, [] )
const [state, dispatch] = useReducer(cartReducer, initialState );
console.log(state);
return (
<CartContext.Provider value={{state, dispatch}}>
{children}
</CartContext.Provider>
);
};
export default GlobalContext;
export const CartState = () => {
return useContext(CartContext);
};
Header.js (part of the code)
import CartState from "../context/GlobalContext";
const Header = () => {
const {
state: { cart },
dispatch,
} = CartState();
return (
<Navbar bg="dark" variant="dark" style={{ height: 100 }}>
<Container>
<Navbar.Brand>
<Link to="/">HOME</Link>
</Navbar.Brand>
<FormControl
className="m-auto"
style={{ width: 500 }}
type="text"
placeholder="Search here the product you want"
/>
<Nav>
<Dropdown
// alignRight
>
<Dropdown.Toggle variant="success">
<FaShoppingCart color="white" fontSize="25px" />
<Badge>
{cart.length}
{/* 1 */}
</Badge>
</Dropdown.Toggle>
Home.js:
import { CartState } from '../../context/GlobalContext'
import SingleProduct from '../SingleProduct';
import './styles.css'
import Filters from '../Filter/Filters';
const Home = () => {
const {state: {products}, } = CartState();
console.log(products);
return (
<div className="home">
<Filters />
<div className="productContainer">
{products.map( (item)=>
<SingleProduct prod = {item} key = {item.id} />
)}
</div>
</div>
)
}
export default Home
You need to modify below code line in Header.js file.
Actual:
import CartState from "../context/GlobalContext";
Expected/correct:
import {CartState} from "../context/GlobalContext";
Reason: CartState is not a default export.
In Header.js:
import CartState from '../context/GlobalContext';
this part of your code is wrong because you didn't export default your CartState function so you should import it like:
import {CartState} from "../context/GlobalContext";

Problem Upgrading React application - material UI v4 to v5

I have this problem upgrading my react application. Basically, I followed the damn mui-v5 documentation, and I still have some erros in my app.tsx file.
import { useReactOidc, withOidcSecure } from '#axa-fr/react-oidc-context';
import { Container } from '#mui/material';
import CssBaseline from '#mui/material/CssBaseline';
import {
ThemeProvider,
StyledEngineProvider,
Theme,
} from '#mui/material/styles';
import createStyles from '#mui/styles/createStyles';
import makeStyles from '#mui/styles/makeStyles';
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { Redirect, Route, Switch } from 'react-router-dom';
import { RootState } from 'src/lib/configuration/store-configuration';
import { DarkTheme, LightTheme } from 'src/components/material-ui-theme';
import routes from 'src/components/routes/routes';
import BreadCrumbs from 'src/components/shared/breadcrumbs/breadcrumbs';
import ErrorBoundary from 'src/components/shared/error/Error';
import Footer from 'src/components/shared/footer/footer';
import Header from 'src/components/shared/header/header';
import Notifier from 'src/components/shared/notifications';
import ScrollTop from 'src/components/shared/scroll-top/scroll-top';
import Sider from 'src/components/shared/sider';
import { useTranslation } from 'react-i18next';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
display: 'flex',
},
toolbar: {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
},
container: {
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(1),
},
}),
);
const App = () => {
const classes = useStyles();
const { oidcUser } = useReactOidc();
const appStore = useSelector((store: RootState) => store.app);
const [drawerOpen, setDrawerOpen] = useState(false);
const handleDrawerToggle = () => {
setDrawerOpen(!drawerOpen);
};
const { i18n } = useTranslation();
useEffect(() => {
document.documentElement.lang = i18n.language;
}, [i18n.language]);
return (
<StyledEngineProvider injectFirst>
<ThemeProvider
theme={
appStore.theme === 'dark' ? DarkTheme : LightTheme
}
>
<div className={classes.root}>
<CssBaseline />
<Header handleDrawerToggle={handleDrawerToggle} />
{oidcUser ? (
<Sider
drawerOpen={drawerOpen}
handleDrawerToggle={
handleDrawerToggle
}
/>
) : null}
<main className={classes.content}>
<div id="back-to-top-anchor" />
<div className={classes.toolbar} />
<Container
maxWidth="xl"
className={classes.container}
>
<Notifier />
<BreadCrumbs />
<ErrorBoundary>
<Switch>
{routes.map(
(route, i) => (
<Route
key={
'route_' +
i
}
exact={
route.exact
}
path={
route.path
}
component={
route.secure
? withOidcSecure(
route.component,
)
: route.component
}
/>
),
)}
<Redirect to="/" />
</Switch>
</ErrorBoundary>
</Container>
<ScrollTop />
<Footer />
</main>
</div>
</ThemeProvider>
</StyledEngineProvider>
);
};
export default App;
Here is the navigator console error :
I tried to modify some imports and used:
import { createTheme } from '#material-ui/core';
const theme = createTheme();
And passing it to makestyles((theme) => ... but nothing changed.
Please guys I need your support ! :)
link for the open-source github project: https://github.com/InseeFr/sugoi-ui

Next.js not able to pre-render error page

I recently started to use Next.JS on a new project and it's working fine but I can't figure out how to keep my layout state active on client side when I throw a 404 error after clicking on a wrong link.
Thanks to Adam's Wathan article, I'm able to share my state between different pages :
On home page
On "about" page
But if i click on "error", it will render the _error.tsx without preserving data i wrote in the input.
This page seems to render the whole app tree on server side, despite the fact I provide the same layout. Is there anyway to prefetch this page, just like a regular one and avoid to lose some information without using a solution like Redux ? I'm note quite familiar with it and it seems a bit too much in this case.
Here is my code:
pages/_error.tsx
import { getLayout } from "../components/layout/mainLayout"
import { withTranslation } from "../i18n";
import { FlexDirectionProperty } from "csstype";
const imgStyle = {
maxWidth: "100%",
maxHeight: "100%"
};
const figureStyle = {
height: "80vh",
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column" as FlexDirectionProperty
};
const CustomError: any = ({ status, t }: any) => (
<>
<figure style={figureStyle}>
<figcaption>
<h1>Statut:{t('WELCOME')}</h1>
</figcaption>
<img
alt="Showing a properly cat according the status code"
src={`https://http.cat/${status}`}
style={imgStyle}
/>
</figure>
</>
)
CustomError.getLayout = getLayout;
CustomError.getInitialProps = async ({ res, err }: any) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : null
return {
statusCode: statusCode,
namespacesRequired: ["common"]
}
};
export default withTranslation('common')(CustomError)
components/layout/header.tsx
import Link from "next/link";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import { withTranslation, i18n } from "../../i18n";
import { capitalize } from "../../helpers/utils";
import Modal from "react-bootstrap/Modal";
import { useState } from "react";
const loadStamp = +new Date();
const Header: any = ({ t }: any) => {
const [show, setShow] = useState(false);
const [active, setActive] = useState("home");
return (
<>
<Navbar fixed="top" bg="light">
<Nav className="mr-auto" activeKey={active}>
<Nav.Item>
<Link href="/">
<Navbar.Brand onClick={() => setActive("home")} href="/">Random Project</Navbar.Brand>
</Link>
</Nav.Item>
...
</Nav>
</Navbar>
</>);
};
export default withTranslation("common")(Header);
components/layout/mainLayout.tsx
import Header from "./header";
import "bootstrap/dist/css/bootstrap.min.css";
import "../../public/stylesheets/style.scss";
type LayoutProps = {
title?: string;
children: any;
};
const Layout: React.FunctionComponent<LayoutProps> = ({ children }) => (
<>
<Header />
<main role="main" className="main">
{children}
</main>
</>
);
export const getLayout: any = (page: any) => <Layout>{page}</Layout>
export default Layout
And my _app.tsx
import React from 'react'
import App from 'next/app'
import { appWithTranslation } from '../i18n'
class MyApp extends App<any> {
render() {
const { Component, pageProps } = this.props
const getLayout = Component.getLayout || ((page: any) => page)
return (
<>
{getLayout(
<Component {...pageProps}></Component>
)}
</>
)
}
}
export default appWithTranslation(MyApp)

React's Context API Cosumer is not rendering Children

I'm implementing a Theme Switcher using the React Context API and this tutorial
https://www.youtube.com/watch?time_continue=1&v=oDgxUodLwGU
I've started by making the Context
export default React.createContext({
theme: dark
});
Then I have defined my themes (dark & light)
Then I created a Component (TodoList)
import React from 'react';
import styled from 'styled-components';
const List = styled.ul`
background: ${props => {
debugger;
props.theme.background;
}};
color: ${props => props.theme.color};
font-family: sans-serif;
font-size: 20px;
li {
line-height: 36px;
}
`;
const TodoList = () => {
debugger;
return (
<List>
<li>item1</li>
<li>item2</li>
<li>item3</li>
</List>
);
};
export default TodoList;
Then in my App Component I used the code below
import React, {
Component
} from 'react';
import './App.css';
import TodoList from './components/TodoList';
import ThemeSwitcher from './components/ThemeSwitcher';
import * as themes from './styles/themes';
import ThemeContext from './styles/themes/context';
import { ThemeProvider } from 'styled-components';
class App extends Component {
state = {
theme: themes.dark,
};
toggleTheme = () => {
this.setState({ theme: this.state.theme === themes.dark ? themes.light : themes.dark });
};
render() {
console.log(this.state.theme);
return (
<div id="root">
<ThemeContext.Provider value={this.state}>
<ThemeSwitcher toggleTheme={this.toggleTheme} />
<ThemeContext.Consumer>
{theme => {
<ThemeProvider theme={theme}>
<TodoList />
</ThemeProvider>
}}
</ThemeContext.Consumer>
</ThemeContext.Provider>
</div>
);
}
}
export default App;
When I use TodoList component outside ThemeContext.Consumer, it renders just fine but inside it never renders. The breakpoints in the TodoList component never get hits...
Also I've tried disregarding the ThemeContext.Consumer and using the snippet below:
<ThemeProvider theme={theme}>
<TodoList />
</ThemeProvider>
the component rendered.
What possibly could I have done wrong?
Thanks in advance.
I was able to identify the problem, It was with the snippet below:
<ThemeContext.Consumer>
{theme => {
<ThemeProvider theme={theme}>
<TodoList />
</ThemeProvider>
}}
</ThemeContext.Consumer>
It should be
<ThemeContext.Consumer>
{theme => {
return (
<ThemeProvider theme={theme}>
<TodoList />
</ThemeProvider>
);
}}
</ThemeContext.Consumer>

Tests with MaterialUI failing because of styles

I'm trying to run some simple unit tests on React with Material UI but I'm getting an error that I don't know the cause of, I've ran tests with MaterialUI on other projects with no problems.
This is the code of the component I'm trying to test:
import React, { Component, Fragment } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import PropTypes from 'prop-types';
import ReactRouterPropTypes from 'react-router-prop-types';
import { Icon } from 'react-fa';
import { withStyles } from 'material-ui/styles';
import AppBar from 'material-ui/AppBar';
import Toolbar from 'material-ui/Toolbar';
import MaterialIcon from 'material-ui/Icon';
import IconButton from 'material-ui/IconButton';
import Modal from 'material-ui/Modal';
import Hidden from 'material-ui/Hidden';
import SimpleButton from '../../common/SimpleButton';
import OutlineButton from '../../common/OutlineButton';
import * as sessionActions from '../../../actions/sessionActions';
import { APP_TITLE, LOGOUT, LOGIN, REGISTER } from '../../../constants/strings';
import { CHECKOUT } from '../../../constants/routes';
import styles from './styles';
import CartIcon from '../../common/Icons/Cart.svg';
import logo from '../../../images/logo.png';
import logoXs from '../../../images/logoXS.png';
import Login from '../Login';
import Register from '../Register';
import Wrapper from '../../common/Wrapper';
class Header extends Component {
constructor(props) {
super(props);
this.state = {};
}
handleLogoutClick = () => {
this.props.actions.logout();
};
handleOpenModal = (s) => {
this.setState({ openModal: s });
};
handleCloseModal = () => {
this.setState({ openModal: null });
};
handleCartIcon = () => {
this.props.history.push(CHECKOUT);
};
renderAuthenticated = () => (
<SimpleButton onClick={this.handleLogoutClick} className={this.props.classes.button}>
{LOGOUT}
</SimpleButton>
);
renderNotAuthenticated = () => {
const { classes, history } = this.props;
return (
<Fragment>
<SimpleButton onClick={() => this.handleOpenModal(LOGIN)} className={classes.button} id="btn-login-modal">
<Hidden smUp>
<Icon className={classes.icons} name="user-circle" alt="login" size="2x" />
</Hidden>
<Hidden xsDown>{LOGIN}</Hidden>
</SimpleButton>
<Modal open={this.state.openModal === LOGIN} onClose={this.handleCloseModal}>
<Login
history={history}
handleCloseModal={this.handleCloseModal}
handleChangeModal={this.handleOpenModal}
/>
</Modal>
<Hidden xsDown>
<OutlineButton
onClick={() => this.handleOpenModal(REGISTER)}
color="primary"
className={classes.button}
>
{REGISTER}
</OutlineButton>
<Modal open={this.state.openModal === REGISTER} onClose={this.handleCloseModal}>
<Register
history={history}
handleCloseModal={this.handleCloseModal}
handleChangeModal={this.handleOpenModal}
/>
</Modal>
</Hidden>
</Fragment>
);
};
render() {
const { authenticated, classes } = this.props;
return (
<AppBar position="static" color="default" className={classes.appBar}>
<Wrapper>
<Toolbar className={classes.toolBar}>
<div className={classes.flex}>
<Hidden smUp>
<img src={logoXs} alt={APP_TITLE} />
</Hidden>
<Hidden xsDown>
<img src={logo} alt={APP_TITLE} />
</Hidden>
</div>
{authenticated ? this.renderAuthenticated() : this.renderNotAuthenticated()}
<IconButton className={classes.button} onClick={() => this.handleCartIcon()}>
<MaterialIcon>
<img src={CartIcon} alt="cart" />
</MaterialIcon>
</IconButton>
</Toolbar>
</Wrapper>
</AppBar>
);
}
}
const { objectOf, any, bool } = PropTypes;
Header.propTypes = {
/* eslint-disable react/no-typos */
classes: objectOf(any).isRequired,
actions: objectOf(any).isRequired,
authenticated: bool.isRequired,
history: ReactRouterPropTypes.history.isRequired,
};
const mapDispatch = dispatch => ({
actions: bindActionCreators(sessionActions, dispatch),
});
const mapStateToProps = ({ session }) => ({
authenticated: session.authenticated,
checked: session.checked,
});
export default connect(mapStateToProps, mapDispatch)(withStyles(styles)(Header));
This is the test file:
import React from 'react';
import { shallow } from 'enzyme';
import Header from './';
import configureStore from '../../../store/configureStore';
const store = configureStore();
const header = shallow(<Header store={store} />);
describe('<Header />', () => {
it('should open modal when clicking `SimpleButton`', () => {
console.log(header.dive().debug());
});
});
And in when I run the tests I get this:
FAIL src/components/containers/Header/Header.test.js ● <Header /> › should open modal when clicking `SimpleButton`
TypeError: Cannot read property '200' of undefined
6 | padding: '10px 5px',
7 | boxShadow: `-webkit-box-shadow: 0px 4px 10px 0px ${
> 8 | theme.palette.gray[200]
9 | }; -moz-box-shadow: 0px 4px 10px 0px ${theme.palette.gray[200]}; box-shadow: 0px 4px 10px 0px ${
10 | theme.palette.gray[200]
11 | }`,
at styles (src/components/containers/Header/styles.js:8:7)
at Object.create (node_modules/material-ui/styles/getStylesCreator.js:31:35)
at WithStyles.attach (node_modules/material-ui/styles/withStyles.js:275:45)
at WithStyles.componentWillMount (node_modules/material-ui/styles/withStyles.js:205:16)
at ReactShallowRenderer._mountClassComponent (node_modules/enzyme-adapter-react-16/node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:137:22)
at ReactShallowRenderer.render (node_modules/enzyme-adapter-react-16/node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:102:14)
at node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:287:35
at withSetStateAllowed (node_modules/enzyme-adapter-utils/build/Utils.js:94:16)
at Object.render (node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:286:68)
at new ShallowWrapper (node_modules/enzyme/build/ShallowWrapper.js:119:22)
at ShallowWrapper.wrap (node_modules/enzyme/build/ShallowWrapper.js:1648:16)
at ShallowWrapper.<anonymous> (node_modules/enzyme/build/ShallowWrapper.js:1718:26)
at ShallowWrapper.single (node_modules/enzyme/build/ShallowWrapper.js:1620:25)
at ShallowWrapper.dive (node_modules/enzyme/build/ShallowWrapper.js:1710:21)
at Object.<anonymous> (src/components/containers/Header/Header.test.js:12:24)
I've tried using mount, wrapping the component in MuiThemeProvider, using the helper createShallow or createMount from MaterialUI.
Nothing works.
AppBar requires a muiTheme context, which you can provide to .dive() (see documentation)
Here is my test for a Header component rendering an AppBar material-ui component.
import React from "react";
import {shallow} from "enzyme";
import Header from "../";
describe("Header", () => {
it("should match the snapshot", () => {
const wrapper = shallow(<Header />);
// dive in AppBar
const wrappedComponents = wrapper.dive({
context: {
muiTheme: {
appBar: "",
zIndex: 0,
prepareStyles: () => {},
button: {
iconButtonSize: 0,
},
},
},
});
expect(wrappedComponents).toMatchSnapshot();
});
});

Resources