Unit testing React click outside component - javascript

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();
})

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;

How can I pass parameter to event listener in react

I have component like this:
import React, { useEffect } from 'react';
const WindowFocusHandler = ({store}) => {
// here store object is valid and work
useEffect(() => {
window.addEventListener('focus', () => {
console.log('Tab is in focus... but no store object here', store);
});
});
return <></>;
};
export default WindowFocusHandler;
And I am trying to pass store object inside listener function but failed what ever I try.
Try this:
import React, { useEffect } from 'react';
const WindowFocusHandler = ({ store }) => {
useEffect(() => {
const fn = () => {
console.log('Tab is in focus...', store);
};
window.addEventListener('focus', fn);
return () => window.removeEventListener('focus', fn);
}, [store]);
return <></>;
};
export default WindowFocusHandler;

Jest - Call React Component function without Enzyme

I am writing a Test for a React Component Product. I am using plain simple Jest without react-renderer or enzyme and I am aiming to keep it this way for the time being. I need to test a function of a component and havent been able to call it directly through jest. Code given below.
Component:
import React, { Component } from 'react';
class Product extends Component {
state = {
heading: `Old Heading`
};
changeHeading() {
this.setState({ heading: `New Heading` });
}
render() {
return (
<div>
<p data-testid='heading'> {this.state.heading} </p>
</div>
);
}
}
export default Product;
Jest Test:
import React from 'react';
import { render } from 'react-dom';
// import { act } from 'react-dom/test-utils';
import Product from './Product';
let container = null;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
describe(`Testing Product Component`, () => {
it('renders without crashing', () => {
// act(() => {
// render(<Product />, container);
// });
const result = render(<Product />, container);
const heading = container.querySelector("[data-testid='heading']");
console.log(heading);
expect(heading).toBe(`Old Heading`);
result.changeHeading();
expect(heading).toBe(`New Heading`);
ReactDOM.unmountComponentAtNode(div);
});
});
OR
it('renders without crashing', () => {
const productComponent = <Product />;
render(productComponent, container);
const heading = container.querySelector("[data-testid='heading']");
console.log(heading);
expect(heading).toBe(`Old Heading`);
productComponent.changeHeading();
expect(heading).toBe(`New Heading`);
ReactDOM.unmountComponentAtNode(div);
});
But it didn't worked. How can I access the changeHeading function from the component in my jest test? and call it to change the content of <p> tag?
EDIT
I will reside with react-test-library if I have to for the timebeing. But it would be great if someone can explain the internal workings as well.
Thank you.
To test that, you need a user interaction that calls changeHeading(). In your test, when you do const result = render(<Product />, container); you are storing a reference to the component DOM node.
So, you need to modify your component to be able to have an interaction:
import React, { Component } from 'react';
class Product extends Component {
state = {
heading: `Old Heading`
};
changeHeading() {
this.setState({ heading: `New Heading` });
}
render() {
return (
<div>
<p data-testid='heading'> {this.state.heading} </p>
<button onclick={this.changeHeading}></button>
</div>
);
}
}
export default Product;
and your test would be:
import React from 'react';
import { render } from 'react-dom';
import { act } from 'react-dom/test-utils';
import Product from './Product';
let container = null;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
describe(`Testing Product Component`, () => {
it('renders without crashing', async () => {
act(() => {
render(<Product />, container);
});
let heading = container.querySelector("[data-testid='heading']");
expect(heading).toBe(`Old Heading`);
const button = container.querySelector('button');
await act(async () => {
button.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
heading = container.querySelector("[data-testid='heading']");
expect(heading).toBe(`New Heading`);
ReactDOM.unmountComponentAtNode(div);
});
});

Categories

Resources