I am trying to mock the implementation of useNavigate with jest to test a component using react testing library, but when i run my test jest don’t recognize the mocked function.
here is the component.
import * as React from "react";
import AppBar from "#mui/material/AppBar";
import Toolbar from "#mui/material/Toolbar";
import Typography from "#mui/material/Typography";
import { useNavigate } from "react-router-dom";
import { Button } from "#mui/material";
import Somelogo from "../../assets/some-logo.svg";
export default function NavBar() {
const navigate = useNavigate();
return (
<AppBar color="primary" position="sticky">
<Toolbar>
<img
width="100%"
style={{ maxWidth: "50px" }}
height="auto"
src={Somelogo}
alt="some logo"
/>
<Typography
variant="h6"
component="div"
sx={{ flexGrow: 1, textAlign: "center" }}
>
E-core Breakdown
</Typography>
<Button color="info" onClick={() => navigate("/")}>
Home
</Button>
<Button color="info" onClick={() => navigate("/accounts")}>
Accounts
</Button>
</Toolbar>
</AppBar>
);
}
here is my test
import userEvent from "#testing-library/user-event";
import { render, screen } from "../../utils/test.utils";
import Navbar from "../navbar";
const mockedUsedNavigate = jest.fn();
jest.mock("react-router", () => ({
...(jest.requireActual("react-router") as any),
useNavigate: () => mockedUsedNavigate,
}));
describe("<Navbar />", () => {
beforeEach(() => {
mockedUsedNavigate.mockReset();
});
test("should navigate to the correct page", () => {
render(<Navbar />);
const accountButton = screen.getByText("Accounts", { exact: false });
userEvent.click(accountButton);
expect(mockedUsedNavigate).toHaveBeenCalledTimes(1);
});
});
When i run this test it returns to me that the test failed. I read some documentations and saw examples using the exact same mock but to me it doesn’t work.
failed test
I find what i was doing wrong, i needed to wait for the expected result to happen, adding a waitfor to the test.
import userEvent from "#testing-library/user-event";
import { render, screen } from "../../utils/test.utils";
import Navbar from "../navbar";
const mockedUsedNavigate = jest.fn();
jest.mock("react-router", () => ({
...(jest.requireActual("react-router") as any),
useNavigate: () => mockedUsedNavigate,
}));
describe("<Navbar />", () => {
beforeEach(() => {
mockedUsedNavigate.mockReset();
});
test("should navigate to the correct page", () => {
render(<Navbar />);
const accountButton = screen.getByText("Accounts", { exact: false });
userEvent.click(accountButton);
await waitFor(() => expect(mockedUsedNavigate).toHaveBeenCalledTimes(1));
});
});
Related
Ive downloaded a DarkModeToggle npm for my react app however I am a bit confused as to actually add the functionallity. Currently the button lets me click it on the appbar and the button itself changes however the state of my app does not.
import React, { useState, useEffect } from "react";
import { Container, AppBar, Typography, Grow, Grid } from "#material-ui/core";
import { useDispatch } from "react-redux";
import DarkModeToggle from "react-dark-mode-toggle";
// import { getPosts } from './actions/posts'
import Posts from "./components/Posts/Posts";
import Form from "./components/Form/Form";
import wisp_logo from "./images/wisp_logo.png";
import useStyles from "./styles";
const App = () => {
const [currentId, setCurrentId] = useState();
const classes = useStyles();
const dispatch = useDispatch();
const [isDarkMode, setIsDarkMode] = useState(() => false);
return (
<Container maxwidth="lg">
<AppBar className={classes.appBar} position="static" color="inherit">
<DarkModeToggle
onChange={setIsDarkMode}
checked={isDarkMode}
size={80}
/>
</AppBar>
</Container>
);
};
export default App;
If you wish to customize the theme, you need to use the ThemeProvider component in order to inject a theme into your application. Here's a simple example:
Custom variables:
const darkTheme = createTheme({
palette: {
type: "dark"
}
});
Use the ThemeProvider component:
<ThemeProvider theme={darkTheme}>
<AppBar
position="static"
color={`${isDarkMode ? "default" : "primary"}`}
>
<DarkModeToggle
onChange={setIsDarkMode}
checked={isDarkMode}
size={80}
/>
</AppBar>
</ThemeProvider>
Using a ternary to change the theme:
color={`${isDarkMode ? "default" : "primary"}`}
Exemplo completo aqui: https://codesandbox.io/s/holy-wood-cdgpks?file=/src/App.js
I believe there are 2 things to change:
The first one is that "isDarkMode" is a boolean, and inside the useState you're using a function, so must be like this:
const [isDarkMode, setIsDarkMode] = useState(false);
Also, when sending the "setIsDarkMode" function you need to update your state, like this:
<DarkModeToggle
onChange={() => setIsDarkMode(prevState => !prevState)}
checked={isDarkMode}
size={80}
/>
So "onChange" is now a function that is going to update the state on every click
My app initially renders with a loading placeholder within the component, after 5 seconds different elements are rendered. I want to test this component with jest and enzyme.
My issue is that when I test it and console.log(wrapper.debug()) it only shows the loading part.
Now my question is how I can unit test the conditional part.
My App.js
import React,{useState, useEffect} from 'react';
import logo from './logo.svg';
import './App.css';
const App = () => {
const [loading, setLoading] = useState(true);
useEffect(()=>{
setTimeout(() => {
setLoading(false)
}, 5000);
},[])
if(loading){
return <span>loading....</span>
}
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
My App.test.js
import React from "react";
import App from "./App";
import { shallow } from "enzyme";
describe("app component", () => {
it("loading state", () => {
const setState = jest.fn();
const useStateMock = (initState) => [initState, setState];
jest.spyOn(React, "useState").mockImplementation(useStateMock);
jest.spyOn(React, "useEffect").mockImplementation(setState);
const wrapper = shallow(<App />);
const result = wrapper.find("#abc");
result.simulate("click");
wrapper.update();
console.log(wrapper.debug());
expect(setState).toHaveBeenCalledWith(false);
});
});
You can try use: jest.setTimeout(6000)
import React from "react";
import App from "./App";
import { shallow } from "enzyme";
describe("app component", () => {
it("loading state", () => {
const setState = jest.fn();
const useStateMock = (initState) => [initState, setState];
jest.spyOn(React, "useState").mockImplementation(useStateMock);
jest.spyOn(React, "useEffect").mockImplementation(setState);
const wrapper = shallow(<App />);
const result = wrapper.find("#abc");
result.simulate("click");
wrapper.update();
jest.setTimeout(6000)
console.log(wrapper.debug());
expect(setState).toHaveBeenCalledWith(false);
})
I've added an authorization with firebase, which works completely fine.
It is possible to login to the app, and navigate, but when I use firebase.auth().signOut the onAuthChanged observable is not changed/not triggered.
For correct login/password(400 for the wrong combination) - the session is saved, and I have the user credentials:
import React, {useContext, useEffect} from 'react';
import {ROUTES} from '../../../constants';
import {AuthUserContext} from "../../../session";
import history from '../../../helpers/history';
import {useLocation} from "react-router";
import app from "../../../api/firebase";
const WithAuthorization: React.FC = ({children}) => {
const authUser = useContext(AuthUserContext);
const isLogin = useLocation().pathname === ROUTES.LOGIN;
const pushLogin = () => !isLogin && history.push(ROUTES.LOGIN);
useEffect(() => {
const listener = app.auth().onAuthStateChanged(
(user: any) => {
if(!user) {
pushLogin()
} else {
console.log('Signed in with user');
console.log(user);
}
},
(e: any) => {
console.log(e);
}, () => {
console.log('completed');
});
return listener();
}, [])
return <>
{authUser ? children : pushLogin()}
</>;
}
export default WithAuthorization;
But then, when the application is refreshed, I want to check if the session is alive.
While looking through the docs I've found onAuthChanged observable, which seems pretty straight-forward, but it is actually triggered only when I log in.
After the page is refreshed, or when I trigger signOut - it does nothing.
This is the authorization protection component, that wraps the entire App:
import React, {useContext, useEffect} from 'react';
import {ROUTES} from '../../../constants';
import {AuthUserContext} from "../../../session";
import history from '../../../helpers/history';
import app from "../../../api/firebase";
const WithAuthorization: React.FC = ({children}) => {
const authUser = useContext(AuthUserContext);
const pushLogin = () => history.push(ROUTES.LOGIN);
useEffect(() => {
const listener = app.auth().onAuthStateChanged(
(user: any) => {
if(!user) pushLogin()
},
(e: any) => {
console.log(e);
}, () => {
console.log('completed');
});
return listener();
}, [])
return <>
{authUser ? children : pushLogin()}
</>;
}
export default WithAuthorization;
Am I missing something with the auth protection component or observable?
--- The app structure:
The App component is quite simple:
import React, {useState} from 'react';
import { Route, Switch, useLocation } from 'react-router';
import { Header, WithAuthorization } from './common';
import DeviceSelection from './DeviceSelection';
import PerfectScroll from 'react-perfect-scrollbar';
import NotFound from './NotFound';
import ThankYou from "./Thankyou";
import 'react-perfect-scrollbar/dist/css/styles.css';
import './App.scss';
import {ROUTES} from "../constants";
import Login from "./Login";
import {AuthUserContext} from "../session";
const App = () => {
const {pathname} = useLocation();
const [authUser, setAuthUser] = useState(null as any);
const isThankYou = pathname === ROUTES.THANKYOU;
return (
<AuthUserContext.Provider
value={authUser}
>
<WithAuthorization>
{!isThankYou && <Header authUser={authUser}/>}
</WithAuthorization>
<div className={`${!isThankYou ? 'appScrollContainer' : ''}`}>
<PerfectScroll>
<Switch>
<Route exact path={[ROUTES.ROOT, ROUTES.HOME]} component={() => <WithAuthorization><DeviceSelection/></WithAuthorization>} />
<Route path={ROUTES.THANKYOU} component={() => <WithAuthorization><ThankYou/></WithAuthorization>} />
<Route path={ROUTES.LOGIN} component={() => <Login setAuthUser={(user: any) => setAuthUser(user)} />}/>
<Route path="*" component={NotFound} />
</Switch>
</PerfectScroll>
</div>
</AuthUserContext.Provider>
);
}
export default App;
Signout is coming from a button, inside Header, which is also wrapped in WithAuthorization:
<Button label={'Sign out'} click={() => app.auth().signOut()} />
Login does only one 1 thing, redirects to /home if login was successful:
import React, {useState} from 'react';
import TextInput from "../common/TextInput";
import history from '../../helpers/history';
import {ROUTES} from "../../constants";
import app, {signInWithEmailAndPassword} from "../../api/firebase";
interface Props {
setAuthUser: (user: any) => void,
}
const Login: React.FC<Props> = ({setAuthUser}) => {
const [form, updateForm] = useState({login: '', password: ''});
const authorize = (user: string, password: string) => {
app.auth().setPersistence(app.auth.Auth.Persistence.SESSION)
.then(() => {
return signInWithEmailAndPassword(user, password).then((user: any) => {
if(user) {
setAuthUser(user);
history.push(ROUTES.ROOT);
return user
}
return null
})
})
.catch((e: any) => {
console.log(e);
})
}
return <div className='form'>
<TextInput
type="text"
placeholder='login'
name={'login'}
value={form.login}
label='Login'
onChange={(e) => updateForm({...form, login: e.currentTarget.value})}
/>
<TextInput
type="password"
placeholder='password'
name={'password'}
value={form.password}
label='Password'
onChange={(e) => updateForm({...form, password: e.currentTarget.value})}
/>
<button onClick={() => authorize(form.login, form.password)}>Submit</button>
</div>
}
export default Login;
FIrebase usage itself:
import firebase from 'firebase/app';
import 'firebase/firestore';
import 'firebase/auth';
import {DEV_LOCAL_CONFIG, DEV_REMOTE_CONFIG, ORDERS_COLLECTION} from "./const";
firebase.initializeApp(window.location.hostname !== 'localhost' ? DEV_LOCAL_CONFIG : DEV_REMOTE_CONFIG);
/* ==== Authorization ==== */
const signInWithEmailAndPassword = (email: string, password: string) =>
firebase.auth().signInWithEmailAndPassword(email, password);
const signOut = () => firebase.auth().signOut();
export default firebase;
export {
signInWithEmailAndPassword,
signOut
}
My mistake was with this line only:
return listener();
When I define listener in useEffect, it is unsubscribed immediately.
Should be:
return () => listener()
Other than this, everything works fine.
I am creating my react app with material-ui Snackbar.
In my project I have a lot of components and don't want to insert <Snackbar/> in each of them.
Is there a way to create function that will show snackbar, then just import and use this function in each component?
Something like:
import showSnackbar from 'SnackbarUtils';
showSnackbar('Success message');
You have to do it in react way. You can achieve this by creating a Higher Order Component.
Create a HOC that returns a snackbar component along with the wrappedComponent
Create a function in that HOC which accepts message, severity (if you are using Alert like me), duration and sets the appropriate states which are set to the props of the snackbar. And pass that function as a prop to the wrappedComponent.
Finally import this HOC wherever you want to display a snackbar, pass your component in it and call the HOC function from the prop (this.prop.functionName('Hello there!')) in the event handler where you want to display a snackbar and pass in a message.
Check this out.
https://stackblitz.com/edit/snackbar-hoc?file=src/SnackbarHOC.js
extend it as a Hook, and then you can call it once and use state with effects to show:
import { useSnackbar } from 'notistack';
import IconButton from "#mui/material/IconButton";
import CloseIcon from "#mui/material/SvgIcon/SvgIcon";
import React, {Fragment, useEffect, useState} from "react";
const useNotification = () => {
const [conf, setConf] = useState({});
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const action = key => (
<Fragment>
<IconButton onClick={() => { closeSnackbar(key) }}>
<CloseIcon />
</IconButton>
</Fragment>
);
useEffect(()=>{
if(conf?.msg){
let variant = 'info';
if(conf.variant){
variant = conf.variant;
}
enqueueSnackbar(conf.msg, {
variant: variant,
autoHideDuration: 5000,
action
});
}
},[conf]);
return [conf, setConf];
};
export default useNotification;
Then you can use it:
const [msg, sendNotification] = useNotification();
sendNotification({msg: 'yourmessage', variant: 'error/info.....'})
Here is a sample code for fully working example using Redux, Material-ui and MUI Snackbar
import { random } from 'lodash'
import { Action } from 'redux'
import actionCreatorFactory, { isType } from 'typescript-fsa'
const actionCreator = actionCreatorFactory()
export type Notification = {
message: string
}
export type NotificationStore = Notification & {
messageId: number
}
export const sendNewNotification =
actionCreator<Notification>('NEW_NOTIFICATION')
const defaultState: NotificationStore = { message: '', messageId: 1 }
const reducer = (
state: NotificationStore = defaultState,
action: Action
): NotificationStore => {
if (isType(action, sendNewNotification)) {
const {
payload: { message }
} = action
return { message, messageId: random(0, 200000) }
}
return state
}
export default reducer
// useNotification to get state from Redux, you can include them into same file if you prefer
import { NotificationStore } from './notification'
export function useNotification(): NotificationStore {
return useSelector<NotificationStore>(
(state) => state.notification
)
}
// Notification React-component - Notification.tsx
import React, { useState } from 'react'
import Button from '#mui/material/Button'
import Snackbar from '#mui/material/Snackbar'
import IconButton from '#mui/material/IconButton'
import CloseIcon from '#mui/icons-material/Close'
type Props = {
message: string
}
export function Notification({ message }: Props): JSX.Element | null {
const [notiOpen, setNotiOpen] = useState(true)
if (!message) {
return null
}
return (
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left'
}}
open={notiOpen}
autoHideDuration={10000}
onClose={() => setNotiOpen(false)}
message={message}
action={
<React.Fragment>
<Button
color="secondary"
size="small"
onClick={() => setNotiOpen(false)}
>
Close
</Button>
<IconButton
size="small"
aria-label="close"
color="inherit"
onClick={() => setNotiOpen(false)}
>
<CloseIcon fontSize="small" />
</IconButton>
</React.Fragment>
}
/>
)
}
// Main App.tsx to run my application
import { Notification } from "./Notification.tsx"
import { useDispatch } from 'react-redux'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
const App: React.FC<AppProps> = () => {
const dispatch = useDispatch()
const { message, messageId } = useNotification()
return (
<ThemeProvider theme={appTheme}>
<Router>
<Switch>
<Route path="/public/:projectId" component={ProjectPage} />
<Route path="/login" component={LoginPage} />
<Route render={() => <PageNotFound />} />
</Switch>
</Router>
<Notification key={messageId} message={message} />
</ThemeProvider>
)
}
export default App
// Usage of hook in application - FileSomething.tsx
import { useDispatch } from 'react-redux'
import { useEffect } from 'react'
import { sendNewNotification } from 'src/redux/notification'
export function FileSomething(): JSX.Element {
function sendNotification() {
dispatch(
sendNewNotification({
message: 'Hey, im a notification'
})
)
}
useEffect(() => {
sendNotification()
}, [])
return (
<div>Component doing something</div>
)
}
I'm trying to create a darkmode library (named react-goodnight) based on https://github.com/luisgserrano/react-dark-mode.
This is where the context is created.
import React from 'react'
const ThemeContext = React.createContext({
theme: '',
toggle: () => {}
})
export default ThemeContext
This is my useDarkMode hook that get/sets the theme to localStorage.
import { useState, useEffect } from 'react'
const useDarkMode = () => {
const [theme, setTheme] = useState('light')
const setMode = (mode) => {
window.localStorage.setItem('theme', mode)
setTheme(mode)
}
const toggle = () => (theme === 'light' ? setMode('dark') : setMode('light'))
useEffect(() => {
const localTheme = window.localStorage.getItem('theme')
localTheme && setTheme(localTheme)
}, [])
return [theme, toggle]
}
export default useDarkMode
This is the index of my library (react-goodnight).
import React, { useContext } from 'react'
import { ThemeProvider } from 'styled-components'
import { GlobalStyles } from './globalStyles'
import { lightTheme, darkTheme } from './settings'
import ThemeContext from './themeContext'
import useDarkMode from './useDarkMode'
const Provider = ({ children }) => {
const [theme, toggle] = useDarkMode()
return (
<ThemeProvider theme={theme === 'light' ? lightTheme : darkTheme}>
<GlobalStyles />
<ThemeContext.Provider value={{ theme, toggle }}>
<button onClick={toggle}>Toggle</button>
{children}
</ThemeContext.Provider>
</ThemeProvider>
)
}
export const useDarkModeContext = () => useContext(ThemeContext)
export default Provider
And, in the end, this is my example app where I'm trying to use it.
import React from 'react'
import Provider, { useDarkModeContext } from 'react-goodnight'
const App = () => {
const { theme, toggle } = useDarkModeContext();
console.log(theme)
return (
<Provider>
<div>hey</div>
<button onClick={toggle}>Toggle</button>
</Provider>
)
}
export default App
The "Toggle" button in the library's index works fine but the one in my example app does not.
The useDarkModeContext() returns empty.
What could be the issue?
Thanks!
You are doing wrong
1st option
you can use react-goodnight provider with your index.js and use useDarkModeContext(), don't name your index.js Provider else you can not use Provider coming from react-goodnight
import Provider, { useDarkModeContext } from 'react-goodnight'
const Provider = ({ children }) => {
const [theme, toggle] = useDarkMode()
return (
<ThemeProvider theme={theme === 'light' ? lightTheme : darkTheme}>
<GlobalStyles />
<Provider>
<ThemeContext.Provider value={{ theme, toggle }}>
<button onClick={toggle}>Toggle</button>
{children}
</ThemeContext.Provider>
</Provider>
</ThemeProvider>
)
}
2nd Option
you are passing ThemeContext in your index.js so you can also access that in app.js
import React, { useContext } from 'react'
import ThemeContext from './themeContext'
const App = () => {
const theme = useContext(ThemeContext);
console.log(theme)
return (
<Provider>
<div>hey</div>
<button onClick={toggle}>Toggle</button>
</Provider>
)
}
export default App
The reason it's not working is because you are calling useContext in the very same place where you print Provider.
Why is that wrong? Because useContext looks for parent context providers. By rendering Provider in the same place you call useContext, there is no parent to look for. The useContext in your example is actually part of App component, who is not a child of Provider.
All you have to do is move the button outside of that print, to its own component, and only there do useContext (or in your case the method called useDarkModeContext.
The only change would be:
import React from 'react'
import Provider, { useDarkModeContext } from 'react-goodnight'
const App = () => {
return (
<Provider>
<div>hey</div>
<ToggleThemeButton />
</Provider>
)
}
export default App
const ToggleThemeButton = () => {
const { theme, toggle } = useDarkModeContext();
return (
<button onClick={toggle}>Switch Theme outside</button>
);
};