React: Enzyme's it local scope does not work - reactjs

I am using React. I used basic redux counter by using redux-toolkit. I am really new in testing. I am using Enzyme and Jest for the test. My redux counter intialState is 1. From my testing, inside it scope I first take the intialState then after simulate('click') increase button, I got result 2, which I expected. When I try to test my decrease button inside the it scope it takes the result from increase's it scope. If I put intialState 1 inside the decrease button's it scope, it gives me failed test because it expected 2.
This is my testing file
import React from 'react';
import { mount } from "enzyme"; // mount is fulldom renderning function with children
import Counter from 'components/counter';
import Root from "root/index"; // this is the root index which connect react component and redux
let wrapped;
beforeEach(() => {
wrapped = mount(
<Root>
<Counter />
</Root>
);
});
afterEach(() => {
wrapped.unmount(); // it cleans the mount after test.
});
describe(`This is counter component`, () => {
it(`This is show intial Value`, () => {
expect(wrapped.find(`h1`).text()).toEqual(`1`);
});
it(`after click it will increase the value`, () => {
expect(wrapped.find(`h1`).text()).toEqual(`1`);
wrapped.find(`button`).at(0).find(`[data-test="increment"]`).simulate(`click`);
expect(wrapped.find(`h1`).text()).toEqual(`2`);
});
it(`after click it will decrease the value`, () => {
expect(wrapped.find(`h1`).text()).toEqual(`1`); // test failed because it expect 2
wrapped.find(`button`).at(1).find(`[data-test="decrement"]`).simulate(`click`);
expect(wrapped.find(`h1`).text()).toEqual(`0`);
});
});
This is my counter component
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from 'store/reducer/counter/index';
import { IRootState } from 'store/combineReducer';
import styled from 'styled-components';
const Button = styled.button`
background-color: #4CAF50; /* Green */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
`;
const Text = styled.h1`
color: blue;
`;
export default () => {
const counter = useSelector((state: IRootState) => state.counter);
const dispatch = useDispatch();
return (
<div >
<Text>{counter}</Text>
<Button data-test="increment" onClick={() => dispatch(increment())}>
Increment counter
</Button>
<br></br>
<br></br>
<Button data-test="decrement" onClick={() => dispatch(decrement())}>
Decrement counter
</Button>
</div>
);
};

it block has a scope in a sense that a function has a scope, additionally other things like spies can be affected by currently running test. There are no problems with scopes here, the only thing that is affected by scopes is wrapper variable and it's defined in a scope that is common to all tests. Since it's reassigned in beforeEach, it cannot result in test cross-contamination.
The problem here is that there's global state because Redux naturally provides one.
Most commonly a custom store is set up for tests so initial value, etc. could be manipulated depending on testing needs:
let wrapped;
beforeEach(() => {
const store = ...
wrapped = mount(
<Provider store={store}><Counter /></Provider>
);
});
This is what the documentation recommends.
Alternatively, a store and all modules that directly depend on it needs to be re-imported per test, this should be done instead of top-level import of these modules:
let wrapped;
let Root;
beforeEach(() => {
jest.resetModules();
Root = require("root/index");
wrapped = mount(
<Root>
<Counter />
</Root>
);
});

Related

Next.js: How to get applied styles from element?

Let's say I have global.css
.test {
background-color: black;
width: 50px;
height: 50px;
}
For some reason, I need to get the styles data from applied element. I tried refs but it always return empty string.
import { useState, useEffect, useRef } from "react";
const IndexPage = () => {
const divEl = useRef<HTMLDivElement | null>(null);
const [divStyle, setDivStyle] = useState({} as CSSStyleDeclaration);
useEffect(() => {
if (divEl.current) {
setDivStyle(divEl.current.style);
}
}, [divEl.current]);
return (
<div>
<div ref={divEl} className="test"></div>
<pre>{JSON.stringify(divStyle, undefined, 2)}</pre>
</div>
);
};
export default IndexPage;
Is it because next.js SSR or should I add something to dependency array?
code sandbox here
You can use computed styles to get what you need, although it won't be a "simple" object with properties. You'll need to query each property individually:
if (divEl.current) {
setDivStyle(getComputedStyle(divEl.current));
}
and then you can do something like:
divStyle.getPropertyValue("background-color")
Here's an example sandbox (forked from yours) that demostrates it.

Why I'm getting invalid hook call using Nav tag?

New React user here. I'm trying to make my first Navbar with React and getting this error:
Error: Invalid hook call. Hooks can only be called inside of the body
of a function component. This could happen for one of the following
reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
I tried to execute npm i -S react react-dom and also check that I'm executing the Hook correctly (understanding this concept yet, sorry). My current code is very simple:
import React from 'react';
import { Nav } from './NavbarElements';
const Navbar = () => {
return (
<Nav>
</Nav>
);
};
export default Navbar;
Why I'm getting this error with Nav tag? The element Nav is declared in NavbarElements.js:
import styled from 'styled-components';
export const Nav = styled.nav`
background: #000;
height: 80px;
display: flex;
justify-content: center;
align-items: center;
font-size: 1rem;
position: sticky;
top: 0;
z-index: 10;
#media screen and (max-width: 960px) {
trasition: 0.8s all ease;
}
`;
Moreover, I'm using React to make the front part of a DApp (blockchain App). I have my file main file App.js (which's kinda big):
class App extends Component {
state = { storageValue: 0, web3: null, accounts: null, contract: null };
componentDidMount = async () => {
try {
// Get network provider and web3 instance.
const web3 = await getWeb3();
// Use web3 to get the user's accounts.
const accounts = await web3.eth.getAccounts();
// Get the contract instance.
const networkId = await web3.eth.net.getId();
const deployedNetwork = SimpleStorageContract.networks[networkId];
const instance = new web3.eth.Contract(
SimpleStorageContract.abi,
deployedNetwork && deployedNetwork.address,
);
instance.options.address = "0xA65990EC0CA555d2eCDD1d84E9D1397CFA967E60"
// Set web3, accounts, and contract to the state, and then proceed with an
// example of interacting with the contract's methods.
this.setState({ web3, accounts, contract: instance }, this.runExample);
} catch (error) {
// Catch any errors for any of the above operations.
alert(
`Failed to load web3, accounts, or contract. Check console for details.`,
);
console.error(error);
}
};
runExample = async () => {
const { accounts, contract } = this.state;
// Stores a given value, 5 by default.
await contract.methods.set(25).send({ from: accounts[0] });
// Get the value from the contract to prove it worked.
const response = await contract.methods.get().call();
// Update state with the result.
this.setState({ storageValue: response });
};
render() {
if (!this.state.web3) {
return <div>Loading Web3, accounts, and contract...</div>;
}
return (
<div className="App">
<Router>
<Navbar />
</Router>
</div>
);
}
}
export default App;
And, the error is on the line this.setState({ web3, accounts, contract: instance }, this.runExample);. My question is: Am I declaring my react "interface" in a bad place? It's after render() instruction (at the end of the file), where I thought it is. But, the point is that I'm getting that error only when I use the Nav tag. Am I misunderstanding something?

React - Material UI test TypeError: Cannot read property 'get' of undefined

I have this code:
/* eslint-disable react/display-name */
import { Box, Button, LinearProgress, makeStyles } from '#material-ui/core';
import { Refresh } from '#material-ui/icons';
import { SearchHistoryContext } from 'appContext';
import useSnackBar from 'components/useSnackbar';
import PropTypes from 'prop-types';
import QueryString from 'qs';
import React, { useContext, useEffect, useState } from 'react';
import { formatDate } from 'utils/formatDate';
import http from 'utils/http';
const styles = makeStyles(() => ({
progress: {
margin: '-15px -15px 11px -15px',
},
button: {
width: '150px',
},
details: {
fontSize: '15px',
},
}));
const LogsMainPage = props => {
const classes = styles();
const { location, history, match, updateQuery } = props;
const [displaySnackbar] = useSnackBar();
const updateData = async () => {
history.push({
search: QueryString.stringify(query),
state: 'compliance-logs',
});
};
const logTableColumn = [
{
columns: [
{
Header: 'Timestamp',
id: '_id',
accessor: c => formatDate(c.timestamp, 'd/MM/yyyy, H:mm:ss'),
minWidth: 120,
},
{
Header: 'User Email',
id: 'user_email',
accessor: c => c.user_email,
minWidth: 80,
},
],
},
];
return (
<div>
{isLoading && <LinearProgress className={classes.progress} />}
<Box display="flex" justifyContent="flex-end">
<Button
id="refresh-button"
variant="outlined"
color="primary"
className={classes.button}
disabled={isLoading}
onClick={updateData}
startIcon={<Refresh />}
>
Refresh
</Button>
</Box>
<Box mb={2} />
</div>
);
};
LogsMainPage.propTypes = {
history: PropTypes.object,
match: PropTypes.object,
location: PropTypes.object,
updateQuery: PropTypes.func,
};
export default LogsMainPage;
Unit Test:
import LogsMainPage from 'containers/Log/LogsMainPage';
import { shallow } from 'enzyme';
import notistack from 'notistack';
import React from 'react';
jest.mock('notistack', () => ({
useSnackbar: jest.fn(),
}));
const enqueueSnackbar = jest.fn();
jest.spyOn(notistack, 'useSnackbar').mockImplementation(() => {
return { enqueueSnackbar };
});
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: () => ({
searches: {},
}),
}));
afterEach(() => {
jest.clearAllMocks();
});
describe('render test', () => {
const mockCallBack = jest.fn();
const wrapper = shallow(
<LogsMainPage history={{ push: jest.fn() }} location={{ search: {} }} />,
);
it('renders without crashing', () => {
expect(wrapper).toHaveLength(1);
});
it('renders refresh button without crashing', () => {
const button = wrapper.find('#refresh-button');
expect(button).toHaveLength(1);
button.setProps({ onClick: mockCallBack });
button.simulate('click');
expect(mockCallBack).toHaveBeenCalledTimes(1);
});
});
When I ran my single test (each of expect) it always pass. But if I ran the describe part it always failed.
TypeError: Cannot read property 'get' of undefined
the editor shown the error is on const classes = styles() part but all my other unit test using same as this, is passed.
Any solution?
Short Answer: In your case, it's because you mocked useContext BUT makeStyles internally uses useContext, hence it cannot find the context object it was supposed to find and try a get method of an undefined result in the mocked context you provided. So you have to:
Mock makeStyles correctly.
Mock useContext EXCEPT for the Material UI contexts.
Let's dive deeper.
Mocking Material UI makeStyles with simple jest function make you loose test coverage. When making it more complex, it leads to some problems.
But when you fix it, or reverse it, each problem solved leads to another when using shallow rendering (mount will not do it, can be slow on top level components):
it lost test coverage when calling useStyles which is now an empty function with no style (const useStyles = makeStyles(theme => {...}))
Not mocking it throw error for additional values of a custom theme
Binding mocked function argument with the custom theme works, and you can call function argument to fill coverage. But you loose coverage if passing parameters when calling useStyles({ variant: 'contained', palette: 'secondary' }) (result function of makeStyles)
Lot of things broke when mocking useContext, because makeStyles result function uses useContext internally.
(example of useStyles parameter handling)
{
backgroundColor: props => {
if (props.variant === 'contained') {
return theme.palette[props.palette].main;
}
return 'unset';
},
}
You should know how to correctly mock makeStyles for maximum testing experience. I managed to solved all of these problems and use manual mock https://jestjs.io/docs/en/manual-mocks (or inline mocking as alternative at the bottom):
Step 1:
I mocked in the core path instead, but both should work: <root>/__mocks__/#material-ui/core/styles.js
// Grab the original exports
// eslint-disable-next-line import/no-extraneous-dependencies
import * as Styles from '#material-ui/core/styles';
import createMuiTheme from '#material-ui/core/styles/createMuiTheme';
import options from '../../../src/themes/options'; // I put the theme options separately to be reusable
const makeStyles = func => {
/**
* Note: if you want to mock this return value to be
* different within a test suite then use
* the pattern defined here:
* https://jestjs.io/docs/en/manual-mocks
*/
/**
* Work around because Shallow rendering does not
* Hook context and some other hook features.
* `makeStyles` accept a function as argument (func)
* and that function accept a theme as argument
* so we can take that same function, passing it as
* parameter to the original makeStyles and
* bind it to our custom theme, created on the go
* so that createMuiTheme can be ready
*/
const theme = createMuiTheme(options);
return Styles.makeStyles(func.bind(null, theme));
};
module.exports = { ...Styles, makeStyles };
So basically, this is just using the same original makeStyles and passes it the custom theme on the go which was not ready on time.
Step 2:
makeStyles result uses React.useContext, so we have to avoid mocking useContext for makeStyles use cases. Either use mockImplementationOnce if you use React.useContext(...) at the first place in you component, or better just filter it out in your test code as:
jest.spyOn(React, 'useContext').mockImplementation(context => {
// only stub the response if it is one of your Context
if (context.displayName === 'MyAppContext') {
return {
auth: {},
lang: 'en',
snackbar: () => {},
};
}
// continue to use original useContext for the rest use cases
const ActualReact = jest.requireActual('react');
return ActualReact.useContext(context);
});
And on your createContext() call, probably in a store.js, add a displayName property (standard), or any other custom property to Identify your context:
const store = React.createContext(initialState);
store.displayName = 'MyAppContext';
The makeStyles context displayName will appear as StylesContext and ThemeContext if you log them and their implementation will remain untouched to avoid error.
This fixed all kind of mocking problems related to makeStyles + useContext. And in term of speed, it just feels like the normal shallow rendering speed and can keep you away of mount for most use cases.
ALTERNATIVE to Step 1:
Instead of global manual mocking, we can just use the normal jest.mock inside any test. Here is the implementation:
jest.mock('#material-ui/core/styles', () => {
const Styles = jest.requireActual('#material-ui/core/styles');
const createMuiTheme = jest.requireActual(
'#material-ui/core/styles/createMuiTheme'
).default;
const options = jest.requireActual('../../../src/themes/options').default;
return {
...Styles,
makeStyles: func => {
const theme = createMuiTheme(options);
return Styles.makeStyles(func.bind(null, theme));
},
};
});
Since then, I also learned to mock useEffect and calling callback, axios global interceptors, etc.
---Store.js----
import React, { createContext, useReducer } from "react";
import Reducer from "./Reducer";
const initialState = {
userActivityData: {},
};
const Store = ({ children }) => {
const [state, dispatch] = useReducer(Reducer, initialState);
return (
<Context.Provider value={[state, dispatch]}>{children}</Context.Provider>
);
};
export const Context = createContext(initialState);
Context.displayName = "MyAppContext";
export default Store;
---Component.js--- using the above context store.
#adding context store in the test file works for me. mocking usecontext will break material styles.
---Component.test.js---
import Store from "../../Utils/Store";
beforeEach(() => {
wrapper = mount(
<Store>
<Component/>
</Store>
);
});
Note: This is not the complete code. But the point is, adding context store to the test file solved the problem.

How to avoid re-rendering with react-swipe library and useState

I am using a react library for swiping: https://github.com/voronianski/react-swipe
Making it work is straight-forward:
import React, {useEffect, useState} from 'react';
import ReactDOM from 'react-dom';
import ReactSwipe from 'react-swipe'
import styled from 'styled-components'
const StyledBox = styled.div`
height: 50px;
width: 100%;
background-color: orange;
`
const Carousel = () => {
let reactSwipeEl;
const [position, setPosition] = useState(0)
console.log('position', position)
const swipeOptions = {
continuous: false,
transitionEnd() {
setPosition(reactSwipeEl.getPos())
}
}
return (
<div>
<ReactSwipe
className="carousel"
swipeOptions={swipeOptions}
ref={el => (reactSwipeEl = el)}
>
<StyledBox>PANE 1</StyledBox>
<StyledBox>PANE 2</StyledBox>
<StyledBox>PANE 3</StyledBox>
</ReactSwipe>
<Circles>
<Circle isActive={0 === position} />
<Circle isActive={1 === position}/>
</Circles>
</div>
);
};
export default Carousel
The code that I added is the one related with useState. The library works fine, but every time I swipe, I use the callback transitionEnd, in order to update the position state. It is updated, but since a state variable changes, the whole Carousel component gets updated, setting automatically the init value, which is 0.
I don't understand how can I avoid this problem which is, updating the state without re-rendering the whole component. Should I split it in two components and use a provider?
I tried it, but when the state of the provider changes, both components are also re-render
The swipeOptions is recreated on each render, which causes the ReactSwipe to rerender as well. Wrap the swipeOptions with useMemo() (sandbox);
const swipeOptions = useMemo(() => ({
continuous: false,
transitionEnd() {
setPosition(reactSwipeEl.current.getPos())
}
}), []);

React + Jest: How to test private styled components?

I have a Charities component that shows text "Sorry..." when the status prop is === "error":
import React from "react";
import styled from "styled-components";
const Notification = styled.p`
text-align: center;
padding: 10px;
font-family: Raleway;
display: ${props => (props.hide ? "none" : "block")};
`;
const ErrorNotification = styled(Notification)`
background: #e3c7c4;
`;
export const Charities = ({
..., status
}) => (
<Container>
<ErrorNotification hide={status !== "error"}>
Sorry, something was wrong with your payment. Please try again.
</ErrorNotification>
...
</Container>
);
export default Charities;
I'm trying to test this with jest like this:
import React from "react";
import { mount, shallow } from "enzyme";
import { Charities } from "./index";
describe("Charities", () => {
let props;
let mountedCharities;
const charities = () => {
if (!mountedCharities) {
mountedCharities = mount(<Charities {...props} />);
}
return mountedCharities;
};
beforeEach(() => {
props = {
status: undefined,
...
};
mountedCharities = undefined;
});
describe("when status is pending", () => {
beforeEach(() => {
props.status = "pending";
});
it("doesn't render error", () => {
expect(charities().text()).not.toMatch(/Sorry/); // <---------- FAILS
});
});
});
My test fails with:
Expected value not to match:
/Sorry/
Received:
"Sorry, something was wrong with your payment. Please try again.Congratulations! You have successfully made a donation."
It seems like it's loading the children of the styled components even when it doesn't meet the conditions. How would I test this?
Your code is working as expected, it's just that styled() works by putting class names on the elements to control the styling.
In the unit test ErrorNotification is still there but it has css classes on it that will give it display: none in the final rendered HTML.
To make your components easier to unit test I recommend doing the hiding within Charities like this:
import React from "react";
import styled from "styled-components";
const Notification = styled.p`
text-align: center;
padding: 10px;
font-family: Raleway;
display: block;
`;
const ErrorNotification = styled(Notification)`
background: #e3c7c4;
`;
export const Charities = ({
..., status
}) => (
<Container>
{status !== "error" ? null : (
<ErrorNotification>
Sorry, something was wrong with your payment. Please try again.
</ErrorNotification>
)}
...
</Container>
);
export default Charities;
That way if the status in the props for Charities is anything except 'error' then ErrorNotification won't be rendered at all.
You are using the attribute hide which maps to 'display: none' when true this still renders the component albeit invisibly you should do something like this instead:
{ status === "error" &&
<ErrorNotification >
Sorry, something was wrong with your payment. Please try again.
</ErrorNotification>
}

Resources