How to mock BrowserRouter of react-router-dom using jest - javascript

I have this components that renders the routes of an app: https://jsbin.com/bahaxudijo/edit?js, I'm trying to mock the BrowserRouter and the Route to do the test, this are my test:
import React from 'react';
import renderer from 'react-test-renderer';
import Router from '../../../components/Router/Component';
jest.mock('react-router-dom', () => ({
BrowserRouter: ({ children }) => <div>{children}</div>,
Route: ({ children }) => <div>{children}</div>,
}));
jest.mock('../../../components/Nav/index', () => '<MockedNav />');
jest.mock('../../../components/ScheduleManager/index', () => '<MockedScheduleManager />');
const props = {
token: '',
loginStaff: jest.fn(),
};
describe('<Router />', () => {
describe('When is passed a token', () => {
it('renders the correct route', () => {
const component = renderer.create(<Router {...props} />);
expect(component).toMatchSnapshot();
});
});
});
But I'm mocking wrong the BrowserRouter and the Route, so the test passes but the snapshots are only empty divs. How can I properly mock the BrowserRouter and the Route?

jest.mock('react-router-dom', () => {
// Require the original module to not be mocked...
const originalModule = jest.requireActual('react-router-dom');
return {
__esModule: true,
...originalModule,
// add your noops here
useParams: jest.fn(),
useHistory: jest.fn(),
};
});

Yet another way:
const rrd = require('react-router-dom');
jest.spyOn(rrd, 'BrowserRouter').mockImplementation(({children}) => children);
Sources:
https://medium.com/#antonybudianto/react-router-testing-with-jest-and-enzyme-17294fefd303
https://stackoverflow.com/a/56565849/1505348

const reactRouter = require('react-router-dom');
const { MemoryRouter } = reactRouter;
const MockBrowserRouter = ({ children }) => (
<MemoryRouter initialEntries={['/']}>
{ children }
</MemoryRouter>
);
MockBrowserRouter.propTypes = { children: PropTypes.node.isRequired };
reactRouter.BrowserRouter = MockBrowserRouter;

Related

Jest: TypeError: Cannot read property 'location' of undefined with react-router-dom

i have an error more like this
TypeError: Cannot read property 'location' of undefined
my component code is like this
import React from "react";
import { useHistory } from "react-router-dom";
const CollapseComponent = () => {
const history = useHistory();
React.useEffect(() => {
if (history.location.search !== "") {
console.log("not empty");
} else {
console.log("empty");
}
}, [history]);
return (
<div>
<h1>Collapse</h1>
</div>
);
};
export default CollapseComponent;
and my test case is like this
import React from "react"
import { shallow } from "enzyme"
import Collapse from "./index";
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: jest.fn().mockReturnValue({
location: {
pathname: '/user-redeem-history',
search: '?redirectfrom=redeem',
}
}),
}));
describe("Collapse component", () => {
let wrapper;
let useEffect;
const mockUseEffect = () => {
useEffect.mockImplementationOnce(f => f());
};
beforeEach(() => {
/* mocking useEffect */
useEffect = jest.spyOn(React, "useEffect");
mockUseEffect();
wrapper = shallow(
<Collapse/>
);
});
describe("on mount", () => {
it("test", () => {
expect(wrapper).toMatchSnapshot();
});
});
});
i'm trying to wrap the wrapper and the error is gone, but i caught another issue, i cant call my useEffect logic
wrapper = shallow(
<Router history={history}>
<Collapse/>
</Router>
);
anyone have any solutions? i'm struggling with this error all day

Typescript, React & Socket.io-client Tests

I'm trying to figure out how to write a Typescript/React app which uses socket.io to communicate to a server and thus other clients. However I'd like to write some tests in doing so.
In my sample app I have:
import io, { Socket } from 'socket.io-client';
const App = () => {
let socket: Socket;
const ENDPOINT = 'localhost:5000';
const join = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
event.preventDefault();
socket = io(ENDPOINT);
socket.emit('join', { name: 'Paola', room: '1' }, () => {});
};
return (
<div className="join-container">
<button className="join-button" onClick={join} data-testid={'join-button'}>
Sign in
</button>
</div>
);
};
export default App;
And my test looks like:
import App from './App';
import { render, screen, fireEvent } from '#testing-library/react';
import 'setimmediate';
describe('Join', () => {
let mockEmitter = jest.fn();
beforeEach(() => {
jest.mock('socket.io-client', () => {
const mockedSocket = {
emit: mockEmitter,
on: jest.fn((event: string, callback: Function) => {}),
};
return jest.fn(() => {
return mockedSocket;
}
);
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('joins a chat', () => {
// Arrange
render(<App />);
const btn = screen.getByTestId('join-button');
// Act
fireEvent.click(btn);
// Assert
expect(btn).toBeInTheDocument();
expect(mockEmitter).toHaveBeenCalled();
});
});
I just want to make sure I can mock socket.io-client so that I can verify that messages are being sent to the client and that it (later) reacts to messages sent in.
However the test is failing and it doesn't seem to be using my mock.
Error: expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0
In the manual-mocks#examples doc, there is a note:
Note: In order to mock properly, Jest needs jest.mock('moduleName') to be in the same scope as the require/import statement.
So, there are two solutions:
app.tsx:
import React from 'react';
import io, { Socket } from 'socket.io-client';
const App = () => {
let socket: Socket;
const ENDPOINT = 'localhost:5000';
const join = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
socket = io(ENDPOINT);
socket.emit('join', { name: 'Paola', room: '1' }, () => {});
};
return (
<div className="join-container">
<button className="join-button" onClick={join} data-testid={'join-button'}>
Sign in
</button>
</div>
);
};
export default App;
Option 1: Call jest.mock and import ./app module in module scope of the test file.
app.test.tsx:
import App from './App';
import { render, screen, fireEvent } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import React from 'react';
let mockEmitter = jest.fn();
jest.mock('socket.io-client', () => {
return jest.fn(() => ({
emit: mockEmitter,
on: jest.fn(),
}));
});
describe('Join', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('joins a chat', () => {
// Arrange
render(<App />);
const btn = screen.getByTestId('join-button');
// Act
fireEvent.click(btn);
// Assert
expect(btn).toBeInTheDocument();
expect(mockEmitter).toHaveBeenCalled();
});
});
Option 2: Since you call the jest.mock in beforeEach hook, require the './app' module in beforeEach hook function scope as well.
app.test.tsx:
import { render, screen, fireEvent } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import React from 'react';
describe('Join', () => {
let mockEmitter = jest.fn();
let App;
beforeEach(() => {
App = require('./app').default;
jest.mock('socket.io-client', () => {
const mockedSocket = {
emit: mockEmitter,
on: jest.fn(),
};
return jest.fn(() => mockedSocket);
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('joins a chat', () => {
// Arrange
render(<App />);
const btn = screen.getByTestId('join-button');
// Act
fireEvent.click(btn);
// Assert
expect(btn).toBeInTheDocument();
expect(fakeEmitter).toHaveBeenCalled();
});
});
package version:
"jest": "^26.6.3",
"ts-jest": "^26.4.4"
jest.config.js:
module.exports = {
preset: 'ts-jest/presets/js-with-ts',
testEnvironment: 'jsdom'
}

how to listen to page changes in nextjs

I want to reuse a menu I made in react with react-router-dom, but this time in nextjs. The goal is to change the state of the menu to 'false' and the menuName to 'menu' when I click on a link inside the menu.
I use a useEffect function to listen history :
//use effect for page changes
useEffect(() => {
//listen for page changes
history.listen(() => {
setState({ clicked: false, menuName: "Menu" })
})
})
and wrapped my component with withRouter :
import { withRouter } from 'next/router'
[...]
export default withRouter(Header);
Unfortunately, it prints :
TypeError: Cannot read property 'listen' of undefined
Should I better use 'useRouter' to solve this problem? How?
Thank you ;)
It worked that way :
Nextjs : Router Events
import { useRouter } from "next/router";
...
const router = useRouter()
useEffect(() => {
const handleRouteChange = (url) => {
console.log(
`App is changing to ${url}`
)
setState({ clicked: false, menuName: "Menu" })
}
router.events.on('routeChangeStart', handleRouteChange)
// If the component is unmounted, unsubscribe
// from the event with the `off` method:
return () => {
router.events.off('routeChangeStart', handleRouteChange)
}
}, [])
import { useState, useEffect } from "react";
import { Layout, PageLoading } from ".";
import { useRouter } from "next/router";
const LayoutContainer = ({ pageProps, Component, store, setDeviceWidthAction }) => {
const router = useRouter();
const [pageLoading, setPageLoading] = useState(true);
useEffect(() => {
setDeviceWidthAction(window.innerWidth);
setPageLoading(false);
}, []);
useEffect(() => {
const handleStart = (url) => {
setPageLoading(true);
};
const handleStop = () => {
setPageLoading(false);
};
router.events.on("routeChangeStart", handleStart);
router.events.on("routeChangeComplete", handleStop);
router.events.on("routeChangeError", handleStop);
return () => {
router.events.off("routeChangeStart", handleStart);
router.events.off("routeChangeComplete", handleStop);
router.events.off("routeChangeError", handleStop);
};
}, [router]);
if (pageLoading) return <PageLoading />;
return <Layout {...{ pageProps, Component, store, pageLoading }} />;
};
export default LayoutContainer;

Passing context on test cases for React shallow enzyme -jest

How can I pass the context and extract the data in a helper method?
See the below snippet:
import AppContext from '../../context/AppContext'
import extractDatta from '../../helper';
class App extends Component{
static contextType = AppContext
componentWillMount(){
const resolvedData = extractData("/home",this.context)
}
render(){
return(
)
}
}
helper/index.js:
const extractData = (path, context) => {
// getting context as undefined
}
App.test.js:
describe('App test suites', () => {
let wrapper;
let appContext;
beforeEach(() => {
appContext = {name: "Application"}
wrapper = shallow(<Supplier />, { appContext })
})
it('Should render the App Component', () => {
expect(wrapper).toMatchSnapshot();
})
})
Any help appreciated :)
So here is my hacky workaround whilst the Enzyme team work on finishing [https://github.com/airbnb/enzyme/issues/1553](React 16 support). I'm providing the legacy contextTypes on the component class, allowing the context to be passed as per the docs.
import PropTypes from "prop-types";
describe('App test suites', () => {
let wrapper;
let appContext;
beforeEach(() => {
appContext = {name: "Application"}
// Hack in the legacy context API just for tests. You'll get a warning, but it
// ought to work...
Supplier.contextTypes = {
name: PropTypes.string
}
wrapper = shallow(<Supplier />, { appContext })
})
it('Should render the App Component', () => {
expect(wrapper).toMatchSnapshot();
})
})
That probably wouldn't help. But shouldn't you pass Options object with context parameter name instead of appContext?
wrapper = shallow(<Supplier />, { context: appContext })

Unit testing React click outside component

Using the code from this answer to solve clicking outside of a component:
componentDidMount() {
document.addEventListener('mousedown', this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClickOutside);
}
setWrapperRef(node) {
this.wrapperRef = node;
}
handleClickOutside(event) {
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
this.props.actions.something() // Eg. closes modal
}
}
I can't figure out how to unit test the unhappy path so the alert isn't run, what i've got so far:
it('Handles click outside of component', () => {
props = {
actions: {
something: jest.fn(),
}
}
const wrapper = mount(
<Component {... props} />,
)
expect(props.actions.something.mock.calls.length).toBe(0)
// Happy path should trigger mock
wrapper.instance().handleClick({
target: 'outside',
})
expect(props.actions.something.mock.calls.length).toBe(1) //true
// Unhappy path should not trigger mock here ???
expect(props.actions.something.mock.calls.length).toBe(1)
})
I've tried:
sending through wrapper.html()
.finding a node and sending through (doesn't mock a event.target)
.simulateing click on an element inside (doesn't trigger event listener)
I'm sure i'm missing something small but I couldn't find an example of this anywhere.
import { mount } from 'enzyme'
import React from 'react'
import ReactDOM from 'react-dom'
it('Should not call action on click inside the component', () => {
const map = {}
document.addEventListener = jest.fn((event, cb) => {
map[event] = cb
})
const props = {
actions: {
something: jest.fn(),
}
}
const wrapper = mount(<Component {... props} />)
map.mousedown({
target: ReactDOM.findDOMNode(wrapper.instance()),
})
expect(props.actions.something).not.toHaveBeenCalled()
})
The solution from this enzyme issue on github.
The selected answer did not cover the else path of handleClickOutside
I added mousedown event on ref element to trigger else path of handleClickOutside
import { mount } from 'enzyme'
import React from 'react'
import ReactDOM from 'react-dom'
it('Should not call action on click inside the component', () => {
const map = {}
document.addEventListener = jest.fn((event, cb) => {
map[event] = cb
})
const props = {
actions: {
something: jest.fn(),
}
}
//test if path of handleClickOutside
const wrapper = mount(<Component {... props} />)
map.mousedown({
target: ReactDOM.findDOMNode(wrapper.instance()),
})
//test else path of handleClickOutside
const refWrapper = mount(<RefComponent />)
map.mousedown({
target: ReactDOM.findDOMNode(refWrapper.instance()),
})
expect(props.actions.something).not.toHaveBeenCalled()
})
I found the case/solution where the usage of ReactDOM.findDOMNode can be avoided. Treat the following example:
import React from 'react';
import { shallow } from 'enzyme';
const initFireEvent = () => {
const map = {};
document.addEventListener = jest.fn((event, cb) => {
map[event] = cb;
});
document.removeEventListener = jest.fn(event => {
delete map[event];
});
return map;
};
describe('<ClickOutside />', () => {
const fireEvent = initFireEvent();
const children = <button type="button">Content</button>;
it('should call actions.something() when clicking outside', () => {
const props = {
actions: {
something: jest.fn(),
}
};
const onClick = jest.fn();
mount(<ClickOutside {...props}>{children}</ClickOutside>);
fireEvent.mousedown({ target: document.body });
expect(props.actions.something).toHaveBeenCalledTimes(1);
});
it('should NOT call actions.something() when clicking inside', () => {
const props = {
actions: {
something: jest.fn(),
}
};
const wrapper = mount(
<ClickOutside onClick={onClick}>{children}</ClickOutside>,
);
fireEvent.mousedown({
target: wrapper.find('button').instance(),
});
expect(props.actions.something).not.toHaveBeenCalled();
});
});
Versions:
"react": "^16.8.6",
"jest": "^25.1.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2"
The simplest thing just dispatchEvent on body
mount(<MultiTagSelect {...props} />);
window.document.body.dispatchEvent(new Event('click'));
Use sinon to track the handleClickOutside is called or not. By the way, I just now released our project where I need this unit-test in the Nav component . Indeed when you click outside, all submenus should be closed.
import sinon from 'sinon';
import Component from '../src/Component';
it('handle clicking outside', () => {
const handleClickOutside = sinon.spy(Component.prototype, 'handleClickOutside');
const wrapper = mount(
<div>
<Component {... props} />
<div><a class="any-element-outside">Anylink</a></div>
</div>
);
wrapper.find('.any-element-outside').last().simulate('click');
expect(handleClickOutside.called).toBeTruthy();
handleClickOutside.restore();
})

Categories

Resources