enzyme shallow calls imported function when passed wrong parameters - reactjs

I have a component that I am writing automated tests for. I want to test a method of this component. THe purpose if this test if if the method will call an imported function when passed the wrong parameters.
This is my component (I removed the non-relevant code):
//Util
import { getMenu } from '../utils/request';
const Dashboard = React.createClass({
getInitialState() {
let today = new Date();
if (today.getDay() == 6) {
today.setDate(today.getDate() + 2);
} else if (today.getDay() == 0) {
today.setDate(today.getDate() + 1);
}
return {
selectedDate: today.toISOString().substring(0, 10)
}
},
componentDidMount() {
getMenu(this.state.selectedDate, (data) => {
if (data.error) {
this.setState({
error: data.error
})
} else {
this.setState({
dailyMenu: data.dailyMenu,
loading: false
})
}
})
},
handleDateChange(date) {
var newDate = date.toISOString().substring(0, 10);
if(newDate !== this.state.selectedDate) {
getMenu(newDate, (data) => {
if (data.error) {
this.setState({
error: data.error
})
} else {
this.setState({
dailyMenu: data.dailyMenu,
loading: false,
selectedDate: newDate
})
}
})
}
},
render() {
return (
<MuiThemeProvider>
<div className="Dashboard">
<div className="Dashboard-body">
<Sidebar dateClick={this.handleDateChange}/>
</div>
</div>
</MuiThemeProvider>
)
}
});
module.exports = Dashboard;
I am mocking the getMenu function using jest.mock:
// Components
import Dashboard from '../components/Dashboard';
// Utils
import { getMenu } from '../utils/request';
jest.mock('../utils/request', () => ({getMenu: jest.fn()}))
Then I have written two tests. The first one passes different dates, the second one passes the same date. So in the second case the getMenu mock should not get called. But for both tests I get the result that the function was called one time:
it('handleDateChange should call getMenu when NOT passed state date', ()=> {
const dashboard = shallow(<Dashboard/>);
const today = new Date();
const longAgo = new Date(1).toISOString().substring(0, 10);
dashboard.setState({ selectedDate: longAgo });
dashboard.instance().handleDateChange(today);
expect(getMenu).toHaveBeenCalledTimes(1);
});
it('handleDateChange should NOT call getMenu when passed state date', ()=> {
const dashboard = shallow(<Dashboard/>);
const today = new Date();
const selectedDate = today.toISOString().substring(0, 10);
dashboard.setState({ selectedDate: selectedDate });
dashboard.instance().handleDateChange(today);
expect(getMenu).toHaveBeenCalledTimes(0);
});
So I did some console logs and so far everything seems as expected:
console.log(today.toISOString().substring(0, 10));
console.log(dashboard.state('selectedDate'));
console.log(today.toISOString().substring(0, 10) !== selectedDate);
This outputs:
2017-01-12
2017-01-12
false
What am I doing wrong?

Related

React testing error "Cannot read get property of undefined"

I am trying to test the function searchTrigger in my CardMain component.
export default class CardMain extends Component {
state = {
Pools : [],
loading: false,
}
componentDidMount(){
axios.get('/pools')
.then (res => {
//console.log(res.data.data);
this.setState({
Pools: res.data.data,
loading: true,
message: "Loading..."
},()=>{
if (res && isMounted){
this.setState({
loading: false
});
}
})
}
)
.catch(err=>{
console.log(err.message);
})
}
// the function is for search method
// upon search, this function is called and the state of the pools is changed
searchTrigger = (search) => {
Search = search.toLowerCase();
SearchList = this.state.Pools.filter((e)=> {
if (e.name.toLowerCase().includes(Search)){
this.setState({
loading: false
})
return e
}
})
if (SearchList.length === 0){
this.setState({
loading: true,
message: "No pools found"
})
}
}
render() {
return (
<div>
<Searchbar trigger={this.searchTrigger}/>
{ this.state.loading ?
<div className="d-flex justify-content-center">{this.state.message}</div>
:<div>
{Search === "" ? <Card1 pools={this.state.Pools}/> : <Card1 pools={SearchList}/> }
</div>
}
</div>
)
}
}
The function searchTrigger is passed to another class component called Searchbar which basically displays the search bar. Upon searching something, the function searchTrigger is called and the searched value is passed as an argument to this function.
So, I am trying to test this function and I am new to react and testing. I found some examples online and tried a simple testing whether the function is called or not. My CardMain.test.js code looks like this:
describe("callback function test", ()=> {
it("runs it", () => {
//const spy = jest.spyOn(CardMain.prototype,"searchTrigger");
const cardmain = shallow(<CardMain/>)
const spy = jest.spyOn(cardmain.instance(), "searchTrigger");
expect(spy).toHaveBeenCalled()
})
});
I get the TypeError: Cannot read property 'get' of undefined pointing to the axios.get("/pools") in the CardMain component inside componentDidMount. axios is being imported from another component api.js which creates the instance of axios using axios.create. I have no idea what the problem is. I am very new to react. I have absolutely no idea, how do I test these components? Could somebody help me?
Update:
So, i tried mocking axios call:
let Wrapper;
beforeEach(() => {
Wrapper = shallow( <CardMain/>);
});
describe("Card Main", ()=> {
it("returns data when called", done => {
let mock = new MockAdapter(axios);
const data = [{
name: "Test",
response: true
}];
mock.onGet('My_URL')
.reply(200,data);
const instance = Wrapper.instance();
instance.componentDidMount().then(response => {
expect(response).toEqual(data);
done();
});
});
});
It says "cannot read property .then of undefined"

React : Prop not updated inside of a map function

Edit : Codesandbox here
Here is a simplified version on my parent component :
export default function Parent() {
// State
const [status, setStatus] = useState(1);
const [source, setSource] = useState('https://packagingeurope.com/downloads/7475/download/danone-05.03.20.jpg');
const [steps, setSteps] = useState([
{
title: 'Prediction Initiated',
value: 1,
loading: false,
completed: false,
active: false,
},
{
title: 'Prediction in Progress',
value: 2,
loading: false,
completed: false,
active: false,
},
{
title: 'Prediction Finished',
value: 3,
loading: false,
completed: false,
active: false,
},
]);
useEffect(() => {
if (status) {
const newSteps = steps;
newSteps[status - 1].active = true;
if (status > 1) {
newSteps[status - 2].completed = true;
}
if (status === 3) {
newSteps[status - 1].active = false;
newSteps[status - 1].completed = true;
}
setSteps(newSteps);
} else {
// Do nothing
console.log('No status match');
}
},
[status]);
return (
<div className="container-fluid">
<Timeline status={status} steps={steps} source={source} />
</div>
);
}
And here is my child component :
export default class Timeline extends React.Component {
renderSteps() {
const { steps } = this.props;
return steps.map((step, index) => {
console.log(step);
console.log(steps);
return (
<div key={`test${index}`}>
{step.title}
{step.active && <span>is active</span>}
{step.loading && <span>is loading</span>}
{step.completed && <span>is completed</span>}
</div>
);
});
}
render() {
const { status, steps, source } = this.props;
return (
<div className="timeline">
{this.renderSteps()}
</div>
</>
);
}
}
When i console.log steps props inside my child component, I see that they are correctly updated. (When status === 1, the first step active property is true)
But when i console.log step (inside my map function), none of the properties are updated. (When status === 1, the first step active property is false)
You can see on the capture below that something is not right (I already encountered this problem but unfortunately I can't remember how I solved it, and I am wondering if it's because of the useEffect hooks which I didn't use before this project.
Thank you for your help
Edit : Codesandbox here
Ok, I figured it out.
Since I got this code from another developer, I didn't understand everything that was implemented. I went to the React documentation to understand where the problem was coming from.
By passing a second parameter to the useEffect function, React can skip the effect and will not re-render the component for performance issues if there is not any change. As my status doesn't change (I change it manually for the moment), my component was not refreshing.
By adding [steps] width [status] inthe second parameter, everything works.
useEffect(() => {
if (status) {
const newSteps = steps;
newSteps[status - 1].active = true;
if (status > 1) {
newSteps[status - 2].completed = true;
}
if (status === 3) {
newSteps[status - 1].active = false;
newSteps[status - 1].completed = true;
}
setSteps(newSteps);
} else {
// Do nothing
console.log('No status match');
}
}, [status, steps]);

Why is not entering componentDidUpdate()?

Hello I'm trying to test a state that is changed in the componentDidUpdate but is not enetering.
Code
componentDidUpdate (newProps) {
const { dataSource } = newProps
// set value for nextButtonDisabled in first results async load
if (dataSource.length) {
const newPaginationInfo = Object.assign({}, this.state.paginationInfo)
newPaginationInfo.nextButtonDisabled = dataSource.length <= this.pageSize
this.setState({ paginationInfo: newPaginationInfo }) /* eslint-disable-line react/no-did-update-set-state */
}
}
State
this.state = {
paginationInfo: {
currentPage: 0,
nextButtonDisabled: true
}
}
And the test
it('should set nextButtonDisabled to false when gets new props.datasource if datasource length <= 20', () => {
const component = shallow(<VehicleHistoryTable {...makeProps()} />)
component.setProps({ dataSource: createDataSourceMock(3) })
expect(component.instance().state.paginationInfo.nextButtonDisabled).toEqual(true)
})
The function createDataSourceMock() creates an array of numbers, in this case 3 rows.
Any suggestions?
P:S I'm trying to migrate to React 17

React "this hasn't been initialised - super() hasn't been called " only in Build

I'm working on a react app base con create-react-app, the app works well on dev server but when I run the build something happen and the app not working.
I use a HOC with some function as context, the function declared in the context (HOC) not working because this is not declared.
Everything work fine on dev, if for test before the build I comment
this.getProducts();
on componentDidMount the problems move forward on the next function that use this.
Someone can help me? Thanks in advance.
const GlobalContext = React.createContext()
class GlobalProvider extends React.Component {
constructor(props) {
super(props)
this.loadingToggle = ( status = null, where = '' ) => {
// enable and disable loading
}
this.loginFunction = (e, utente_id, password) => {
// rest api login
}
this.logoutFunction = () => {
// logout
}
this.getProducts = () => {
this.forceUpdate();
this.loadingToggle(true, "getProducts");
// HERE THE PROBLEMS
var _this = this;
axios.post(Config.apiBaseUrl + '/users/products', {
token: localStorage.getItem('token')
})
.then( (response) => {
if (response.data.success !== true ){
// user not exist
}else{
// populate user data
// HERE I USE _this
}
})
.catch(function (error) {
// catch error
});
}
this.cartVariation = (id, qty, minQty = 0) => {
// cart action
}
this.sendOrder = (addressId) => {
// send order
}
this.state = {
isAuth: false,
loginFunction: this.loginFunction,
logoutFunction: this.logoutFunction,
cartVariation: this.cartVariation,
removeCart: this.removeCart,
cart: null,
forceUpdate: this.forceUpdate,
lastUpdate: new Date().getTime(),
cartCount: JSON.parse(localStorage.getItem("mf-cart")) !== null ? Object.keys( JSON.parse(localStorage.getItem("mf-cart"))).length : 0,
loadingToggle: this.loadingToggle,
loading: false,
store : {
mf_product_list : [],
mf_categories : [],
mf_users : [],
mf_users_formatted : [],
mf_backorders : [],
mf_backorders_list : [],
mf_address : []
},
sendOrder: this.sendOrder
}
}
componentDidMount () {
if (localStorage.getItem('token') !== null && localStorage.getItem('token-timestamp') !== null ){
this.setState({isAuth : true});
}
this.getProducts();
}
render() {
return (
<GlobalContext.Provider
value={{
isAuth: this.state.isAuth,
authToken: null,
loginFunction: this.state.loginFunction,
logoutFunction: this.state.logoutFunction,
cartVariation: this.state.cartVariation,
removeCart: this.state.removeCart,
cart: null,
forceUpdate: this.state.forceUpdate,
lastUpdate: this.state.lastUpdate,
cartCount: this.state.cartCount,
loading: this.state.loading,
store: this.state.store,
sendOrder: this.sendOrder
}}
>
{this.props.children}
</GlobalContext.Provider>
)
}
}
const GlobalConsumer = GlobalContext.Consumer
export { GlobalProvider, GlobalConsumer }
Change the function binding from Class properties
this.getProducts = () => {
// ...
}
to bind it in the constructor (ES2015)
constructor(props) {
// ...
this.getProducts = this.getProducts.bind(this);
}
More information about function binding is here.

How to trigger handleChange event from inside the function using jquery and react?

I am using fullcalendar-scheduler plugin for following calendar. Currently I have integrated it with react and rails. In order to change the positions of the element I have called the select function from inside viewRender function of fullCalendar instead of render on react. On this case how do we change state when select option is changed and fetch the data again from api?
import React from "react";
import PropTypes from "prop-types";
import axios from "axios";
class TestCalendar extends React.Component {
constructor(props) {
super(props);
this.state = {
cars: [],
events: [],
price: [],
selectDates: [],
startDate: moment(),
endDate: moment().add(3, 'years')
}
}
componentDidMount() {
const headers = {
'Content-Type': 'application/json'
}
axios.get('/api/v1/test_calendars?date_from=' + this.state.startDate.format(), { headers: headers })
.then(res => {
const cars = res.data;
this.setState({ cars });
});
axios.get('/api/v1/test_calendars/events?date_from=' + this.state.startDate.format(), { headers: headers })
.then(res => {
const events = res.data;
this.setState({ events });
});
axios.get('/api/v1/test_calendars/prices?date_from=' + this.state.startDate.format(), { headers: headers })
.then(res => {
const price = res.data;
this.setState({ price });
});
this.updateEvents(this.props.hidePrice);
}
componentDidUpdate() {
console.log('componentDidUpdate');
this.updateEvents(this.props.hidePrice);
console.log(this.state.cars);
}
componentWillUnmount() {
$('#test_calendar').fullCalendar('destroy');
};
handleChange(e) {
debugger;
}
updateEvents(hidePrice) {
function monthSelectList() {
let select = '<div class="Select select-me"><select id="months-tab" class="Select-input">' +
'</select></div>'
return select
}
function getDates(startDate, stopDate) {
var dateArray = [];
while(startDate.format('YYYY-MM-DD') <= stopDate.format('YYYY-MM-DD')) {
dateArray.push(startDate.format('YYYY-MM'));
startDate = startDate.add(1, 'days');
};
return dateArray;
}
$('#test_calendar').fullCalendar('destroy');
$('#test_calendar').fullCalendar({
selectable: false,
defaultView: 'timelineEightDays',
defaultDate: this.props.defaultDate,
views: {
timelineEightDays: {
type: 'timeline',
duration: { days: 8 },
slotDuration: '24:00'
}
},
header: {
left: 'prev',
right: 'next'
},
viewRender: function(view, element) {
let uniqueDates;
$("span:contains('Cars')").empty().append(
monthSelectList()
);
$("#months-tab").on("change", function() {
let index, optionElement, month, year, goToDate;
index = this.selectedIndex;
optionElement = this.childNodes[index];
month = optionElement.getAttribute("data-month");
year = optionElement.getAttribute("data-year");
goToDate = moment([year, (month - 1), 1]).format("YYYY-MM-DD");
$("#test_calendar").fullCalendar('gotoDate', moment(goToDate));
$("#months-tab").find("option[data-month=" + month + "][data-year=" + year + "]").prop("selected", true);
this.handleChange.bind(this)
});
let dates = getDates(moment(), moment().add(3, "years"));
uniqueDates = [...new Set(dates)];
$('#months-tab option').remove();
$.each(uniqueDates, function(i, date) {
$('#months-tab').append($('<option>', {
value: i,
text: moment(date).format('MMMM') + " " + moment(date).format('YYYY'),
'data-month': moment(date).format('MM'),
'data-year': moment(date).format('YYYY'),
}));
});
},
resources: this.state.cars,
resourceRender: function(resourceObj, labelTds, bodyTds) {
labelTds.css('background-image', "url(" + resourceObj.header_image + ")");
labelTds.css('background-size', "160px 88px");
labelTds.css('background-repeat', "no-repeat");
labelTds.css("border-bottom", "1px solid");
labelTds.addClass('resource-render');
labelTds.children().children().addClass("car-name");
},
resourceLabelText: 'Cars',
dayClick: function(date, jsEvent, view, resource) {
},
dayRender: function(date, cell){
cell.addClass('dayrender');
},
select: function(startDate, endDate, jsEvent, view, resource) {
},
events: this.state.events.concat(this.state.price),
eventRender: function(event, element, view){
},
schedulerLicenseKey: 'CC-Attribution-NonCommercial-NoDerivatives'
});
// Should stay after full component is initialized to avoid fc-unselectable class on select tag for months
$("#months-tab").on("mousedown click", function(event){event.stopPropagation()});
$(".prev-link").on("click", function(event){event.stopPropagation()});
$(".next-link").on("click", function(event){event.stopPropagation()});
}
render () {
return (
<div id='test_calendar'>
</div>
);
}
}
export default TestCalendar;
Here your onchange callback doesn't have the react component context so you cannot change the state without giving access to the proper context. One solution I may quickly suggest is to change your updateEvents function like bellow. I have only kept the changed code.
updateEvents(hidePrice) {
let context = this;
... // your code
$('#test_calendar').fullCalendar({
... // your code
viewRender: function(view, element) {
... // your code
$("#months-tab").on("change", function() {
... // your code
// Call the handleChange with the context.
context.handleChange.bind(context)(this); // edited here
});
... // your code
});
... // your code
}
Then you will be able to call the setState method from the handleChange function.
You must be facing an issue with the this reference, as you are trying to access the method handleChange which is associated with the component this but you are using the normal function for viewRender instead you should use arrow function
see the updated code below, it will solve the issue,
updateEvents(hidePrice) {
$('#test_calendar').fullCalendar({
...
viewRender: (view, element) => { // convert to arrow function so, this (component instance) will be accessible inside.
// store the reference of this (component instance).
const $this = this;
$("#months-tab").on("change", function (e) {
...
// always bind methods in constructor.
$this.handleChange(e);
...
});
},
...
});
}
thanks.

Resources