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