React/enzyme: How to test for subcomponent? - javascript

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

Related

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

React Test cases with Jest and Enzyme

I am new to writing test cases for React. Can someone tell me how to proceed with writing test cases for this file and how to finish code coverage.
How do i test mapDispatchToProps, componentDidMount or handleClick functions below. Can someone explain me how to proceed with steps to achieve test cases.
import React, { Component } from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import DOMPurify from 'dompurify'
import escape from 'escape-html'
import Message from 'wf-dbd-react-ui/es/Message'
import ContentEventWrapper from 'wf-dbd-react-ui/es/ContentEventWrapper'
import { unescapeHtml } from 'wf-dbd-react-ui/es/lib'
import { requestNavigation } from 'wf-dbd-react-ui/es/actions'
import NavigationItemRecord from 'wf-dbd-react-ui/es/lib/records/NavigationItemRecord'
import ScrollToTopOnMount from 'wf-dbd-react-ui/es/ScrollToTopOnMount'
class MessageDisplayWithSpecialCharacters extends Component { //NOSONAR
constructor(props) {
super(props)
this.elementRef = null
}
componentDidMount() {
if (this.props.focusOnMount) {
if (this.elementRef) {
this.elementRef.blur() //needed to reset focus in iOS
this.elementRef.focus()
setTimeout(() => { this.elementRef.focus() }, 100) //timeout needed for Android
}
}
}
setElementRef = element => {
this.elementRef = element
}
handleClick = ({ target }) => {
const { requestNavigation } = this.props
if (target.hasAttribute('data-cui-link')) {
const navigationItem = new NavigationItemRecord({
samlNavigation: true,
displayType: 'saml',
navigationUrl: target.getAttribute('data-cui-link')
})
requestNavigation(navigationItem)
}
}
render() {
const { messages, className } = this.props
return (
<div className={className} tabIndex="-1" ref={this.setElementRef}>
<ScrollToTopOnMount />
{messages.map((message, index) => {
const purifiedContent = { __html: DOMPurify.sanitize(unescapeHtml(JSON.parse(`"${escape(window.decodeURIComponent(message.get('message')))}"`))) }
return (
<ContentEventWrapper handleContentClick={this.handleClick} key={index}>
<Message announce={true} level={message.get('level')}>
<p dangerouslySetInnerHTML={purifiedContent} />
</Message>
</ContentEventWrapper>
)
})}
</div>
)
}
}
MessageDisplayWithSpecialCharacters.propTypes = {
messages: PropTypes.array,
className: PropTypes.string,
focusOnMount: PropTypes.bool,
requestNavigation: PropTypes.func
}
const mapDispatchToProps = dispatch => ({
requestNavigation: navigationItem => dispatch(requestNavigation(navigationItem))
})
export default connect(null, mapDispatchToProps)(MessageDisplayWithSpecialCharacters)
Any help/ advice is appreciated for a novice like me.

Enzyme is not testing onChange method

I'm trying to test, by passing mock data the on change method for App.test.js, however im getting the following error.
● Should handle onChange event › should handle onChange event
expect(received).toEqual(expected)
Expected value to equal:
"Owl"
Received:
undefined
Difference:
Comparing two different types of values. Expected string but received undefined.
i checked out a similar post
onChange - Testing using Jest Enzyme - check?, however their wasn't an answer that could help
App.js
import React, {Component} from 'react';
import logo from './logo.svg';
import './App.css';
import Card from './Card';
import PropTypes from "prop-types";
const Styles = {
marginTop: '100px',
inputStyle: {
borderRadius: '0px',
border: 'none',
borderBottom: '2px solid #000',
outline: 'none',
focus: 'none'
}
}
class App extends Component {
constructor(props) {
super(props);
this.state = {
query: '',
title: undefined,
url: undefined
}
this.onChange = this.onChange.bind(this);
}
onChange(e) {
this.setState({query: e.target.value})
}
getGIY = async(e) => {
e.preventDefault();
const { query } = this.state;
await fetch(`http://api.giphy.com/v1/gifs/search?q=${query}&api_key=iBXhsCDYcnktw8n3WSJvIUQCXRqVv8AP&limit=5`)
.then(response => response.json())
.then(({ data }) => {
this.setState({
title: data[0].title,
url: data[0].images.downsized.url
});
})
.catch(console.log);
}
render() {
return (
<div className="col-md-6 mx-auto" style={Styles}>
<h1 className="gif-title">Random GIF fetch</h1>
<form className="form-group" onSubmit={this.getGIY}>
<input
style={Styles.inputStyle}
className="form-control"
type="text"
name="query"
onChange={this.onChange}
placeholder="Search GIF..."/>
<button type="submit" className="btn btn-primary mt-4">Get GIF</button>
</form>
<Card title={this.state.title} url={this.state.url}/>
</div>
);
}
}
PropTypes.propTypes = {
onChange: PropTypes.func.isRequired,
getGIY:PropTypes.func.isRequired,
title:PropTypes.string.isRequired,
url:PropTypes.string.isRequired
}
export default App;
App.test.js
import React from 'react';
import ReactDOM from 'react-dom';
import {shallow} from 'enzyme';
import App from './App';
describe('Should render App Component', ()=> {
it('should render app component', ()=> {
const component = shallow(<App />);
})
})
describe('Should have h1 title', ()=> {
it('Should show Random GIF fetch', ()=>{
const component = shallow(<App/>);
expect(component.find("h1.gif-title")).toHaveLength(1);
expect(component.find("h1.gif-title").text()).toContain("Random GIF fetch")
})
})
describe('Should handle onChange event', ()=> {
it('should handle onChange event', ()=> {
const component = shallow(<App/>)
const form = component.find('input')
form.props().onChange({
target:{
title: 'Owl',
query: 'Owl',
url: 'https://media.giphy.com/media/qISaMW1xwmvNS/giphy.gif'
}
});
expect(component.state('query')).toEqual('Owl')
})
})
Your event handler sets the state based on e.target.value:
onChange(e) {
this.setState({query: e.target.value})
}
...but you're not passing anything for target.value in your mocked event.
Change your test to this:
describe('Should handle onChange event', ()=> {
it('should handle onChange event', ()=> {
const component = shallow(<App/>)
const form = component.find('input')
form.props().onChange({
target:{
value: 'Owl'
}
});
expect(component.state('query')).toEqual('Owl') // Success!
})
})
...and it should work.

mapStateToProps is not defined

1.Can someone help me where i made a thing wrong?
2.the component i am mapping the state to its properties but i still get this
error"mapStateToProps is not defined"
this is the whole component below. the error reads "mapStateToProps not defined"
import React, {Component} from 'react';
import Icon from 'react-native-vector-icons/EvilIcons';
import { loadInitialPosts} from './actions';
import {connect } from 'react-redux';
import _ from 'lodash';
import {View, StyleSheet,FlatList} from 'react-native';
import PostItem from './PostItem';
import PostDetail from './PostDetail';
class PostsList extends Component {
componentWillMount() {
this.props.loadInitialPosts();
}
renderItem({item}){
return <PostItem posts = { item } />;
}
renderInitialView(){
if(this.props.postDetailView === true){
return(
<PostDetail />
);
}
else{
return(
<FlatList
data={this.props.posts}
renderItem={this.renderItem} />
)}
}
render(){
return(
<View style={styles.list}>
{this.renderInitialView()}
</View>
);
}
}
const mapStateToProps = state => {
const posts = _.map(state.posts, (val, id) =>
{
return { ...val, id};
});
return{
posts: posts,
postDetailView: state.postDetailView,
};
}
export default connect(mapStateToProps, { loadInitialPosts })(PostsList)
1.This is the action that dispatches the data
export const loadInitialPosts = () => {
return function(dispatch){
return axios.get(apiHost
+"/api/get_posts?
count=20")
.then((response) => {
dispatch({ type:
'INITIAL_POSTS_FETCH', payload:
response.data.posts});
}).catch((err) => {
console.log(err);
});
};
};
mapStateToProps sits outside of the class before export default connect(mapStateToProps)(SomeClass)
class SomeClass extends React.Component {
...
}
const mapStateToProps = state => {
const posts = _.map(state.posts, (val, id) => {
return { ...val,
id
};
});
return {
posts: posts,
postDetailView: state.postDetailView,
};
}
To eliminate the possibility of mapStateToProps being undefined, consider defining the mapStateToProps directly in the call to connect() like this:
class PostsList extends React.Component {
componentWillMount() {
this.props.loadInitialPosts();
}
renderItem({item}){
return <PostItem posts = { item } />;
}
renderInitialView(){
if(this.props.postDetailView === true){
return <PostDetail />;
}
else{
return <FlatList
data={this.props.posts}
renderItem={this.renderItem} />
}
}
render(){
return(<View style={styles.list}> {this.renderInitialView()} </View>);
}
}
/*
Avoid declaration of mapStateToProps object by defining this object
directly in the call to connect()
*/
export default connect((state => {
return {
posts : state.posts.map((val, id) => ({ ...val, id })),
postDetailView: state.postDetailView,
}
}), { loadInitialPosts })(PostsList)

ReactJS - how to update data for an attribute in props?

I have a list of posts and for every post, there's is a button that will display a modal window with a confirmation if the user really wants to delete the respective post.
When the user confirms, data are send to backend, there's delete the respective post and back to ReactJS is returned a set of all posts. But when try to update the list of posts on the front-end, I get this error:
Posts.jsx:61 Uncaught (in promise) TypeError: _this2.props.posts is not a function
This error is raised on this line:
this.props.posts(res.data);
Home.jsx
import React from "react";
import Posts from './Posts';
import NewPost from './NewPost';
import axios from 'axios';
import Moment from 'react-moment';
import LoadModal from './LoadModal';
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
posts: [],
loading: true
};
}
componentDidMount() {
axios.get('/posts')
.then(response => {
console.log('---');
console.log(response.data);
console.log('---');
this.setState({ posts: response.data, loading: false });
});
}
render() {
return (
<div>
<Posts posts={this.state.posts} loading={this.state.loading} />
</div>
)
}
}
export default Home
Posts.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';
import {Collapse} from 'react-collapse';
import classNames from "classnames";
import Dialog from 'react-bootstrap-dialog';
class Posts extends React.Component {
constructor(props) {
super(props);
this.state = {
activeIndex: null,
removePostBtn: 'Remove'
}
}
onClick(post_id) {
this.dialog.show({
title: 'Remove Post - #'+post_id,
body: 'Do you really want to remove this post?',
actions: [
Dialog.CancelAction(),
Dialog.DefaultAction(
this.state.removePostBtn,
() => {
this.setState({ removePostBtn: 'Removing...' }, () => {
axios.get('/remove_post/'+post_id, { post_id: post_id })
.then(res => {
this.props.posts(res.data); // here's the error
})
})
},
'btn-danger'
)
],
})
}
render () {
let content;
const { activeIndex } = this.state;
const Button = require('react-bootstrap').Button;
if (this.props.loading) {
content = 'Loading...';
} else {
content = this.props.posts.map((post, index) => {
return(
<li key={index}>
<div>
<span>{post.id}</span>
<span>{post.message}</span>
<Button onClick={() => this.onClick(post.id)}>Show alert</Button>
<Dialog ref={(el) => { this.dialog = el }} />
</div>
</li>
)
});
}
return (
<div>
<h1>Posts!</h1>
<div className="row">
<div className="col-md-6">
<ul>
{content}
</ul>
</div>
</div>
</div>
);
}
}
export default Posts
How do I properly update the props with posts?
You can't directly update any props. You need to create an update handler in the parent component that will update this.state.posts:
import React from "react";
import Posts from './Posts';
import NewPost from './NewPost';
import axios from 'axios';
import Moment from 'react-moment';
import LoadModal from './LoadModal';
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
posts: [],
loading: true
};
}
componentDidMount() {
this.getPosts();
}
getPosts = () => {
axios.get('/posts')
.then(response => {
console.log('---');
console.log(response.data);
console.log('---');
this.setState({ posts: response.data, loading: false });
});
}
updatePosts = posts => {
this.setState({ posts });
}
render() {
return (
<div>
<Posts posts={this.state.posts} loading={this.state.loading} getPosts={this.getPosts} updatePosts={this.updatePosts} />
</div>
)
}
}
export default Home
import React from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';
import {Collapse} from 'react-collapse';
import classNames from "classnames";
import Dialog from 'react-bootstrap-dialog';
class Posts extends React.Component {
constructor(props) {
super(props);
this.state = {
activeIndex: null,
removePostBtn: 'Remove'
}
}
onClick(post_id) {
this.dialog.show({
title: 'Remove Post - #'+post_id,
body: 'Do you really want to remove this post?',
actions: [
Dialog.CancelAction(),
Dialog.DefaultAction(
this.state.removePostBtn,
() => {
this.setState({ removePostBtn: 'Removing...' }, () => {
axios.get('/remove_post/'+post_id, { post_id: post_id })
.then(res => {
//this.props.posts(res.data); // here's the error
// Call parent function to re-retch posts
this.props.getPosts();
// Or direclty pass data to update the parent state
this.props.updatePosts(res.data);
})
})
},
'btn-danger'
)
],
})
}
render () {
let content;
const { activeIndex } = this.state;
const Button = require('react-bootstrap').Button;
if (this.props.loading) {
content = 'Loading...';
} else {
content = this.props.posts.map((post, index) => {
return(
<li key={index}>
<div>
<span>{post.id}</span>
<span>{post.message}</span>
<Button onClick={() => this.onClick(post.id)}>Show alert</Button>
<Dialog ref={(el) => { this.dialog = el }} />
</div>
</li>
)
});
}
return (
<div>
<h1>Posts!</h1>
<div className="row">
<div className="col-md-6">
<ul>
{content}
</ul>
</div>
</div>
</div>
);
}
}
export default Posts

Categories

Resources