Testing child component props after parent update state using set - javascript

I have a following functional component which send some filtered data to the child component. Code is working fine i.e I can run app and see the components being render with right data. But the test I have written below is failing for ChildComponent. Instead of getting single array element with filtered value it is getting all three original values.
I am confused as similar test for FilterInputBox component for props filterValue is passing. Both tests are checking the updated props value after same event filter input change i.e handleFilterChange.
Am I missing anything? Any suggestion?
Source Code
function RootPage(props) {
const [filterValue, setFilterValue] = useState(undefined);
const [originalData, setOriginalData] = useState(undefined);
const [filteredData, setFilteredData] = useState(undefined);
const doFilter = () => {
// do something and return some value
}
const handleFilterChange = (value) => {
const filteredData = originalData && originalData.filter(doFilter);
setFilteredData(filteredData);
setFilterValue(value);
};
React.useEffect(() => {
async function fetchData() {
await myService.fetchOriginalData()
.then((res) => {
setOriginalData(res);
})
}
fetchData();
}, [props.someFlag]);
return (
<>
<FilterInputBox
filterValue={filterValue}
onChange={handleFilterChange}
/>
<ChildComponent
data={filteredData}
/>
</>
);
}
Test Code
describe('RootPage', () => {
let props,
fetchOriginalDataStub,
useEffectStub,
originalData;
const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
beforeEach(() => {
originalData = [
{ name: 'Brown Fox' },
{ name: 'Lazy Dog' },
{ name: 'Mad Monkey' }
];
fetchOriginalDataStub = sinon.stub(myService, 'fetchOriginalData').resolves(originalData);
useEffectStub = sinon.stub(React, 'useEffect');
useEffectStub.onCall(0).callsFake((f) => f());
props = { ... };
});
afterEach(() => {
sinon.restore();
});
it('should send filtered data', async () => {
const renderedElement = enzyme.shallow(<RootPage {...props}/>);
const filterBoxElement = renderedElement.find(FilterInputBox);;
await flushPromises();
filterBoxElement.props().onChange('Lazy');
await flushPromises();
//This "filterValue" test is passing
const filterBoxWithNewValue = renderedElement.find(FilterInputBox);
expect(filterBoxWithNewValue.props().filterValue).to.equal('Lazy');
//This "data" test is failing
const childElement = renderedElement.find(ChildComponent);
expect(childElement.props()).to.eql({
data: [
{ name: 'Lazy Dog' }
]
});
});
});
UPDATE After putting some log statements I am seeing that when I am calling onChange originalData is coming undefined. Not sure why that is happening that seems to be the issue.
Still looking for help if anyone have any insight on this.

Related

Empty Object on React useEffect

In my project I have the component ExportSearchResultCSV. Inside this component the nested component CSVLink exports a CSV File.
const ExportSearchResultCSV = ({ ...props }) => {
const { results, filters, parseResults, justify = 'justify-end', fileName = "schede_sicurezza" } = props;
const [newResults, setNewResults] = useState();
const [newFilters, setNewFilters] = useState();
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true)
const [headers, setHeaders] = useState([])
const prepareResults = () => {
let newResults = [];
if (results.length > 1) {
results.map(item => {
newResults.push(parseResults(item));
}); return newResults;
}
}
const createData = () => {
let final = [];
newResults && newResults?.map((result, index) => {
let _item = {};
newFilters.forEach(filter => {
_item[filter.filter] = result[filter.filter];
});
final.push(_item);
});
return final;
}
console.log(createData())
const createHeaders = () => {
let headers = [];
newFilters && newFilters.forEach(item => {
headers.push({ label: item.header, key: item.filter })
});
return headers;
}
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters])
return (
<div className={`flex ${justify} h-10`} title={"Esporta come CSV"}>
{results.length > 0 &&
<CSVLink data={createData()}
headers={headers}
filename={fileName}
separator={";"}
onClick={async () => {
await setNewFilters(filters);
await setNewResults(prepareResults());
await setData(createData());
await setHeaders(createHeaders());
}}>
<RoundButton icon={<FaFileCsv size={23} />} onClick={() => { }} />
</CSVLink>}
</div >
)
}
export default ExportSearchResultCSV;
The problem I am facing is the CSV file which is empty. When I log createData() function the result is initially and empty object and then it gets filled with the data. The CSV is properly exported when I edit this component and the page is refreshed. I tried passing createData() instead of data to the onClick event but it didn't fix the problem. Why is createData() returning an empty object first? What am I missing?
You call console.log(createData()) in your functional component upon the very first render. And I assume, upon the very first render, newFilters is not containing anything yet, because you initialize it like so const [newFilters, setNewFilters] = useState();.
That is why your first result of createData() is an empty object(?). When you execute the onClick(), you also call await setNewFilters(filters); which fills newFilters and createData() can work with something.
You might be missunderstanding useEffect(). Passing something to React.useEffect() like you do
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters]) <-- look here
means that useEffect() is only called, when results or filters change. Thus, it gets no executed upon initial render.

how to display my list from store at initialization with easy-peasy?

I want to get a list of churches from the store at initialization but i can't. The log get my initial array and the new one but doesn't display. Log below:
log
here is my model:
const churchModel = {
items: [],
// ACTIONS
setAllChurches: action((state, payload) => {
state.items = payload;
}),
getInitialChurches: thunk(async (actions) => {
const { data } = await axios.post(
'http://localhost:3000/api/geo/closeto?latlong=2.3522219 48.856614&distance=10000'
);
let array = [];
const resData = data.map(async (index) => {
const res = await axios.get(`http://localhost:3000/api/institutions/all?idInstitution=${index.idInstitution}`);
array.push(res.data[0]);
});
actions.setAllChurches(array);
})
}
and my component:
const ChurchList = () => {
const classes = useStyles();
const setInitialChurches = useStoreActions(action => action.churches.getInitialChurches);
const churches = useStoreState(state => state.churches.items);
const [activeItem, setActiveItem] = React.useState(null);
useEffect(() => {
setInitialChurches()
}, []);
return (
<div className={classes.root} style={{marginTop: '20px',}}>
{ churches.map( (church) => (
<ChurchItem
key={ church.idInstitution }
church={ church }
setActiveItem={setActiveItem}
activeItem={activeItem}
/>)
), console.log(churches)}
</div>
)
};
export default ChurchList;
I tried a useEffect but nothing true. Could you help me please ?
that is not a good location to put console.log in, either put it outside the component render, inside the map or on a useEffect.
You can achieve it by using useEffect and passing churches on the array.
useEffect(() => {
// this will log everytime churches changes / initialized churches
console.log(churches);
}, [churches]);

How can I run multiple queries on load of functional component using UseEffect and get result in render method?

I have the following functional component where, on load of the component, it needs to loop through an array and run some async queries to populdate a new array I want to display in render method.
import React, { useEffect, useState, useContext } from 'react';
import { AccountContext } from '../../../../providers/AccountProvider';
import { GetRelationTableCount } from '../../../../api/GetData';
import { getTableAPI } from '../../../../api/tables';
const RelatedRecordsPanel = (props) => {
const { userTokenResult } = useContext(AccountContext);
const { dataItem } = props;
const [relatedTableItems, setRelatedTableItems] = useState([]);
useEffect(() => {
const tempArray = [];
const schema = `schema_${userTokenResult.zoneId}`;
const fetchData = async () => {
return Promise.all(
dataItem.tableinfo.columns
.filter((el) => el.uitype === 1)
.map(async (itm) => {
const tblinfo = await getTableAPI(itm.source.lookuptableid);
const tblCount = await GetRelationTableCount(
dataItem.tableid,
itm.source.jointable,
schema,
);
const TableIconInfo = { name: tblinfo.name, icon: tblinfo.icon, count: tblCount };
tempArray.push(TableIconInfo);
})
);
};
fetchData();
setRelatedTableItems(tempArray)
}, []);
return (
<div>
{relatedTableItems.length > 0 ? <div>{relatedTableItems.name}</div> : null}
</div>
);
};
In the above code, the queries run correctly and if I do a console.log in the loop, I can see if fetches the data fine, however, the array is always [] and no data renders. How do I write this async code such that it completes the queries to populate the array, so that I can render properly?
Thx!
You aren't using the return value of the Promise.all and since all your APIs are async, the tempArray is not populated by the time you want to set it into state
You can update it like below by waiting on the Promise.all result and then using the response
useEffect(() => {
const schema = `schema_${userTokenResult.zoneId}`;
const fetchData = async () => {
return Promise.all(
dataItem.tableinfo.columns
.filter((el) => el.uitype === 1)
.map(async (itm) => {
const tblinfo = await getTableAPI(itm.source.lookuptableid);
const tblCount = await GetRelationTableCount(
dataItem.tableid,
itm.source.jointable,
schema,
);
const TableIconInfo = { name: tblinfo.name, icon: tblinfo.icon, count: tblCount };
return TableIconInfo;
})
);
};
fetchData().then((res) => {
setRelatedTableItems(res);
});
}, []);

How to set value of react hook in promise?

I am getting a data from the promise and I want to set it in react hook, it does but it makes infinite requests and re-rendering of the page, also I want some specific only data to fill
const [rows, setRows] = useState([]);
useEffect(() => {
async function fetchData() {
myEmploiList({
user: user.id
}).then((data) => {
//setRows(data);
const newData = [];
data.forEach((item, index) => {
newData[index] = {
id: item._id,
name: item.name,
society: item.society
};
setRows(newData);
});
});
}
fetchData();
});
You should add dependencies to your useEffect hook. It is the second argument of this hook.
useEffect(() => {
// your code
}, [deps]);
deps explanation:
no value: will execute effect every time your component renders.
[]: will execute effect only the first time the component renders.
[value1, value2, ...]: will execute effect if any value changes.
For further reading, I highly recommend this blog post.
Move setRows call out of the forEach loop and include user.id into the dependency array
const [rows, setRows] = useState([]);
useEffect(() => {
async function fetchData() {
myEmploiList({
user: user.id
}).then((data) => {
//setRows(data);
const newData = [];
data.forEach((item, index) => {
newData[index] = {
id: item._id,
name: item.name,
society: item.society
};
});
setRows(newData);
});
}
fetchData();
}, [user.id]);

How to test window.open is being called from a react component

I have a react component that renders a series of other components, each with their own checkbox.
There is a state hook called rulesToDownload which begins as an empty array and has ids added to / removed from it as checkboxes are checked / unchecked.
When the 'download' button is clicked, the rulesToDownload array is passed to a data function DownloadFundDataById that forEach's over the array and calls window.open for each value with an api call with the id appended. The data function is imported into the component, not passed in as a prop.
This causes multiple tabs to flash up before closing when the data downloads. It's not perfect but it works.
I want to complete my test coverage and need to test that the function gets called on button click, and that it does what it should.
Any help appreciated.
Code below:
Summary.test.js:
it(`should create correct download array when some rules are selected`, async () => {
global.open = sandbox.spy();
fetch.mockResponseOnce(JSON.stringify(selectedRules));
wrapper = mount(<Summary/>);
await act(async () => {} );
wrapper.update();
wrapper.find('ReportProgressSummary').first().find('input').last().simulate('change', {target: {checked: true}});
wrapper.find('button').first().simulate('click');
expect(global.open).to.have.been.called();
});
I can confirm that all the 'find' statements are correct, and correctly update the checked value.
Summary.js:
const Summary = () => {
const [expand, setExpand] = useState(false);
const [buttonText, setButtonText] = useState("expand other rules");
const [rulesToDownload, setRulesToDownload] = useState([]);
const [data, setData] = useState([]);
const [dataLoadComplete, setDataLoadComplete] = useState(false);
const [dataLoadFailed, setDataLoadFailed] = useState(false);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
let importedData = await ExecuteRules();
setData(importedData);
setDataLoadComplete(true);
} catch (_) {
setDataLoadFailed(true);
}
};
const onButtonClick = () => {
setExpand(!expand);
if(!expand) setButtonText("hide other rules");
else setButtonText("expand other rules");
};
const modifyDownloadArray = (id, checked) => {
let tempArray;
if(checked) tempArray = [...rulesToDownload, id];
else tempArray = [...rulesToDownload.filter(ruleId => ruleId !== id)];
setRulesToDownload([...tempArray]);
};
const dataFilter = (inputData, isFavouriteValue) => {
return inputData.filter(rule => rule.isFavourite === isFavouriteValue)
.sort((a, b) => a.percentage - b.percentage)
.map((rule, i) => {
return <ReportProgressSummary
result={rule.percentage}
id={rule.id}
title={rule.name} key={i}
modifyDownloadArray={modifyDownloadArray}
/>
})
};
return (
<div className="test">
{
dataLoadFailed &&
<div>Rule load failed</div>
}
{
!dataLoadComplete &&
<LoadingSpinnerTitle holdingTitle="Loading rule data..."/>
}
{
dataLoadComplete &&
<Fragment>
<PageTitle title="System Overview"/>
<LineAndButtonContainerStyled>
<ContainerStyled>
{
dataFilter(data, true)
}
</ContainerStyled>
<ContainerStyled>
<ButtonStyled
disabled={!rulesToDownload.length}
onClick={() => DownloadFundDataById(rulesToDownload)}>
download
</ButtonStyled>
</ContainerStyled>
</LineAndButtonContainerStyled>
<LineBreakStyled/>
<ButtonStyled onClick={() => onButtonClick()}>{buttonText}</ButtonStyled>
{
expand &&
<ContainerStyled>
{
dataFilter(data, false)
}
</ContainerStyled>
}
</Fragment>
}
</div>
)
};
export default Summary;
DataMethod.js:
export function DownloadFundDataById(downloadArray) {
downloadArray.forEach(id => window.open(baseApiUrl + '/xxxx/xxxx/' + id));
}
I can confirm the url is fine, just replaced for now
TestSetup:
const doc = jsdom.jsdom('<!doctype html><html><body></body></html>')
global.document = doc;
global.window = doc.defaultView;
configure({ adapter: new Adapter() });
global.expect = expect;
global.sandbox = sinon.createSandbox();
global.React = React;
global.mount = mount;
global.shallow = shallow;
global.render = render;
global.fetch = jestFetchMock;
global.act = act;
chai.use(chaiAsPromised);
chai.use(sinonChai);
chai.use(chaiEnzyme());
chai.use(chaiJestDiff());
console.error = () => {};
console.warn = () => {};
Current test output says that global.open is not being called. I know this makes sense as it isn't actually assigned as a prop to the onClick of the button or anything. This I think is one of my issues - I can't assign a stub to the button directly, but I'm trying not to re-write my code to fit my tests...
Managed to get this working with a couple of updates to my test file:
it(`should create correct download array when some rules are selected`, async () => {
global.open = sandbox.stub(window, "open");
fetch.mockResponseOnce(JSON.stringify(selectedRules));
wrapper = mount(<Summary/>);
await act(async () => {} );
wrapper.update();
wrapper.find('ReportProgressSummary').first().find('input').last().simulate('change', {target: {checked: true}});
wrapper.find('button').first().simulate('click');
expect(global.open).to.have.been.called;
});
the sandbox.spy() was updated to a sandbox.stub() with (window, "open")
thanks to this article for the help!
https://github.com/mrdulin/mocha-chai-sinon-codelab/blob/master/src/stackoverflow/53524524/index.spec.js
Also the expect statement using to.be.called() is actually not a function and so was updated to to.be.called

Categories

Resources