Jest Manual Mock for ThemeProvider - reactjs

I want to test a React Component that uses Material-UI`s makeStyles.
My Component:
import React from 'react';
import { useTranslation } from 'react-i18next';
import { DefaultButton } from '../../../../components';
import { makeStyles } from '#material-ui/styles';
const useStyles = makeStyles((theme: any) => ({
root: {},
row: {
marginTop: theme.spacing()
},
spacer: {
flexGrow: 1
},
}));
const UsersToolbar: React.FC<any> = (props) => {
const classes = useStyles();
const { t } = useTranslation();
return (
<div className={classes.root}>
<div className={classes.row}>
<span className={classes.spacer} />
<DefaultButton id="createUserBtn">{t('Create User')}</DefaultButton>
</div>
</div>
);
};
export default UsersToolbar;
My test:
import React from 'react';
import ReactDOM from 'react-dom';
import { createMuiTheme } from '#material-ui/core';
import { ThemeProvider } from '#material-ui/styles';
import UsersToolbar from '.';
describe('<UsersToolbar />', () => {
it('passes smoke test', () => {
const div = document.createElement('div');
ReactDOM.render(
<UsersToolbar />,
div
);
});
});
I was thinking about using jest.mock() and place a manual mock in __mocks__/
How can I do that? I tried to provide a ThemeProvider as proposed on the official Material-UI homepage (https://material-ui.com/guides/testing/) but it did not work out.

I solved this by creating a component named MockTheme which wraps the component that needs to be tested. The result looks like the following:
import React from 'react';
import { createMuiTheme } from '#material-ui/core';
import { ThemeProvider } from '#material-ui/core/styles';
function MockTheme({ children }: any) {
const theme = createMuiTheme({});
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
export default MockTheme;
The modified test now works:
import React from 'react';
import ReactDOM from 'react-dom';
import MockTheme from '../../../../theme/MockTheme';
import UsersToolbar from '.';
describe('<UsersToolbar />', () => {
it('passes smoke test', () => {
const div = document.createElement('div');
ReactDOM.render(
<MockTheme>
<UsersToolbar />
</MockTheme>,
div
);
});
});

Related

Material UI: "styled" child component not applying css rules

I'm having an issue where I'm using the styled function to style a custom React component but the styles are not being applied. In the example below, I would expect the Child component to have the color: red style, but it doesn't. The sibling component, however, is styled correctly.
import "./styles.css";
import { Child } from "./Child";
import { Typography } from "#mui/material";
import { styled } from "#mui/material/styles";
const StyledChild = styled(Child)(() => ({
color: "red"
}));
const StyledSibling = styled(Typography)(() => ({
color: "blue"
}));
export default function App() {
return (
<>
<StyledChild />
<StyledSibling>Sibling</StyledSibling>
</>
);
}
import { Typography } from "#mui/material";
import { FunctionComponent } from "react";
export const Child: FunctionComponent = () => {
return <Typography>Child</Typography>;
};
CodeSandbox
styled causes a className prop to be passed to the wrapped component, but Child isn't passing the className prop along to Typography.
Here's an example of how to fix Child.tsx:
import { Typography } from "#mui/material";
import { FunctionComponent } from "react";
export const Child: FunctionComponent<{ className?: string }> = ({
className
}) => {
return <Typography className={className}>Child</Typography>;
};

React/TypeScript/Redux/Thunk reducer gets triggered but state is not updated in redux store

I am trying to populate my redux store state with a list of genres from an API, I can get the action to dispatch to the reducer, but the reducer does not seem to update the state because my console.log in src/components/MovieForm.tsx returns the default state of "null" instead of the array of genres and I do not know why, I am trying to see if the state is updated in src/components/MovieForm.tsx in the setInterval where I am logging the state, maybe the problem is how I am accessing the state? Here are the files:
src/actions/movieActions.ts:
import { ActionCreator, Dispatch } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { IMovieState } from '../reducers/movieReducer';
import axios from 'axios';
export enum MovieActionTypes {
ANY = 'ANY',
GENRE = 'GENRE',
}
export interface IMovieGenreAction {
type: MovieActionTypes.GENRE;
genres: any;
}
export type MovieActions = IMovieGenreAction;
/*<Promise<Return Type>, State Interface, Type of Param, Type of Action> */
export const movieAction: ActionCreator<ThunkAction<Promise<any>, IMovieState, void, IMovieGenreAction>> = () => {
return async (dispatch: Dispatch) => {
try {
console.log('movieActions called')
let res = await axios.get(`https://api.themoviedb.org/3/genre/movie/list?api_key=${process.env.REACT_APP_MOVIE_API_KEY}&language=en-US`)
console.log(res.data.genres)
dispatch({
genres: res.data.genres,
type: MovieActionTypes.GENRE
})
} catch (err) {
console.log(err);
}
}
};
src/reducers/movieReducer.ts:
import { Reducer } from 'redux';
import { MovieActionTypes, MovieActions } from '../actions/movieActions';
export interface IMovieState {
//property: any;
genres: any;
}
const initialMovieState: IMovieState = {
//property: null,
genres: null,
};
export const movieReducer: Reducer<IMovieState, MovieActions> = (
state = initialMovieState,
action
) => {
switch (action.type) {
case MovieActionTypes.GENRE: {
console.log('MovieActionTypes.GENRE called')
return {
genres: action.genres,
};
}
default:
console.log('Default action called')
return state;
}
};
src/store/store.ts:
import { applyMiddleware, combineReducers, createStore, Store } from 'redux';
import thunk from 'redux-thunk';
import { IMovieState, movieReducer } from '../reducers/movieReducer';
// Create an interface for the application state
export interface IAppState {
movieState: IMovieState
}
// Create the root reducer
const rootReducer = combineReducers<IAppState>({
movieState: movieReducer
});
// Create a configure store function of type `IAppState`
export default function configureStore(): Store<IAppState, any> {
const store = createStore(rootReducer, undefined, applyMiddleware(thunk));
return store;
}
src/components/MovieForm.tsx (the file that is supposed to dispatch the action):
import React, { useState } from 'react';
import { makeStyles } from '#material-ui/core/styles';
import Paper from '#material-ui/core/Paper';
import Box from '#material-ui/core/Box';
import Select from '#material-ui/core/Select';
import MenuItem from '#material-ui/core/MenuItem';
import { spacing } from '#material-ui/system';
import Card from '#material-ui/core/Card';
import Button from '#material-ui/core/Button';
import Typography from '#material-ui/core/Typography';
import { CardHeader, TextField, CircularProgress } from '#material-ui/core';
import { useDispatch, useSelector } from 'react-redux';
import { movieAction } from '../actions/movieActions';
import { IAppState } from '../store/store';
import axios from 'axios';
const MovieForm = () => {
const dispatch = useDispatch()
const getGenres = () => {
console.log('actions dispatched')
dispatch(movieAction())
}
const genres = useSelector((state: IAppState) => state.movieState.genres);
//const [genreChoice, setGenreChoice] = useState('')
return (
<>
<h1>Movie Suggester</h1>
<Paper elevation={3}>
<Box p={10}>
<Card>
<div>Hello World. </div>
<Select onChange={() => console.log(genres)}>
<MenuItem>
hello
</MenuItem>
<br />
<br />
</Select>
<Button onClick={() => {
getGenres()
setTimeout(function(){
console.log(genres)
}, 5000)
}}>genres list</Button>
<Button onClick={() => console.log(axios.get(`https://api.themoviedb.org/3/discover/movie?api_key=${process.env.REACT_APP_MOVIE_API_KEY}&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&with_genres=35&page=1`))}>Click me</Button>
</Card>
</Box>
</Paper>
</>
)
}
export default MovieForm
and here is the src/index.tsx in case the problem is here and I'm unaware:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import { BrowserRouter as Router } from 'react-router-dom';
import './index.css';
import CssBaseline from '#material-ui/core/CssBaseline';
import { ThemeProvider } from '#material-ui/core/styles';
import theme from './theme';
import App from './App';
import configureStore from './store/store';
const store = configureStore();
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<Router>
<App />
</Router>
</ThemeProvider>
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
Thanks for taking a look at this and and attempting to help me see what I am unable to!

material ui styling not applying

For some reason my material ui styles are not applying to my html element? Any idea why? I have no other styles applied to this page
import React, { Component } from 'react';
import LoginForm from '../components/form/loginForm';
import { makeStyles } from '#material-ui/core';
const classes = makeStyles( (theme) => ({
root: {
paddingTop: theme.spacing(8),
backgroundColor: "white"
},
}) )
class Login extends Component {
render() {
return(
<div className = {classes.root}>
<LoginForm/>
</div>
);
}
}
export default Login;
In your case, if you want to style your class component, you should use withStyles. Try this:
import React, { Component } from 'react';
import LoginForm from '../components/form/loginForm';
import { withStyles } from '#material-ui/core/styles';
const useStyles = (theme) => ({
root: {
paddingTop: theme.spacing(8),
backgroundColor: "white"
},
})
class Login extends Component {
render() {
const { classes } = this.props
return(
<div className = {classes.root}>
<LoginForm/>
</div>
);
}
}
export default withStyles(useStyles)(Login);
makeStyles returns a react hook to use in the component. Hooks also only work in functional components, so you'll need to convert Login to a functional component.
import React, { Component } from 'react';
import LoginForm from '../components/form/loginForm';
import { makeStyles } from '#material-ui/core';
const useStyles = makeStyles(theme => ({
root: {
paddingTop: theme.spacing(8),
backgroundColor: "lightblue"
}
}));
const Login = props => {
const classes = useStyles();
return(
<div className={classes.root}>
<LoginForm/>
</div>
);
}
export default Login;

Material UI: Dark theme not being applied

I'm using the Context API to store the theme value. The theme itself is created with <createMuiTheme> and passed down from <Layout> to children via <MuiThemeProvider> and <CssBaseline>. I can see the state change via React DevTools but the theme itself is not being applied - and I'm at loss as to why...
Here is codesandbox with a full example - warning: contains Samuel L. Ipsum. What follows is an abridged version.
Default and dark theme definitions:
// theme/dark.js
import { createMuiTheme } from "#material-ui/core/styles";
const theme = createMuiTheme({
typography: {
useNextVariants: true
},
palette: {
type: "dark"
}
});
export default theme;
// theme/default.js
import { createMuiTheme } from "#material-ui/core/styles";
const theme = createMuiTheme({
typography: {
useNextVariants: true
},
palette: {
type: "light"
}
});
export default theme;
Context:
// context/settings/SettingsContext.js
import React from "react";
export default React.createContext({
darkMode: false
});
// context/settings/SettingsProvider.js
import React, { useState } from "react";
import SettingsContext from "./SettingsContext";
const storage = {
getItem(key) {
if (localStorage) {
return localStorage.getItem(key);
}
},
setItem(key, value) {
if (localStorage) {
return localStorage.setItem(key, value);
}
}
};
const SettingsProvider = props => {
const [darkMode, setDarkMode] = useState(
storage.getItem("darkMode") === "true"
);
const onSetDarkMode = darkMode => {
setDarkMode(darkMode);
storage.setItem("darkMode", darkMode);
};
return (
<SettingsContext.Provider
value={{
darkMode,
onSetDarkMode
}}
>
{props.children}
</SettingsContext.Provider>
);
};
export default SettingsProvider;
index.js:
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import App from "./containers/app";
import SettingsProvider from "./context/settings/SettingsProvider";
ReactDOM.render(
<BrowserRouter>
<SettingsProvider>
<App />
</SettingsProvider>
</BrowserRouter>,
document.getElementById("root")
app/index.js:
import React, { useState } from "react";
import { Switch, Route } from "react-router-dom";
import { default as home } from "../home/routes";
import Layout from "../layout";
const App = () => {
const [anchorEl, setAnchorEl] = useState(null);
return (
<div>
<Layout anchorEl={anchorEl} setAnchorEl={setAnchorEl}>
<Switch>
{home.map((route, index) => (
<Route
key={index}
path={route.path}
exact={route.exact}
render={route.render}
/>
))}
</Switch>
</Layout>
</div>
);
};
export default App;
And layout/index.js:
import React, { useContext } from "react";
import { MuiThemeProvider } from "#material-ui/core/styles";
import { makeStyles } from "#material-ui/core/styles";
import CssBaseline from "#material-ui/core/CssBaseline";
import defaultTheme from "../../themes/default";
import darkTheme from "../../themes/default";
import SettingsContext from "../../context/settings/SettingsContext";
import Header from "../../components/header/index";
const useStyles = makeStyles(theme => ({
toolbarMargin: {
...theme.mixins.toolbar
}
}));
const Layout = props => {
const classes = useStyles();
const context = useContext(SettingsContext);
const theme = context.darkMode ? darkTheme : defaultTheme;
const { children, anchorEl, setAnchorEl } = props;
return (
<MuiThemeProvider theme={theme}>
<CssBaseline />
<Header anchorEl={anchorEl} setAnchorEl={setAnchorEl} />
<main>
<div className={classes.toolbarMargin} />
{children}
</main>
</MuiThemeProvider>
);
};
export default Layout;
What did I miss?
You're importing the same theme twice. I'd suggest using named exports instead of defaults, it helps a lot with auto importing, as well as spotting mistakes like this.
// layout/index.js
import defaultTheme from "../../themes/default";
import darkTheme from "../../themes/default"; // should be "../../theme/dark"

How do I style a material-ui Icon which was passed as prop

I'm writing a custom Material UI React component which I want to pass an Icon into as a prop. However I want to style the icon when I get it and make it a minimum width and height.
Here's a simplified version of what I'm trying to do. I want to apply the iconStyle to the icon passed in as props.statusImage but can't figure out how.
import PropTypes from "prop-types";
import { makeStyles } from "#material-ui/styles";
const useStyles = makeStyles({
iconStyle: {
minWidth: 100,
minHeight: 100
}
});
function MyComponentWithIconProps(props) {
const styles = useStyles();
return <div>{props.statusImage}</div>;
}
MyComponentWithIconProps.propTypes = {
statusImage: PropTypes.element
};
export default MyComponentWithIconProps;
I use the component like this
import {Done} from "#material-ui/icons";
<MyComponentWithIconProps statusImage={<Done/>}
Code Sandbox : https://codesandbox.io/s/jovial-fermi-dmb0p
I've also tried wrapping the supplied Icon in another Icon element and attempting to style that. However that didn't work and seems sort of 'hacky' anyway.
There are three main alternatives:
Pass in the element type of the icon rather than an element (e.g. Done instead of <Done/>) and then add the className as you render the element (this is the approach in Fraction's answer).
Clone the element in order to add the className prop to it.
Put a class on the parent element and target the appropriate child type (e.g. svg).
Approach 1:
index.js
import React from "react";
import ReactDOM from "react-dom";
import { Done } from "#material-ui/icons";
import MyComponentWithIconProps from "./MyComponentWithIconProps";
function App() {
return (
<div className="App">
<MyComponentWithIconProps statusImage={Done} />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
MyComponentWithIconProps.js
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "#material-ui/styles";
const useStyles = makeStyles({
iconStyle: {
minWidth: 100,
minHeight: 100
}
});
function MyComponentWithIconProps(props) {
const styles = useStyles();
const StatusImage = props.statusImage;
return (
<div>
<StatusImage className={styles.iconStyle} />
</div>
);
}
MyComponentWithIconProps.propTypes = {
statusImage: PropTypes.element
};
export default MyComponentWithIconProps;
Approach 2:
index.js
import React from "react";
import ReactDOM from "react-dom";
import { Done } from "#material-ui/icons";
import MyComponentWithIconProps from "./MyComponentWithIconProps";
function App() {
return (
<div className="App">
<MyComponentWithIconProps statusImage={<Done />} />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
MyComponentWithIconProps.js
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "#material-ui/styles";
import clsx from "clsx";
const useStyles = makeStyles({
iconStyle: {
minWidth: 100,
minHeight: 100
}
});
function MyComponentWithIconProps(props) {
const styles = useStyles();
const styledImage = React.cloneElement(props.statusImage, {
// Using clsx to combine the new class name with any existing ones that may already be on the element
className: clsx(styles.iconStyle, props.statusImage.className)
});
return <div>{styledImage}</div>;
}
MyComponentWithIconProps.propTypes = {
statusImage: PropTypes.element
};
export default MyComponentWithIconProps;
Approach 3:
index.js
import React from "react";
import ReactDOM from "react-dom";
import { Done } from "#material-ui/icons";
import MyComponentWithIconProps from "./MyComponentWithIconProps";
function App() {
return (
<div className="App">
<MyComponentWithIconProps statusImage={<Done />} />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
MyComponentWithIconProps.js
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "#material-ui/styles";
const useStyles = makeStyles({
iconStyle: {
"& > svg": {
minWidth: 100,
minHeight: 100
}
}
});
function MyComponentWithIconProps(props) {
const styles = useStyles();
return <div className={styles.iconStyle}>{props.statusImage}</div>;
}
MyComponentWithIconProps.propTypes = {
statusImage: PropTypes.element
};
export default MyComponentWithIconProps;
Pass the icon like this:
<MyComponentWithIconProps statusImage={Done} />
then use it as follows:
return <div><props.statusImage className={styles.iconStyle} /></div>;
I would do like this:
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "#material-ui/styles";
const useStyles = makeStyles({
iconStyle: {
minWidth: 100,
minHeight: 100,
color: "red"
}
});
function MyComponentWithIconProps(props) {
const styles = useStyles();
return <div className={styles.iconStyle}>{props.statusImage}</div>;
}
MyComponentWithIconProps.propTypes = {
statusImage: PropTypes.element
};
export default MyComponentWithIconProps;
In my case this is work :
Index.js
import {Done} from "#material-ui/icons";
<MyComponentWithIconProps icon={<Done {/*with some props*/}/>}/>
MyComponentWithIconProps.js
return (<div >{props.icon}</div>);
CodeSanbox

Resources