Jest - Call React Component function without Enzyme - javascript

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

Related

Getting the error as " Cannot read property 'wait' of null" while testing my component containing i18next library using react testing library in react

This is my file which I am trying to test
So this is my about file which contains i18next library beacuse of which I am getting the error as "cannot read property wait of null
import React, { Component } from 'react';
import LoadingImg from '../../assets/images/loader.gif'
import {getChapterData,fetchChapterData,htmlDecode} from '../helpers/common'
import { Trans, withNamespaces } from 'react-i18next';
class AboutReportChapter extends Component {
constructor(props){
super(props)
this.state={
isDataLoaded:false
}
}
render() {
return (
<div className="drChapterContainer aboutChapterReport" id="pills-AboutReportChapter">
{this.state.isDataLoaded?
<div className="aboutReport">
{this.state.chapterDescription ?
<div className="aboutReportHeader flexContainer">
<div className="hideShowAll show unselectable" id="about_HS"><Trans>HIDE_ALL</Trans></div>
</div>
:null
}
</div>
:
<div className="loading" key="loading_key">
<img src={LoadingImg} alt ="Loading"/>
</div>
}
</div>
)
}
componentDidMount= async()=> {
let result = await fetchChapterData('AboutReportChapter')
this.setState({...result});
}
}
export default withNamespaces() (AboutReportChapter);
And this is my testing file
This is my testing file which I am testing using react testing library jest and I am getting null value on rendering the component
import React from 'react'
import About from '../about'
import { render,unmountComponentAtNode} from "react-dom";
import { screen,getByText,getByTestId,queryByText } from '#testing-library/dom'
import {fetchChapterData,htmlDecode} from '../../helpers/common'
import { act } from "react-dom/test-utils";
jest.mock('../../helpers/common', () => ({
fetchChapterData:jest.fn(),
htmlDecode:jest.fn()
}))
let container = null;
beforeEach(() => {
// setup a DOM element as a render target
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
// cleanup on exiting
unmountComponentAtNode(container);
container.remove();
container = null;
});
describe("Introduction component testing",() => {
const fakeData=require('../_Mock Data_/about.json');
fetchChapterData.mockResolvedValue({...fakeData})
it("check if about component is rendered or not", async () => {
await act(async () => {
render(<About t={key => key} />, container);
});
//Assertions
expect(container.getElementsByClassName('aboutReport').length).toBe(1);
expect(container.getElementsByClassName('loading').length).toBe(0);
});
})

unit test react container component

I am trying to unit test React container component using chai/sinon. This is how the component is defined:
import { connect } from 'react-redux';
import React, { Component } from 'react';
class TestComponent extends Component {
componentDidMount() {
const { loadData } = this.props;
loadData();
}
render() {
const { links, heading } = this.props;
return (
<p>
<h1>{heading}</h1>
<div>{links.map(link => link.label)}</div>
</p>
);
}
}
const mapStateToProps = state => ({
heading: state.heading,
links: state.links
});
const mapDispatchToProps = dispatch => ({
loadData: dispatch('LOAD_DATA')
});
export default connect(mapStateToProps, mapDispatchToProps)(TestComponent);
I am writing a unit test to assert that container component has the props loadData, heading and links. Below is the unit test:
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import React from 'react';
import { shallow } from 'enzyme';
import TestContainer from './TestContainer';
const mockStore = configureStore({});
describe('TestContainer', () => {
const initialState = {
heading: 'A test component',
links: []
};
const store = mockStore(initialState);
it(
'renders correctly',
sinon.test(() => {
const component = shallow(
<Provider store={store}>
<TestContainer />
</Provider>
);
console.log(component.prop('heading')); // prints undefined
expect(component.prop('heading')).to.equal('A test component'); // gives AssertionError: expected undefined to equal 'A test component'
})
);
});
Above unit test fails and I get all props as undefined. Can somebody explain what am I doing wrong OR how a container component props should be asserted?
Thanks.

react-dom - An update to ... inside a test was not wrapped in act(...)

I have the following component that loads data from an external API using the useEffect hook on mount:
import React, { useEffect, useState } from "react";
import Dropdown from "./common/Dropdown";
import { getWeapons } from "../services/gear";
import { DropdownOptionType } from "./common/Dropdown";
const EMPTY_OPTION = {
key: "0",
label: "",
value: "null"
};
const Gear = () => {
const [weaponOptions, setWeaponOptions] = useState([EMPTY_OPTION]);
const [weapon, setWeapon] = useState("null");
useEffect(() => {
const fetchWeaponsOptions = async (): Promise<void> => {
const weaponsData = await getWeapons();
const newWeaponOptions: DropdownOptionType[] = [
EMPTY_OPTION,
...weaponsData.map(({ id, name }) => {
return {
key: id,
label: name,
value: id
};
})
];
setWeaponOptions(newWeaponOptions);
};
fetchWeaponsOptions();
}, []);
// TODO add weapon dropdown on change, selected weapon state
const handleWeaponChange = ({ value }: DropdownOptionType): void => {
setWeapon(value);
};
return (
<div>
<h2>Gear:</h2>
<Dropdown
defaultValue={weapon}
label="Weapon"
name="weapon"
options={weaponOptions}
onChange={handleWeaponChange}
/>
</div>
);
};
export default Gear;
I am mounting this component inside my App Component:
import React from "react";
import Stats from "./components/Stats";
import Gear from "./components/Gear";
const App = () => {
return (
<div className="App">
<h1>Untitled combat game character creator</h1>
<Stats />
<Gear />
</div>
);
};
export default App;
I have the following test for my App component:
import React from "react";
import { render } from "#testing-library/react";
import App from "./App";
test("renders app title", () => {
const { getByText } = render(<App />);
const titleElement = getByText(/Untitled combat game character creator/i);
expect(titleElement).toBeInTheDocument();
});
The test is passing but outputs the following warning:
PASS src/App.test.tsx ● Console
console.error node_modules/react-dom/cjs/react-dom.development.js:530
Warning: An update to Gear inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at ...
in Gear (at App.tsx:11)
in div (at App.tsx:8)
in App (at App.test.tsx:6)
I tried wrapping the test code inside the act callback, but I am getting the same warning:
test("renders app title", () => {
act(() => {
const { getByText } = render(<App />);
const titleElement = getByText(/Untitled combat game character creator/i);
expect(titleElement).toBeInTheDocument();
});
});
And:
test("renders app title", () => {
let titleElement;
act(() => {
const { getByText } = render(<App />);
titleElement = getByText(/Untitled combat game character creator/i);
});
expect(titleElement).toBeInTheDocument();
});
But I am getting the same warning in both cases. What would be the correct way to handle this?

React/enzyme: How to test for subcomponent?

This is how my Messenger Component looks like. As you can see there is the main component and a list component. The main component is exported as default.
With this everything is working as expected in my application.
/imports/ui/components/messenger.jsx
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Container, Segment, Loader, Header } from 'semantic-ui-react'
class Messenger extends Component {
static get propTypes () {
return {
data: PropTypes.array,
articleId: PropTypes.string,
isLoading: PropTypes.bool
}
}
render () {
const { data, articleId, isLoading } = this.props
if (isLoading) { return (<Loader active inverted size='massive' className='animated fadeIn' />) }
if (articleId) { return (<MessengerList data={data} articleId={articleId} />) }
return (
<Container>
<Segment id='' className='m-b-1'>
<Header as='h1'>Title</Header>
<MessengerList data={data} />
</Segment>
</Container>
)
}
}
class MessengerList extends Component {
/* ... */
}
export default Messenger
Now I would like to do some unit testing for the main component using enzyme. This is how I am doing it, but the last test is failing as MessengerList is not defined. So how should this be done.
import React from 'react'
import { expect } from 'meteor/practicalmeteor:chai'
import { shallow } from 'enzyme'
import { Container, Loader } from 'semantic-ui-react'
import Messenger from '/imports/ui/components/messenger.jsx'
describe('<Messenger />', () => {
const defaultProps = {
data: [],
articleId: '',
isLoading: true
}
it('should show <Loader /> while loading data', () => {
const wrapper = shallow(<Messenger {...defaultProps} />);
expect(wrapper.exists()).to.be.true
expect(wrapper.find(Loader).length).to.equal(1)
})
it('should show <Container /> data has been loaded', () => {
defaultProps.isLoading = false
const wrapper = shallow(<Messenger {...defaultProps} />);
expect(wrapper.find(Container).length).to.equal(1)
expect(wrapper.find(Loader).exists()).to.be.false
})
it('should show <MessengerList /> if articleID is given', () => {
defaultProps.articleID = 'dummy'
defaultProps.isLoading = false
const wrapper = shallow(<Messenger {...defaultProps} />);
expect(wrapper.find(MessengerList).length).to.equal(1)
expect(wrapper.find(Loader).exists()).to.be.false
})
})
I do not want to change export default Messenger
Export your MessengerList class ....
export class MessengerList extends Component {
/* ... */
}
And then in the test do ....
import React from 'react'
import { expect } from 'meteor/practicalmeteor:chai'
import { shallow } from 'enzyme'
import { Container, Loader } from 'semantic-ui-react'
import Messenger, { MessengerList } from '/imports/ui/components/messenger.jsx';
describe('<Messenger />', () => {
let wrapper;
const defaultProps = {
data: [],
articleId: '',
isLoading: true
}
beforeEach(() => {
// render the component once up here in this block. It runs before each test.
wrapper = shallow(<Messenger {...defaultProps} />);
});
it('should show <Loader /> while loading data', () => {
expect(wrapper.exists()).to.be.true
expect(wrapper.find(Loader).length).to.equal(1)
});
it('should show <Container /> data has been loaded', () => {
defaultProps.isLoading = false
expect(wrapper.find(Container).length).to.equal(1)
expect(wrapper.find(Loader).exists()).to.be.false
});
it('should show <MessengerList /> if articleID is given', () => {
defaultProps.articleID = 'dummy'
defaultProps.isLoading = false
expect(wrapper.find(MessengerList).length).to.equal(1);
expect(wrapper.find(Loader).exists()).to.be.false
});
});
UPDATE
Ideally, you should state that a prop is being modified first ...
...
describe('and the data has loaded', () => {
beforeEach(() => {
defaultProps.isLoading = false;
});
it('should show <Container /> component', () => {
expect(wrapper.find(Container).length).to.equal(1)
expect(wrapper.find(Loader).exists()).to.be.false
});
});
...

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